Forking API¶
Live Run Forking lets an agent explore multiple solution branches in parallel and
merge the best result. Enable it via forking=True on
create_deep_agent. See
Live Run Forking for the conceptual overview.
LiveForkCapability¶
pydantic_deep.capabilities.forking.LiveForkCapability
dataclass
¶
Bases: AbstractCapability[Any]
Capability that wires Live Run Forking into an agent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_branches
|
int
|
Maximum branches per fork. |
10
|
max_depth
|
int
|
Maximum fork nesting depth - |
2
|
store
|
ForkStateStore | None
|
Optional :class: |
None
|
test_command
|
str | None
|
Optional shell command run against each branch's
materialised tree during :meth: |
None
|
test_timeout_s
|
float
|
Wall-clock cap (seconds) per branch test run. On
timeout the branch's |
60.0
|
The owning agent reference is set by create_deep_agent() after the
Agent is constructed (mirrors how agent._task_manager is set today).
latest_messages
property
¶
Snapshot of the parent run's most recent message list.
Returns a copy so callers can't mutate the capability's internal state.
Updated on every before_model_request; used by fork_run to
seed each branch's history at the moment of the fork call.
for_run(ctx)
async
¶
Return a fresh per-run capability with an independent coordinator.
Preserves an unresolved coordinator from a previous turn rather
than overwriting it. If the previous parent run forked but did
not call merge_or_select before yielding back to the user,
the coordinator stays on deps.fork_coordinator so the next
turn (or the CLI adopter) can still resolve or abort it; the
new capability clone takes ownership via the capability
back-reference so per-run state (e.g. latest_messages)
flows through the right instance.
after_run(ctx, *, result)
async
¶
Anchor for the post-turn stash protocol - currently a no-op.
The coordinator survives a parent turn ending because
:meth:for_run refuses to overwrite an unresolved one on the
next turn (see above). This hook exists as a documented
anchor so future strategy changes (eager artefact cleanup on
abort, post-turn telemetry, partial-history persistence) plug
in here without restructuring the lifecycle. result is
returned unchanged.
before_model_request(ctx, request_context)
async
¶
Snapshot the latest message list.
For parent runs the snapshot lands on
:attr:_latest_messages so :meth:ForkCoordinator.fork can seed
each branch's history.
For branch runs (identified by ctx.deps._branch_id being
non-None - set by :meth:ForkCoordinator.fork) the snapshot
is forwarded to the parent coordinator via
:meth:ForkCoordinator.capture_partial_history, so that if a
budget watcher cancels the branch the merge resolver still has
a history to return when the branch is picked as winner.
ForkCoordinator¶
pydantic_deep.toolsets.forking.ForkCoordinator
¶
Owns per-branch state for one parent run.
A fresh coordinator is allocated by :meth:LiveForkCapability.for_run,
so concurrent parent runs of the same agent never share state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agent
|
Any
|
The owning agent - used to spawn branch |
required |
parent_deps
|
DeepAgentDeps
|
The parent run's deps; cloned per branch via
:func: |
required |
max_branches
|
int
|
Maximum number of branches per fork. |
required |
max_depth
|
int
|
Maximum fork nesting depth. |
required |
store
|
ForkStateStore
|
The :class: |
required |
checkpoint_store
|
CheckpointStore | None
|
Optional explicit checkpoint store. When |
None
|
test_command
|
str | None
|
Optional shell command run against each branch's
materialised tree during :meth: |
None
|
test_timeout_s
|
float
|
Wall-clock cap (seconds) per branch test run. On
timeout the branch's |
60.0
|
fork_id
property
¶
The active fork's id, or None if fork() has not been called yet.
Exposed so consumers (e.g. the diff_branches tool) can
validate caller-supplied fork_id without reaching into the
coordinator's private _handle attribute.
handle
property
¶
The :class:ForkHandle returned by :meth:fork, or None before fork.
Read-only public accessor for the same value :meth:fork returns.
Cross-package consumers (the CLI adopter, debug inspectors) read this
instead of reaching into _handle.
is_resolved
property
¶
True when the coordinator no longer owns live branch state.
Resolved iff either the coordinator has not yet forked
(_handle is None) or every branch's overlay has been released
(rt.overlay is None) - which only happens inside
:meth:merge_or_select (winner flushed, losers cancelled) and
:meth:aclose (abort). This is the canonical "safe to discard"
signal used by the CLI adopter and :meth:LiveForkCapability.for_run
to distinguish a fork that needs preserving from one that does not.
fork(specs, *, parent_history, isolation=None, strategy=None, aggregate_budget_usd=None)
async
¶
Spawn len(specs) branch tasks and return a handle.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
specs
|
list[BranchSpec]
|
Branch definitions; |
required |
parent_history
|
list[Any]
|
Parent run's message snapshot at fork time. |
required |
isolation
|
BranchIsolation | None
|
Per-branch isolation overrides (defaults to
:class: |
None
|
strategy
|
MergeStrategy | None
|
Merge strategy (currently |
None
|
aggregate_budget_usd
|
float | None
|
Optional fork-wide budget cap; when set,
hitting it terminates every still-running branch with
|
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
ForkBranchLimitError
|
If |
ForkDepthLimitError
|
If parent's |
run_on_branch(branch_id, user_message)
async
¶
Start a new turn on a finished branch with user_message.
The branch must be in done state. Seeds the new turn's message
history from the previous run's all_messages() (which already holds
the full conversation), spawns a new asyncio.Task, replaces
runtime.task, and re-attaches the done-callback so the status
transitions back through running → done.
Returns the spawned task so the caller can await it if needed.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If the branch is still running. |
terminate_branch(branch_id, *, reason=None)
async
¶
Cancel a branch task and mark its terminal status.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch_id
|
str
|
Branch to cancel. |
required |
reason
|
str | None
|
When |
None
|
Only sets a new terminal state when the task is still running; if
it already completed (success / failure / earlier cancellation),
_on_done has already written the correct terminal state and we
must not overwrite it. Idempotent: a second call for the same
branch_id (e.g. when the aggregate watcher races with the
per-branch watcher) is a no-op.
iter_pending_approvals()
¶
Return (branch_id, request) for every branch currently suspended on approval.
The TUI poll loop uses this instead of reading
:attr:BranchRuntime.pending_approval directly, so the coordinator
owns the contract about who may inspect that field.
merge_or_select(action)
async
¶
Resolve the fork by picking a winner.
action="pick:<branch_id>" awaits the winning branch's task,
cancels and discards the others, replays the winner's overlay
onto the parent backend, releases every overlay, and saves a
post-fork:<fork_id> checkpoint when checkpointing is available.
abort_fork()
async
¶
Discard the entire fork without merging - releases overlays, cancels tasks.
Use when every branch has failed (or otherwise become unmergeable)
so the fork can be resolved without picking a winner. Mirrors the
cleanup half of :meth:merge_or_select but without flushing any
overlay onto the parent backend.
Returns the list of branch ids that were aborted. After this
call, :attr:is_resolved becomes True and a new fork can be
started on the same coordinator.
resolve(strategy=None)
async
¶
Dispatch on :attr:MergeStrategy.kind - judge runs for non-manual modes.
"manual"→ early-returnResolveOutcome(committed=False, auto_eligible=False, verdict=None, ...); the caller picks via :meth:merge_or_select."auto"→ judge picks,merge_or_selectfires immediately."auto_with_fallback"→ judge picks. If the combined confidence is at or above :attr:MergeStrategy.confidence_thresholdthe commit is deferred to the caller (committed=False, auto_eligible=True) so the CLI's acceptance widget can offer an override; otherwiseauto_eligible=Falseand the caller opens the manual picker preselected."vote"→ multiple judges evaluate concurrently; majority wins, ties broken by highest individual confidence;merge_or_selectfires immediately on the synthetic majority verdict.
The judge's result.usage() rides on
:attr:ResolveOutcome.judge_usage (summed across judges for
"vote") so the caller can attribute cost without faking a
cost_category field on pydantic-ai-shields' CostTracking.
inspect_branches()
¶
Return a snapshot of every branch's current status.
fork_cost()
¶
Return per-branch and aggregate cost for the active fork.
Returns:
| Type | Description |
|---|---|
ForkCostSummary
|
class: |
ForkCostSummary
|
branch. |
ForkCostSummary
|
branches whose cost is known (skips branches whose model has no |
ForkCostSummary
|
pricing or has not produced a tracked run yet); when no branch |
ForkCostSummary
|
has a known cost, |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If called before :meth: |
capture_partial_history(branch_id, messages)
¶
Record a branch's latest message snapshot for merge fallback.
Called by :class:LiveForkCapability.before_model_request on every
branch run so that, if the branch is later cancelled by a budget
watcher, :meth:merge_or_select has a snapshot to return when the
user picks the exhausted branch as winner. Silently ignored when
the branch is unknown - defensive against late callbacks after
aclose().
aclose()
async
¶
Cancel every outstanding branch task - used on parent cancellation.
Also runs materializer.cleanup() so the on-disk fork directory
is removed on abort (unless keep_artifacts is set), mirroring
the merge-resolution cleanup. Safe to call multiple times.
create_fork_toolset¶
pydantic_deep.toolsets.forking.create_fork_toolset(id='deep-forking')
¶
Build the four-tool forking toolset wired to the per-run coordinator.
ForkStateStore¶
pydantic_deep.toolsets.forking.ForkStateStore
¶
Bases: Protocol
Protocol for fork state storage backends.
Stores ForkHandle records keyed by fork_id. Forks live for the
duration of the parent run; on process restart, in-memory state is lost.
InMemoryForkStateStore¶
pydantic_deep.toolsets.forking.InMemoryForkStateStore
¶
Default in-memory fork state store.
build_diff_report¶
pydantic_deep.toolsets.forking.build_diff_report(fork_id, runtimes, *, paths_filter=None)
¶
Build a :class:BranchDiffReport from a list of branch runtimes.
This is the public entry point for the diff explorer. The
agent-facing :func:diff_branches tool calls into this; downstream
consumers (CLI merge picker, judge) call it directly for
programmatic access.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fork_id
|
str
|
Identifier of the fork being inspected - echoed in the
report's |
required |
runtimes
|
list[BranchRuntime]
|
Branch runtimes to compare; usually |
required |
paths_filter
|
list[str] | None
|
Optional path list; when provided, only these paths
appear in the report. Untouched filtered paths surface as
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
BranchDiffReport
|
class: |
BranchDiffReport
|
filtered path). See module docstring for read-consistency caveat. |
JudgeAgent¶
pydantic_deep.toolsets.forking.JudgeAgent
¶
Thin :class:Agent wrapper that picks the winning branch via structured output.
Holds an internal pydantic-ai :class:Agent with
:class:JudgeVerdict as output_type. The agent is built once at
construction; concurrent evaluate calls reuse the same underlying
agent. The system prompt is the module-level
:data:JUDGE_SYSTEM_PROMPT; per-call context is composed by
:func:_build_judge_prompt.
evaluate(goal, diff_report, outcomes)
async
¶
Run the judge and return (verdict, usage).
usage is the result.usage property value (pydantic-ai exposes
it as a property, not a method) - the coordinator bubbles it up via
:attr:ResolveOutcome.judge_usage for cost attribution. The judge
never receives full per-branch message history, only the goal + diff
+ outcome summaries.
compute_confidence¶
pydantic_deep.toolsets.forking.compute_confidence(signals, judge_confidence)
¶
Combine the three signals with the judge's self-reported confidence.
Heuristic = 0.4 * quality_spread + 0.4 * test_pass_ratio + 0.2 *
internal_consistency. If signals.test_pass_ratio is None the
test slot is treated as 0.0 for the weighted sum AND the heuristic is
capped at 0.65 before multiplying by judge_confidence - the
safety rail that keeps auto_with_fallback defaulting to manual when
the test signal is missing. The product is clamped to [0.0, 1.0].
Coordinator¶
pydantic_deep.toolsets.forking.coordinator.ForkBranchLimitError
¶
Bases: Exception
Raised when a fork_run call exceeds max_branches.
pydantic_deep.toolsets.forking.coordinator.ForkDepthLimitError
¶
Bases: Exception
Raised when a fork_run call from within a branch exceeds max_depth.
Isolation¶
pydantic_deep.toolsets.forking.isolation.BranchOverlay
¶
Copy-on-write backend overlay for a single branch.
Reads consult the overlay first; if a path has not been written in
this branch, the read falls through to the parent backend. Writes go
to the overlay only and are logged to _changes for downstream
consumers (diff builder, materializer, judge).
The overlay implements the subset of :class:BackendProtocol exercised
by branch operations (read, write, edit, ls_info, glob_info,
grep_raw, read_bytes). Forwarding for the latter three merges
overlay and parent results with the overlay taking precedence.
changes()
¶
Return the temporal-ordered list of writes recorded in this overlay.
deleted()
¶
Paths the branch has explicitly removed via :meth:delete.
Returns a copy so callers can't mutate the overlay's internal
tombstone set. Mirrors :meth:changes - same convention.
snapshot(parent_root, *, include_venv=False)
¶
Yield a tempdir presenting an isolated, branch-flavoured view of parent_root.
Parent files are detached file-level copies (the subprocess reads
and may rewrite them in place without ever touching the real parent
file - copying is the core isolation guarantee, see :func:_copy_tree);
overlay writes are materialised as real files; deletions remove the
corresponding entry. The directory is cleaned up on context exit
regardless of how the body returns.
parent_root is taken explicitly rather than read off
:attr:_parent because not every backend has a root_dir
(StateBackend does not); the caller decides whether
snapshotting is meaningful before invoking this.
When include_venv=True and parent_root / ".venv" exists, a
symlink to it is added to the snapshot. .venv is normally in
:data:_SNAP_SKIP_DIRS to keep snapshot creation lean, but a test
runner (pytest, uv run, etc.) typically needs the virtual
environment on PATH - opting in restores it without copying.
Off by default so the existing execute consumer keeps the
slim layout.
delete(path)
¶
Mark path deleted in this branch - propagated on merge.
After this returns, exists / read / read_bytes for
path behave as if the file is gone, even when path lives
in the parent backend. A FileChange(op="delete") is appended
to :attr:_changes so :meth:flush_to can replay the deletion
onto the parent. The parent's bytes are snapshotted on first
touch so third-actor-delete conflict detection works - subject to
the first-touch (not fork-time) limitation documented in
:meth:_snapshot_parent_on_first_touch.
Writing the same path afterwards "un-deletes" it (see :meth:write).
record_mkdir(path)
¶
Record a directory creation detected by _propagate_mutations.
record_rmdir(path)
¶
Record a directory deletion detected by _propagate_mutations.
exists(path)
¶
Public BackendProtocol predicate - overlay first, fall through to parent.
A branch "sees" any file present in either layer, mirroring the
copy-on-write read semantics: written-by-this-branch files take
precedence, otherwise the parent backend answers. Paths the
branch has deleted via :meth:delete - or that live inside a
deleted directory - are hidden regardless of parent presence.
attach_materializer(materializer, branch_label)
¶
Wire a :class:ForkMaterializer into this overlay.
After this call every successful write / edit is mirrored
to disk under the materializer's branches/{branch_label}/
subtree, and the parent backend's bytes for each touched path are
captured lazily on first touch via
:meth:ForkMaterializer.snapshot_parent_path. Note this is a
first-touch snapshot, not a fork-time one - see the limitation in
:meth:_snapshot_parent_on_first_touch for the conflict-detection
gap when a third actor writes a path before this branch touches it.
flush_to(parent, pre_flush_snapshot=None)
¶
Replay this overlay's writes onto parent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parent
|
BackendProtocol
|
Destination backend. Usually the parent run's backend;
for fork-of-fork it is the OUTER branch's overlay (which
is itself a :class: |
required |
pre_flush_snapshot
|
dict[str, bytes | None] | None
|
Optional mapping of |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
FlushReport
|
class: |
FlushReport
|
successfully-replayed path, last-write-wins), |
|
FlushReport
|
(every replayed op - ≥ |
|
FlushReport
|
(divergent paths - these are NOT replayed so the newer parent |
|
FlushReport
|
content is preserved), and |
|
FlushReport
|
|
Order: writes are replayed in :attr:_changes order (temporal),
so a sequence write A → edit A → write B results in
parent reflecting the final overlay state for both A and B.
pydantic_deep.toolsets.forking.isolation.clone_for_branch(deps, isolation)
¶
Clone DeepAgentDeps for a branch according to isolation.
See :class:BranchIsolation for per-flag semantics. Memory isolation
follows the backend (memory lives at
{memory_dir}/{agent_name}/MEMORY.md inside the backend); the
memory flag is recorded for forward-compat but has no separate
effect here. team_bus is a no-op when the teams capability is not
enabled on the parent run; when enabled it propagates the parent bus
reference by default.
Editor¶
pydantic_deep.toolsets.forking.editor.EditorDetector
¶
Static utility - detection and invocation are stateless.
detect(env=None)
staticmethod
¶
Return the kind of editor to use based on env + PATH.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
env
|
Mapping[str, str] | None
|
Optional mapping to read the override variable from; when
|
None
|
invoke(kind, parent, branch_paths, *, custom_cmd=None)
staticmethod
¶
Launch the detected editor against parent and branch_paths.
Returns:
| Type | Description |
|---|---|
list[Popen[bytes]]
|
The list of spawned :class: |
list[Popen[bytes]]
|
for the TUI fallback (caller opens the in-TUI explorer |
list[Popen[bytes]]
|
instead). For PyCharm the list always has length 1; for VS |
list[Popen[bytes]]
|
Code it has length |
list[Popen[bytes]]
|
branch, each diffing parent vs that one branch - VS Code only |
list[Popen[bytes]]
|
does 2-way diff). |
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
EditorKind
|
Editor kind from :meth: |
required |
parent
|
Path | None
|
Path to the materialised parent snapshot, or |
required |
branch_paths
|
list[Path]
|
One path per branch (materialised under
|
required |
custom_cmd
|
str | None
|
Command template used when |
None
|
Judge¶
pydantic_deep.toolsets.forking.judge.count_retry_parts(messages)
¶
Count every RetryPromptPart in messages (any source - stuck-loop or other).
pydantic_deep.toolsets.forking.judge.count_stuck_loop_hits(messages)
¶
Count RetryPromptPart parts in messages that match a stuck-loop marker.
Best-effort heuristic - relies on the marker strings in
:data:_STUCK_LOOP_MARKERS matching the ModelRetry messages raised by
:class:StuckLoopDetection. If the stuck-loop module's message wording
changes, update the markers list.
Materializer¶
pydantic_deep.toolsets.forking.materializer.ForkMaterializer
dataclass
¶
Real-time disk mirror for one fork's overlays.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
Path
|
Base directory - typically |
required |
fork_id
|
str
|
The fork identifier; surfaced in the manifest. |
required |
keep_artifacts
|
bool
|
When |
False
|
The :attr:root directory is created on construction along with its
parent/ and branches/ subdirectories. The manifest is written
eagerly with the initial branch list (whatever the caller passes to
:meth:update_manifest after construction) so the layout is
discoverable even before any writes happen.
parent_path(path)
¶
Return on-disk location of the parent snapshot for path.
branch_path(branch_label, path)
¶
Return on-disk location of a branch's materialised path.
snapshot_parent_path(path, content)
¶
Capture the parent backend's content for path at first touch.
Called lazily on the first overlay write for a path so we don't
eagerly walk the parent backend at fork time (we don't know which
paths will be touched until the branch agent writes them).
content is None when the path didn't exist in the parent
at first touch - recorded as a sentinel so the deletion-by-third-actor
conflict path is detectable later.
Caveat: because the capture happens at first touch rather than at
fork time, a path written by a third actor between the fork and
this branch's first touch is snapshotted with the third actor's
bytes, so :meth:BranchOverlay.flush_to cannot flag it as a
conflict. See
:meth:~pydantic_deep.toolsets.forking.isolation.BranchOverlay._snapshot_parent_on_first_touch.
pre_flush_snapshot()
¶
Return the pre-fork parent snapshot consumed by flush_to.
flush_change(branch_label, change, content)
¶
Mirror one overlay change to disk under branches/{label}/.
flush_delete(branch_label, change)
¶
Remove the on-disk mirror for a deleted path.
Mirrors the branch's end-state so external diff tools comparing
parent/<path> against branches/{label}/<path> see "file
removed in branch". A no-op when the path was never materialised
in this branch.
update_manifest(statuses)
¶
Refresh manifest.json with the latest per-branch status.
cleanup()
¶
Remove the fork directory unless keep_artifacts is set.
Forking Types¶
pydantic_deep.types.BranchSpec
dataclass
¶
Specification for a single branch in a fork.
Attributes:
| Name | Type | Description |
|---|---|---|
label |
str
|
Human-readable branch label (e.g. |
steer |
str
|
First user message delivered to the branch agent - the instruction that differentiates this branch from siblings. |
model |
str | None
|
Optional model override for the branch. |
budget_usd |
float | None
|
Optional per-branch USD budget. Enforced by
:class: |
extra_instructions |
str | None
|
Optional extra instructions appended to the branch's system prompt. |
pydantic_deep.types.BranchState = Literal['running', 'done', 'failed', 'terminated', 'budget_exhausted', 'aggregate_budget_exhausted']
module-attribute
¶
Lifecycle states of a single branch.
budget_exhausted means the per-branch cap was crossed: the watcher
cancels the branch mid-run and the partial-history snapshot becomes the
durable record. aggregate_budget_exhausted means the fork-wide cap
was hit and may interrupt any still-running branch mid-stream.
pydantic_deep.types.BranchStatus
dataclass
¶
Runtime status snapshot of a single branch.
pydantic_deep.types.BranchOutcome
dataclass
¶
Per-branch summary the judge sees in its prompt.
Intentionally narrow - no full message history. The judge sees the
original goal, the structured diff report, and one BranchOutcome
per branch. Keeps the prompt bounded and the cost predictable.
error_count is typed as int but in practice is 0 or 1
per branch (derived from the terminal branch state). Richer tool-level
error counting is a forward-compatible extension.
pydantic_deep.types.BranchIsolation
dataclass
¶
Isolation flags controlling what state branches share with the parent.
Defaults match the project's per-branch isolation policy:
history always copies; backend, memory, todos copy by
default; message_queue is isolated; team_bus is shared
(peer-to-peer bus - branches can talk to each other by default).
Only backend="copy" and message_queue="isolated" are exercised
by the current fork pipeline; the other share / share_readonly
values are accepted for forward compatibility.
pydantic_deep.types.BranchChange
dataclass
¶
One branch's outcome for a single path within a BranchDiffReport.
State-level aggregate: describes the END STATE of a path on a single
branch relative to the parent backend, classified into one
:data:BranchDiffOperation ("created" / "modified" /
"deleted" / "untouched"). The classification is derived from
parent existence + overlay content, NOT from :class:FileChange.op -
a branch that issues an op="write" on a path absent from the
parent yields operation="created"; the same op="write" on a
path present in the parent yields operation="modified".
Not to be confused with :class:FileChange, which is the event-level
log of individual writes/edits/deletes that produced this state.
pydantic_deep.types.BranchCost
dataclass
¶
Per-branch cost snapshot - element of :class:ForkCostSummary.
cumulative_usd is the externally-facing name for the upstream
CostTracking.total_cost value; the rename happens at the
:meth:ForkCoordinator.fork_cost boundary. None indicates pricing
was unavailable for the branch's model (e.g. an unrecognised model in
the genai-prices catalogue) - the branch still runs, but its budget
cap is effectively disabled.
pydantic_deep.types.BranchDiffAgreement = Literal['unanimous_change', 'unanimous_no_change', 'split', 'unique']
module-attribute
¶
Cross-branch classification of a single path's outcomes.
unanimous_change: ≥2 branches touched and all touchers produced identical content.unanimous_no_change: no branch touched the path (only surfaces when an explicitpathsfilter pulls it into the report for transparency).split: ≥2 branches touched and their outcomes differ.unique: exactly one branch touched (others left the path alone).
pydantic_deep.types.BranchDiffOperation = Literal['created', 'modified', 'deleted', 'untouched']
module-attribute
¶
What a single branch did to a given path, relative to the parent backend.
"deleted" surfaces when a branch removed the path — either via the
delete_file agent tool, or via a shell rm invoked through
execute against a :class:~pydantic_ai_backends.LocalBackend parent
(the snapshot mutation tracker propagates the deletion back into the
overlay). The classifier _classify_agreement treats deletions like
any other operation: all-deleters → unanimous_change, mixed →
split, single deleter → unique.
pydantic_deep.types.BranchDiffReport
dataclass
¶
Typed cross-branch diff returned by :func:diff_branches.
Bundles a per-path :class:PathDiff list with a :class:DiffSummary
of aggregate metrics (agreement score, unanimous vs split path
counts, per-branch unique-touch counts) so callers can render or
score the fork's divergence without re-walking individual overlays.
pydantic_deep.types.ConfidenceSignals
dataclass
¶
Three weighted signals combined by compute_confidence.
quality_spread-1 - agreement_scorefrom :class:BranchDiffReport. High = branches diverged meaningfully; weight 0.4.test_pass_ratio- passed / total tests in the winner branch.Nonewhen no per-branch test signal is available; the cap-at-0.65 safety rail kicks in. Weight 0.4.internal_consistency-1 - (retries + stuck_loop_hits) / turnsfor the winner; clamped to[0.0, 1.0]. Weight 0.2.
The pipeline currently ships without a per-branch test-runner hook,
so test_pass_ratio is None in practice; the safety rail caps
the combined heuristic at 0.65 in that case and
auto_with_fallback falls through to manual resolution until a
test-signal hook lands (see follow-ups in the live-fork doc).
pydantic_deep.types.DiffSummary
dataclass
¶
Aggregate metrics for a BranchDiffReport.
agreement_score is 1.0 - split_paths / max(total_paths_touched, 1).
Only contested paths - those touched by more than one branch with
diverging end states (agreement == "split") - count against the
score. Paths touched by exactly one branch (agreement == "unique")
count toward neither agreement nor disagreement, so maximal
non-overlapping divergence (every branch editing different files) yields
agreement_score == 1.0. This is intentional: the metric measures
same-path conflict, not breadth of divergence. per_branch_unique
captures the orthogonal "how much did each branch touch alone" signal.
The judge consumes 1 - agreement_score as quality_spread.
pydantic_deep.types.FileChange
dataclass
¶
Single overlay mutation event recorded by :class:BranchOverlay.
Event-level log entry: one record per successful write, edit,
or delete on the branch overlay. The temporal-ordered list
returned by :meth:BranchOverlay.changes is the data spine consumed
by every downstream consumer of the fork pipeline:
- :func:
build_diff_report- usespathto know which files a branch touched. - :class:
ForkMaterializer- usesopto replaywrite/edit/deletesemantics on the on-disk mirror. - :class:
JudgeAgent- usestimestampfor temporal heuristics when scoring branch outcomes.
Not to be confused with :class:BranchChange, which is a state-level
aggregate describing a branch's per-path outcome relative to the
parent backend ("created" / "modified" / "deleted" /
"untouched"). FileChange logs individual operations;
BranchChange summarises their cumulative effect.
pydantic_deep.types.FlushError
dataclass
¶
One per-write failure observed by :meth:BranchOverlay.flush_to.
flush_to never aborts on the first failure - it accumulates errors
for the caller to surface alongside the changes that did land. Surfaced
on :attr:MergeResult.errors so the CLI / agent can report partial
success without losing track of what didn't apply.
pydantic_deep.types.FlushReport
dataclass
¶
Outcome of replaying a :class:BranchOverlay onto the parent backend.
Produced by :meth:BranchOverlay.flush_to during merge_or_select
when the user picks a winner with default-flush semantics. The fields
flow through to :class:MergeResult so the CLI / agent can render
"Merged: kept branch X · N files applied · conflicts: … · errors: N"
style notifications.
applied_pathslists paths whose final overlay content was written; a path's last write/edit wins, multiple in-overlay edits to the same path collapse to one entry.applied_changescounts every replayed op (≥len(applied_paths)).conflictslists paths where the parent's pre-flush content diverged from the pre-fork snapshot - both modified-by-third-actor and deleted-by-third-actor cases land here. Conflicting paths are NOT replayed onto the parent (non-destructive): the newer parent content is preserved and the path is excluded fromapplied_pathsso the caller can resolve the conflict manually.errorsis one :class:FlushErrorper per-write failure (e.g. parentWriteResult.errornon-empty or parent raised). The failing path is excluded fromapplied_paths; remaining writes still flush.deleted_pathslists paths the branch removed via thedeleteagent tool that were successfully propagated to the parent backend on merge. Paths whose deletion failed (parent raised, or the parent backend cannot delete) land inerrorsinstead.
pydantic_deep.types.ForkCostSummary
dataclass
¶
Output of the fork_cost(fork_id) tool.
Sums :attr:BranchCost.cumulative_usd across branches with a non-None
cost; branches with None are omitted from the aggregate to avoid
misleading partial sums.
pydantic_deep.types.ForkHandle
dataclass
¶
Handle returned by ForkCoordinator.fork() identifying a live fork.
pydantic_deep.types.JudgeVerdict
¶
Bases: BaseModel
Structured output of :meth:JudgeAgent.evaluate.
Pydantic BaseModel (not dataclass) because pydantic-ai's output_type
contract requires a BaseModel-shaped schema for structured output.
pydantic_deep.types.MergeResult
dataclass
¶
Result returned by ForkCoordinator.merge_or_select().
pydantic_deep.types.MergeStrategy
dataclass
¶
How a fork is resolved into a single winning branch.
:attr:kind selects the resolution mode:
"manual"- caller picks viamerge_or_select(action="pick:<id>")."auto"- :class:JudgeAgentpicks; coordinator commits immediately."auto_with_fallback"- judge picks; if effective confidence is at or above :attr:confidence_thresholdthe commit is deferred to the caller (so the acceptance widget can offer an override), otherwise the caller opens the manual picker with the judge's pick preselected. This is the default."vote"- multiple judges (default: Haiku + GPT-mini + Gemini Flash) evaluate independently; majority wins, tie broken by highest confidence; coordinator commits immediately.
pydantic_deep.types.PathDiff
dataclass
¶
Per-path slice of a BranchDiffReport: parent state + every branch's outcome.
pydantic_deep.types.ResolveOutcome
dataclass
¶
Outcome envelope returned by :meth:ForkCoordinator.resolve.
Three commit semantics live behind the same envelope so the caller (CLI) can branch cleanly:
committed=True: the coordinator already ranmerge_or_select; see :attr:merge_result. Modes that hit this path:"auto"and"vote".committed=False, auto_eligible=True: above-threshold"auto_with_fallback". Commit is deferred to the caller so the acceptance widget can offer an[o]override before the merge fires.committed=False, auto_eligible=False: below-threshold"auto_with_fallback"(caller opens picker preselected) OR"manual"(no judge ran, caller picks).
:attr:judge_usage carries the judge's result.usage (summed across
judges in vote mode) so the caller can attribute the cost separately
from branch-agent usage, without extending pydantic-ai-shields'
:class:CostTracking API with a new cost category.