fractal.core package#
Submodules#
fractal.core.agent module#
Base classes for agents.
- fractal.core.agent.command_base(command)[source]#
Return an agent command’s base word, refusing shell quoting.
Commands split on whitespace into argv words (
Agent.parts) with no shell interpretation, so a quoted argument would silently mangle into garbage words (--flag "two words"becomes['--flag', '"two', 'words"']) deep inside the loop’s tmux pane, after start already printed its success – refuse quoting at the accept-time gates instead, where the error still reaches the caller’s terminal.- Parameters:
command (
str) – Full agent command (e.g.'claude --some-flag').- Return type:
str- Returns:
The command’s first whitespace-separated word.
- Raises:
ValueError – If the command carries a quote or backslash.
- fractal.core.agent.resolve(name, *, root=None)[source]#
Return the
Agentclass registered for a base command.Consults the deployment hook file under
rootonce per process (its subclasses register byname), then the registry, importing backend modules at call time (the sanctioned lazy upward handle).- Parameters:
name (
str) – Agent base command (e.g.claude).root (
Path|None) – Tree data dir whose deployment hook file to consult.
- Return type:
type[Agent]- Returns:
The registered
Agentsubclass.- Raises:
ValueError – If no backend is registered under
name, or the registered backend module cannot be imported.RuntimeError – If the tree’s deployment hook file fails to load (sticky – re-raised on every later resolve for that tree).
- fractal.core.agent.register(name, target)[source]#
Register (or override) an agent backend under a base command.
The in-process injection point: an embedding application registers a custom subclass at import time. Explicit registrations win over the deployment hook file.
- Parameters:
name (
str) – Agent base command to register under.target (
str|type[Agent]) –Agentsubclass, or a'module:Class'string imported at resolve time.
- Return type:
None
- fractal.core.agent.supported()[source]#
Return the registered base commands, registration order.
- Return type:
tuple[str,...]
- class fractal.core.agent.Invocation(agent, argv, cwd, env=None, session=None)[source]#
Bases:
objectA spawnable agent invocation.
An
_invocationhook setsenvto only its reserved provider keys (orNone); the publicinvocationverb composes the full Popen environment (os.environ, then the caller overlay, then those reserved keys), so a composed invocation’s env passes straight toPopenandNoneinherits the parent’s. A reserved key valuedNonemeans unset: the verb pops it from the merged environment, so a hook can scrub keys the ambient environment inherited from an ancestor’s route.sessioncarries the caller-minted id for providers that do not mint their own; the launcher must pre-stamp it on the step row before spawning, so a boot-window abort still leaves a resumable session. Pure serializable data: a host can ship it to any executor.- Parameters:
agent (
str)argv (
tuple[str,...])cwd (
Path)env (
Mapping[str,str|None] |None)session (
str|None)
- agent: str#
- argv: tuple[str, ...]#
- cwd: Path#
- env: Mapping[str, str | None] | None = None#
- session: str | None = None#
- class fractal.core.agent.StreamEvent(kind, text=None, tool=None, session=None, model=None, cost=None, final=False, duration=None, turns=None, budget_stopped=False, failed=False, message=None)[source]#
Bases:
objectOne semantic frame parsed from an agent’s output stream.
kindis one of:'session' | 'text' | 'tool' | 'tool_result' | 'cost' | 'result' | 'error'. Fields are populated per kind; absent facts stayNone(a codex result carries no cost). Consumers (renderer, TUI bubbles, record verbs) branch onkindonly – never on the provider.- Parameters:
kind (
str)text (
str|None)tool (
str|None)session (
str|None)model (
str|None)cost (
float|None)final (
bool)duration (
float|None)turns (
int|None)budget_stopped (
bool)failed (
bool)message (
str|None)
- kind: str#
- text: str | None = None#
- tool: str | None = None#
- session: str | None = None#
- model: str | None = None#
- cost: float | None = None#
- final: bool = False#
- duration: float | None = None#
- turns: int | None = None#
- budget_stopped: bool = False#
- failed: bool = False#
- message: str | None = None#
- class fractal.core.agent.StreamResult(session, model, cost, budget_stopped=False)[source]#
Bases:
objectOutcome of one streamed invocation.
- Parameters:
session (
str|None)model (
str|None)cost (
float|None)budget_stopped (
bool)
- session: str | None#
- model: str | None#
- cost: float | None#
- budget_stopped: bool = False#
- class fractal.core.agent.StreamParser(*, model=None)[source]#
Bases:
objectIncremental parser for one agent output stream.
Pure protocol: subclasses own the full wire format (JSON decode included) – deliberate sibling duplication over premature hoisting. Parsers never touch the ledger; recording is the driver’s (
Agent.stream). State accumulates acrossfeedcalls so callers can attribute a step after draining.- Parameters:
self (
StreamParser)model (
str|None)
Initialize
StreamParser.- Parameters:
self (
StreamParser)model (
str|None)
- session: str | None#
- cost: float | None#
- final: bool#
- budget_stopped: bool#
- errors: list[str]#
- feed(line)[source]#
Parse one stdout line into normalized events. Hook for backends.
- Parameters:
self (
StreamParser)line (
str)
- Return type:
list[StreamEvent]
- class fractal.core.agent.Agent(node, command=None, provider=None)[source]#
Bases:
objectBase class for agent CLI backends.
An agent owns its own conversation loop inside a provider subprocess: the contract is capabilities – invocation building, spawning, stream parsing, session policy, cost mode – never a message list. Sync-only by design: agents are subprocesses driven line-by-line (the TUI streams on a worker thread); an async variant arrives with its first consumer, never speculatively.
Backends override only
_-hooks and capability attributes; public verbs own validation, events, and the settle invariants. Override theon_verbhooks (calling super and returning the event) to ship observability to a host application – the package emits to named loggers and never configures handlers, so the host owns logging config.Initialize
Agent.- Parameters:
node (
Node) – The node this agent runs for (config dirs, worktree, ledger).command (
str|None) – Full agent command (e.g.'claude --some-flag'); defaults to the node’s configured agent. Extra words are spliced into every invocation.provider (
str|None) – Provider-route override;Nonemeans native (Node.agentthreads the node’s effective configured route).self (
Agent)
- name: str = ''#
- config_file: str = ''#
- can_fork: bool = False#
- mints_session: bool = False#
- needs_pricing: bool = False#
- cost_scope: str = 'call'#
- enforces_budget: bool = False#
- results_per_step: bool = False#
- providers: tuple[str, ...] = ()#
- property parts: list[str]#
Return the configured command split into argv words. :type self:
Agent:param self:
- property config_dir: Path#
Return the node’s per-agent config dir (
<node_dir>/.<name>). :type self:Agent:param self:
- property err_path: Path#
Return the stderr log path (
<node_dir>/<name>.err). :type self:Agent:param self:
- property provider: str | None[source]#
Return the bound provider route the caller threaded, or
None.Nonemeans the vendor’s own endpoint.Node.agentthreads the node’s effective configured route (per-step frontmatter rides the same channel), so accessor-built backends carry it here; a directly-constructed backend is native unless its caller passes one – the binder never reads node state, so a throwaway node (chat doubles, hosts) constructs and parses freely. A backend with no routes (providers == ()) has no route axis, so an openrouter-defaulting ancestor never pins a route-less descendant. The spawn verbs validate membership.
- log(message, level=20)[source]#
Log a message to this agent’s logger.
- Parameters:
self (
Agent)message (
str)level (
int)
- Return type:
None
- invocation(prompt, *, session=None, fork=False, model=None, effort=None, budget=None, env=None)[source]#
Build one spawnable invocation. Delegates to
_invocation.A pure builder pair – the observable moment is
spawn.session=Nonestarts fresh; a session resumes in place, or forks withfork=True(the backend’sNotImplementedErrorsurfaces for non-forking agents – one raise site with its citations). Mints the fresh session id centrally when the caller owns minting (uuid4; already lowercase).- Parameters:
prompt (
str) – The prompt text (the subprocess’s only input).session (
str|None) – Session to resume or fork;Nonestarts fresh.fork (
bool) – Forksessioninstead of resuming it in place.model (
str|None) – Model override for this invocation.effort (
str|None) – Reasoning-effort override, passed through to the provider’s effort flag unvalidated (the agent’s own error surfaces for an unknown level).budget (
float|None) – Hard budget in USD (enforcing agents only).env (
dict[str,str] |None) – Caller env overlay, merged here in the public verb – baseos.environ, then the overlay, then_invocation’s provider-specific keys, with any key whose merged value isNonepopped;spawnkwargs stay supervision-only.self (
Agent)
- Return type:
- Returns:
The spawnable invocation.
- Raises:
ValueError – For an unsupported provider route, a budget on a non-enforcing agent, or fork without a session.
- spawn(invocation, **kwargs)[source]#
Spawn an invocation: fires
on_spawn, delegates to_spawn.Callers own supervision policy via
**kwargs(the loop passesstart_new_session=Trueand a stderr file; chat inherits stderr; the TUI pipes it). All launch surfaces routePopenthrough here, so overriding_spawnis the single sanctioned way for a host to redirect execution (a wrapper binary, a container, a remote executor).- Parameters:
self (
Agent)invocation (
Invocation)kwargs (
Any)
- Return type:
Popen
- parser(*, model=None)[source]#
Return a fresh stream parser (constructed from
__parser__).- Parameters:
self (
Agent)model (
str|None)
- Return type:
- stream(lines, *, step_id=None, model=None, detached=False, render=None)[source]#
Drive a parser over the agent’s stdout lines.
Fires
on_callat entry andon_call_success/on_call_failureat exit; dispatches each parsed event to its hook (on_sessiononce,on_actionper tool,on_errorper stream-borne failure,on_budgeton a budget stop), the render callback, and – whenstep_idis given – the record verbs (session stamped as the stream opens; each cost figure flushed immediately, so a signal-killed reader still recorded the last one). Text deltas fire no hook (render only).- Parameters:
lines (
Iterable[str]) – The agent’s stdout, line by line.step_id (
int|None) – Step row to record against;Nonerecords nothing (a live chat writes nothing).model (
str|None) – Configured-model fallback for the parser (the stream-reported model overwrites it).detached (
bool) – Suppress the.sessionmap persistence (the step-row stamp always lands).render (
Callable[[StreamEvent],None] |None) – Presentation callback receiving every parsed event (CLI ANSI, TUI bubbles, loop pane).self (
Agent)
- Return type:
- Returns:
The stream outcome, read off the drained parser state.
- Raises:
AgentStreamError – When the stream carried error frames (codex errors ride the JSON stream and must fail the step even after a fully drained stdout).
- record_session(step_id, session, *, model=None, detached=False)[source]#
Record a captured session.
Fires
on_record_sessionwith exactly the persisted values, then delegates persistence to_record_session.- Parameters:
step_id (
int) – Step to stamp.session (
str) – Real, agent-specific session.model (
str|None) – The model that ran the step – stream-reported where the agent names it, else the configured one.detached (
bool) – Suppress the.sessionmap persistence.self (
Agent)
- Return type:
None
- record_cost(step_id, cost, *, final=False)[source]#
Record a cost figure.
Applies the scope settle, fires
on_record_costwith the settled amount, and delegates persistence to_record_cost.'call'scope writes figures as-is;'thread'scope settles the cumulative total against prior sibling steps sharing the session. The settle lives here, in the public verb, so a billing override cannot fork provider math; the event and the hook both see exactly what lands on the ledger.Noneis never recorded – a NULL cost means unknowable, never $0.- Parameters:
step_id (
int) – Step to record against.cost (
float) – Cost in USD – per-invocation for'call'scope, the cumulative thread total for'thread'scope.final (
bool) – Whether the figure is authoritative (a result frame) rather than a running estimate.self (
Agent)
- Return type:
None
- tracks_cost(model=None)[source]#
Return whether spend will be recorded.
Never conflates zero with unknowable: cost-reporting agents always track; token-priced agents track only a model present in the pricing cache.
- Parameters:
self (
Agent)model (
str|None)
- Return type:
bool
- config_model()[source]#
Return the model this agent’s own per-node config defaults to.
- Parameters:
self (
Agent)- Return type:
str|None
- transcript(session)[source]#
Resolve a session’s transcript for this node.
Providers persist a session as a JSONL file that grows while the session runs, so polling this returns the live transcript.
pathrides along so a caller can tail or range-read the file as it grows – it may be the expected location even while absent, so a poller can wait for it to appear.- Parameters:
session (
str) – The agent session id (asessionvalue off a step or iteration row).self (
Agent)
- Return type:
dict[str,Any]- Returns:
{agent, session, path, exists, content}–contentis the raw JSONL text (empty when absent).- Raises:
ValueError – If
sessionis not a bare session id (anything but[A-Za-z0-9_-]could escape the transcript directory).
- preflight(model=None)[source]#
Fail fast before a run.
Checks the agent binary is on
PATH, fireson_preflight, and delegates provider probes to_preflight. Abort semantics stay with the caller.- Parameters:
model (
str|None) – The model the run will use, for provider probes.self (
Agent)
- Raises:
RuntimeError – For an unsupported provider route, a missing binary, or a provider probe relaying the provider’s own diagnostics.
- Return type:
None
- classmethod seed(node_dir, *, parent_dir=None, reset=False)[source]#
Seed the per-node agent config dir.
Creates the dir (wiping first on
reset), copies the config file – the parent node’s live copy wins over the package seed, and an existing file is never overwritten – symlinks skills, then delegates provider extras to_seed. A backend without a package seed still gets its dir and skills link, just no config file.- Parameters:
node_dir (
Path) – The node data directory to seed under.parent_dir (
Path|None) – The parent node’s data directory, when one exists.reset (
bool) – Wipe the agent dir before seeding.
- Return type:
None
- on_call(message=None, *, logging_level=20, **kwargs)[source]#
Handle agent call event.
Fired when consumption of an invocation’s stream begins.
- Parameters:
self (
Agent)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- on_call_success(message=None, *, logging_level=10, **kwargs)[source]#
Handle agent call success event.
Fired after an agent stream drains cleanly.
- Parameters:
self (
Agent)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- on_call_failure(message=None, *, logging_level=30, **kwargs)[source]#
Handle agent call failure event.
Fired when an agent stream fails.
- Parameters:
self (
Agent)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- on_spawn(message=None, *, logging_level=20, **kwargs)[source]#
Handle agent spawn event.
Fired when an invocation is spawned.
- Parameters:
self (
Agent)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- on_action(message=None, *, logging_level=20, **kwargs)[source]#
Handle agent action event.
Fired when the agent invokes a tool.
- Parameters:
self (
Agent)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- on_session(message=None, *, logging_level=20, **kwargs)[source]#
Handle agent session event.
Fired when a session id is captured or minted.
- Parameters:
self (
Agent)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- on_record_session(message=None, *, logging_level=20, **kwargs)[source]#
Handle session record event.
Fired by
record_sessionwith exactly the persisted values.- Parameters:
self (
Agent)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- on_record_cost(message=None, *, logging_level=10, **kwargs)[source]#
Handle cost record event.
Fired by
record_costwith the settled figure that lands on the ledger.- Parameters:
self (
Agent)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- on_budget(message=None, *, logging_level=30, **kwargs)[source]#
Handle agent budget event.
Fired when the agent stops on its hard budget.
- Parameters:
self (
Agent)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- on_error(message=None, *, logging_level=30, **kwargs)[source]#
Handle agent error event.
Fired for each stream-borne error frame.
- Parameters:
self (
Agent)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- fractal.core.agent.seed_agents(node_dir, *, parent_dir=None, reset=False)[source]#
Seed every registered agent’s config dir plus the neutral one.
- Parameters:
node_dir (
Path) – The node data directory to seed under.parent_dir (
Path|None) – The parent node’s data directory, when one exists.reset (
bool) – Wipe each agent dir before seeding.
- Return type:
None
- class fractal.core.agent.AgentCallEvent(source, message=None, **kwargs)[source]#
Bases:
InitialEventEmitted when consumption of an agent invocation’s stream begins.
- Parameters:
self (
Event)source (
Any)message (
str|list[str] |None)kwargs (
Any)
Initialize
Event.- Parameters:
source (
Any) – The object that emitted this event.message (
str|list[str] |None) – Optional caller-supplied free-form context to fold into the event description.self (
Event)kwargs (
Any)
- session: str | None#
- model: str | None#
- class fractal.core.agent.AgentCallSuccessEvent(source, **kwargs)[source]#
Bases:
TerminalEventEmitted after an agent stream drains cleanly.
- Parameters:
self (
TerminalEvent)source (
Any)kwargs (
Any)
Initialize terminal event.
Pairs with the corresponding
initial_eventand computesdurationas the elapsed seconds between the two events.- Parameters:
source (
Any) – The object that emitted this event.self (
TerminalEvent)kwargs (
Any)
- class fractal.core.agent.AgentCallFailureEvent(source, **kwargs)[source]#
Bases:
FailureEventEmitted when an agent stream fails.
- Parameters:
self (
TerminalEvent)source (
Any)kwargs (
Any)
Initialize terminal event.
Pairs with the corresponding
initial_eventand computesdurationas the elapsed seconds between the two events.- Parameters:
source (
Any) – The object that emitted this event.self (
TerminalEvent)kwargs (
Any)
- class fractal.core.agent.AgentSpawnEvent(source, message=None, **kwargs)[source]#
Bases:
EventEmitted when an invocation is spawned.
- Parameters:
self (
Event)source (
Any)message (
str|list[str] |None)kwargs (
Any)
Initialize
Event.- Parameters:
source (
Any) – The object that emitted this event.message (
str|list[str] |None) – Optional caller-supplied free-form context to fold into the event description.self (
Event)kwargs (
Any)
- invocation: Invocation#
- class fractal.core.agent.AgentActionEvent(source, message=None, **kwargs)[source]#
Bases:
EventEmitted when the agent invokes a tool.
Observation only – the subprocess executes its own tools, so there is no success/failure pairing.
- Parameters:
self (
Event)source (
Any)message (
str|list[str] |None)kwargs (
Any)
Initialize
Event.- Parameters:
source (
Any) – The object that emitted this event.message (
str|list[str] |None) – Optional caller-supplied free-form context to fold into the event description.self (
Event)kwargs (
Any)
- tool: str#
- class fractal.core.agent.AgentSessionEvent(source, message=None, **kwargs)[source]#
Bases:
EventEmitted when a session id is captured, minted, or recorded.
- Parameters:
self (
Event)source (
Any)message (
str|list[str] |None)kwargs (
Any)
Initialize
Event.- Parameters:
source (
Any) – The object that emitted this event.message (
str|list[str] |None) – Optional caller-supplied free-form context to fold into the event description.self (
Event)kwargs (
Any)
- session: str#
- model: str | None#
- class fractal.core.agent.AgentCostEvent(source, message=None, **kwargs)[source]#
Bases:
EventEmitted when a settled cost figure is flushed to the ledger.
- Parameters:
self (
Event)source (
Any)message (
str|list[str] |None)kwargs (
Any)
Initialize
Event.- Parameters:
source (
Any) – The object that emitted this event.message (
str|list[str] |None) – Optional caller-supplied free-form context to fold into the event description.self (
Event)kwargs (
Any)
- cost: float#
- final: bool#
- class fractal.core.agent.AgentBudgetEvent(source, message=None, **kwargs)[source]#
Bases:
EventEmitted when the agent stops on its hard budget.
- Parameters:
self (
Event)source (
Any)message (
str|list[str] |None)kwargs (
Any)
Initialize
Event.- Parameters:
source (
Any) – The object that emitted this event.message (
str|list[str] |None) – Optional caller-supplied free-form context to fold into the event description.self (
Event)kwargs (
Any)
- class fractal.core.agent.AgentErrorEvent(source, message=None, **kwargs)[source]#
Bases:
EventEmitted when a stream-borne error frame arrives.
- Parameters:
self (
Event)source (
Any)message (
str|list[str] |None)kwargs (
Any)
Initialize
Event.- Parameters:
source (
Any) – The object that emitted this event.message (
str|list[str] |None) – Optional caller-supplied free-form context to fold into the event description.self (
Event)kwargs (
Any)
- error: str#
- class fractal.core.agent.AgentPreflightEvent(source, message=None, **kwargs)[source]#
Bases:
EventEmitted before a preflight probe runs.
- Parameters:
self (
Event)source (
Any)message (
str|list[str] |None)kwargs (
Any)
Initialize
Event.- Parameters:
source (
Any) – The object that emitted this event.message (
str|list[str] |None) – Optional caller-supplied free-form context to fold into the event description.self (
Event)kwargs (
Any)
fractal.core.commit module#
Functions for the work-product commit pipeline.
- fractal.core.commit.commit(node, message, *, init=False, check=False, ignore_scope=False, force=False, body=None, iteration=None, run_id=None, iter_id=None, step_id=None)[source]#
Commit a node’s current iteration work.
The pipeline half of
Node.commit()(which owns the flag validation): scope check, wiki index refresh, lint, stage, commit, and push unlesslocalis set.- Parameters:
node (
Node) – The node whose worktree to commit.message (
str) – Short description appended to the commit message.init (
bool) – Use theinitlabel instead ofiteration <run>.<iter>.check (
bool) – Error if uncommitted changes exist instead of committing.ignore_scope (
bool) – Commit out-of-scope changes but still lint.force (
bool) – Bypass scope and lint checks and git hooks.body (
str|None) – Optional paragraph below the subject; aforcecommit’s body also folds in the staged-sweep warnings and a capped diffstat, so a backstop save describes itself.iteration (
int|None) – Commit-message iteration number (the loop passes the live one); resolved from the node’s open iteration row, else0, when omitted.run_id (
int|None) – Run the commit event belongs to (also run-qualifies the subject’s iteration label).iter_id (
int|None) – Iteration the commit event belongs to.step_id (
int|None) – Step the commit event belongs to.
- Return type:
str- Returns:
Commit output and notices.
- Raises:
RuntimeError – On a dirty
--check, an out-of-scope change, a failed wiki index refresh, a lint failure, a failed commit, or amessagecarrying the subject’s own labels (the branch name oriteration).
- fractal.core.commit.commit_user_init(node, message)[source]#
Commit a user node’s baseline: the project wiki (and node data when tracked).
The
--initbaseline is the only commit a user node takes. By default the node’s own.fractal/data is git-excluded on the top-level branch, so this stages only the project wiki (under<project>/for a sub-project) and the.gitattributesmerge attributewiki initwrote; on a tree opted in viafractal trackthe node’s seed dir rides along too. Everything is committed with a pathspec, so the user’s other staged work is never swept in, and does not push. A node worktree branched later then starts from a committed tree.- Parameters:
node (
Node) – The user node to baseline.message (
str) – Short description appended to the commit message.
- Return type:
str- Returns:
Confirmation message.
fractal.core.config module#
Implements Config class.
- class fractal.core.config.Config(node)[source]#
Bases:
objectRead/write surface for a node’s
config.json.Reads from disk on every access – nothing is cached – so writes from other processes (CLI commands, lifecycle scripts) are immediately visible.
Initialize
Config.- load()[source]#
Read and parse the node’s
config.json.A hand-corrupted config otherwise yields a context-free
Expecting value: line 1 column 1from every command that touches it; this re-raises with the file path so the error points at what to fix.- Return type:
dict[str,Any]- Returns:
The parsed config mapping.
- Raises:
ValueError – When
config.jsondoes not hold valid JSON.- Parameters:
self (
Config)
- get(key, default=None)[source]#
Read a config value.
- Parameters:
key (
str) – Config key.default (
Any) – Value to return when the key or config file is missing.self (
Config)
- Return type:
Any- Returns:
Config value, or
defaultif missing.
- set(key, value)[source]#
Read-merge-write one config key.
The read-merge-write holds a per-config kernel flock, so setters in concurrent processes never revert each other’s keys.
- Parameters:
key (
str) – Config key.value (
Any) – Value to write.self (
Config)
- Raises:
ValueError – When
keyis fixed at init (IMMUTABLE_KEYS) andvaluewould change its stored value.- Return type:
None
- validate(config=None)[source]#
Validate the launch-time config invariants the loop depends on.
The one launch validator. Checks only the keys present (and not
None) inconfig, so it can be called with a node’s effective (merged) config;Nonevalidates the storedconfig.json– theNode.start()re-check, since the documented steering path edits the file directly, bypassing the init/update setters’ checks. Rejects a non-numeric or non-finite cost (NaN/Infinity slip past every comparison), a non-positive ceiling (which makes the subtree check finish the node at $0), a per-iter/step cap with no ceiling (which the loop cannot enforce once the per-iter budget drains), an out-of-range reserve, a brokenstep <= iter <= runcost ordering, a non-integer or degenerate integer cap (a non-positivemax_itersreads as unlimited in the loop), a non-bool mode flag (the loop’sbool()coercion reads a hand-edited"false"string asTrue), a bare-number or zero-truncating duration (which bricks the loop at launch or at its mid-run re-reads), and an absolute or..scope root (which never matches the commit pipeline’s relative prefix check, bricking every scoped commit).- Parameters:
config (
dict[str,Any] |None) – The config mapping to validate; the storedconfig.jsonif omitted.self (
Config)
- Raises:
ValueError – On any violated invariant.
- Return type:
None
- reconcile()[source]#
Reconcile the registry cap row to this node’s config file.
The loop enforces the caps in
config.json, so a post-spawn config edit is live budget truth – but thenodesregistry row keeps the spawn-time values, and a registry reader can kill a node at a stale cap. Config wins: drifted caps are pushed over the registry row and reported. Keys absent from the config are left alone (a registry-only cap is its own problem, not drift);fractal node updatewrites both sides in one step and remains the supported retune path.- Return type:
dict[str,tuple[Any,Any]]- Returns:
Mapping of drifted key to
(config, registry)values – empty when nothing drifted or the node has no registry row (user/root node).- Parameters:
self (
Config)
fractal.core.cost module#
Implements Cost class.
- class fractal.core.cost.Cost(node)[source]#
Bases:
objectCost ledger readers over config caps and step-cost rows.
Initialize
Cost.- remaining(*, run_id=None, iter_id=None, step_id=None)[source]#
Compute remaining cost budget for a run, iteration, or step.
--max-costis a per-run ceiling: by default this returnsmax_costminus the current run’s subtree spend (own steps plus descendants chained byparent_run_id; the active run, else the most recent), so a budget drained in one run is fresh again in the next. Passrun_idfor a specific run.iter_id/step_idinstead scope to the matching per-level cap (max_iter_cost/max_step_cost). Reflects recorded step costs only – the active step counts whatever cost is already flushed to its row, not its unflushed in-progress accrual. The run budget is subtree-shared with no reserved self-slice: a manager that sizes children to its full remaining leaves itself none and can be starved out of its own merge-up iteration – size children below the remaining if the manager must keep working after spawning.- Parameters:
run_id (
int|None) – A run to scope to; the current run if omitted.iter_id (
int|None) – Scope to an iteration’smax_iter_costheadroom.step_id (
int|None) – Scope to a step’smax_step_costheadroom.self (
Cost)
- Return type:
float|None- Returns:
Remaining cost in USD, or
Noneif the relevant cap is not configured (max_cost/max_iter_cost/max_step_cost).
- spent(*, run_id=None, iter_id=None, step_id=None, max_depth=None)[source]#
Return the total cost for a run, iteration, step, or per-run subtree.
Sums the current run’s direct step cost by default (the active run, else the most recent) – the per-run reading behind
--runscoping. Passrun_idfor a specific run. Includes child node costs: a child counts only the runs it spawned under this run’s lineage, chained viaparent_run_id(the per-run subtree) – a deleted child’s recorded runs still count. Passmax_depth=0for this node only,max_depth=1to include children, etc.When
iter_id(orstep_id) is given, returns cost for that iteration (or step) only (children are not included).- Parameters:
run_id (
int|None) – A run to scope to; the current run if omitted.iter_id (
int|None) – Scope to a specific iteration.step_id (
int|None) – Scope to a specific step.max_depth (
int|None) – Maximum child depth to include.Noneincludes all descendants,0is direct cost only.self (
Cost)
- Return type:
float- Returns:
Cost in USD (0.0 if no data).
- untracked(*, run_id=None, iter_id=None, step_id=None, max_depth=None)[source]#
Return whether a scope’s spend is untracked rather than genuinely zero.
A token-priced agent with no priced model records
NULLcost, so its spend sums to0yet is not actually$0. ReturnsTruewhen the scope has unknowable (NULL-cost) steps and none recorded a real figure, letting the CLI showuntrackedinstead of$0so a parent can tell “spent nothing” from “untrackable”. Zero-cost rows (never-launched steps, and a flushed genuine $0) are neutral: they prove nothing spent, not that spend was tracked. The run scope mirrorsspent: it walks the per-run subtree (tomax_depth) so a fully-untracked child reads as untracked at the parent, not as$0– a mixed subtree (any real figure) is tracked. The iteration/step scope is own steps only (no children).- Parameters:
run_id (
int|None) – A run to scope to; the current run if omitted.iter_id (
int|None) – Scope to a specific iteration.step_id (
int|None) – Scope to a specific step.max_depth (
int|None) – Maximum child depth to include for the run scope (Noneall descendants,0this node only).self (
Cost)
- Return type:
bool- Returns:
Trueif the scope hasNULL-cost steps and no real figure.
- unpriced(*, run_id=None, iter_id=None, step_id=None, max_depth=None)[source]#
Count a scope’s ended steps that recorded no cost.
SUM()skips NULL-cost rows without a trace, so ledger-facing readings disclose this count when nonzero. NULL means the spend is unknowable – a launched step whose usage never flushed: kills before the first usage flush (markedunpricedon their metadata), post-launch pre-stream deaths, and untracked-agent rows. A step the loop closes without reaching its launch records an explicit zero instead (out-of-band crash heals still land here: a healed pre-launch row is indistinguishable from a launched pre-stream death). Ended rows only – an open step is merely not priced yet. Scopes mirrorspent.- Parameters:
run_id (
int|None) – A run to scope to; the current run if omitted.iter_id (
int|None) – Scope to a specific iteration.step_id (
int|None) – Scope to a specific step.max_depth (
int|None) – Maximum child depth to include for the run scope (Noneall descendants,0this node only).self (
Cost)
- Return type:
int- Returns:
Number of ended steps in the scope with
NULLcost.
- breakdown(*, run_id=None, max_depth=None)[source]#
Map each in-subtree descendant branch to its own spend in a run.
Walks the
parent_run_idchain fromrun_id(the current run – active, else most recent – when omitted): a branch maps to its own step cost across the runs it spawned, directly or transitively, under that run – a deleted descendant’s recorded runs still count. Branches with no run in the lineage are absent (callers treat a missing branch as0). Powersfractal node cost breakdown.- Parameters:
run_id (
int|None) – Run to scope to; the current run if omitted.max_depth (
int|None) – Maximum descendant depth to include (1= direct children only); all descendants if omitted.self (
Cost)
- Return type:
dict[str,float]- Returns:
{branch: own cost in USD}for each in-subtree descendant.
- rows(*, run_id=None, max_depth=None, deleted=None)[source]#
Return the display-complete per-branch spend table for one run lineage.
Rows are
{'node', 'max_cost', 'spent', 'deleted'}: the target’s own row leads (its cap from config; the whole answer for a leaf and formax_depth=0), each still-registered descendant follows with its cap (idle children spend0.0), then any lineage descendant whose registry row is gone (max_costNone,deletedTrue) – its spend would otherwise vanish from the table while still counting inspent(), so the rows always sum to it. Whendeletednames a deleted target (the caller resolves for it), the lead row is that branch with no cap and no registered-children rows are emitted – delete clears the whole subtree’s registry rows, the lineage carries the rest.- Parameters:
run_id (
int|None) – Run to scope to; the current run if omitted.max_depth (
int|None) – Maximum descendant depth to include (1= direct children only); all descendants if omitted.deleted (
str|None) – A deleted target branch to lead the table with.self (
Cost)
- Return type:
list[dict[str,Any]]- Returns:
Spend rows summing to the scope’s
spent().
- fractal.core.cost.run_spend(costs, steps, run_id, until=None)[source]#
Return a run’s subtree spend as of an instant, over prefetched plain rows.
The in-memory twin of
Cost.spent()’s recursive SQL walk (kept adjacent so the pairing is one-file-visible): the run’s own steps ended by the instant, plus every descendant run chained to it viaparent_run_id– each hop is one node level. Open rows read as all-time. Pure – no database access.- Parameters:
costs (
dict[int,tuple[int|None,float]]) –{run_id: (parent_run_id, own step-cost sum)}across the subtree (the chain substrate).steps (
dict[int,list[tuple[float,float]]]) –{run_id: [(ended_epoch, cost), ...]}for costed steps.run_id (
int) – The run to total.until (
float|None) – Epoch instant to total as of; all time if omitted.
- Return type:
float- Returns:
Subtree spend in USD as of
until.
- fractal.core.cost.subtree_spent(connection, run_id)[source]#
Return a run’s per-run-subtree step cost via the
parent_run_idwalk.The DB-authoritative all-time counterpart to
run_spend(): it walks the persistedrunstable, so a deleted or orphaned descendant’s runs still count – matchingCost.spent(), the figure budget enforcement reads. Runs on any read-only connection.- Parameters:
connection (
Connection) – Open connection to read through.run_id (
int) – The run to total.
- Return type:
float- Returns:
Subtree spend in USD.
fractal.core.db module#
Implements Database class.
- class fractal.core.db.Database(path, schema)[source]#
Bases:
objectThin SQLite wrapper for the tree’s central database.
Provides generic table operations –
init,read,write,update,merge,delete,exists, andcount– with no domain-specific logic. Each operation runs on its own one-shot handle unless passed atransaction()connection viaconnection=, which makes a multi-statement block one atomic unit.- Parameters:
self (
Database)path (
Path)schema (
Path)
Initialize
Database.- Parameters:
path (
Path) – Path to the.dbfile.schema (
Path) – Path to the.sqlschema executed byinit.self (
Database)
- init()[source]#
Create the database and tables from the schema.
Idempotent – safe to call on an existing database – but purely additive: the schema’s
IF NOT EXISTSDDL never alters an existing table or refreshes a changed view, so a database created under an older schema is not upgraded and must be rebuilt.- Parameters:
self (
Database)- Return type:
None
- checkpoint()[source]#
Backfill the WAL into the main file and truncate it.
Writable handles never checkpoint on close (the sidecars must stay put for write-denied readers), and one-shot connections never observe the full backfill that lets SQLite restart the log – so without an explicit consolidation the WAL only grows and the bare
.dbis incomplete at rest. TRUNCATE backfills and zeroes the log while leaving both sidecars in place. Best-effort: a busy fleet defers the consolidation to the next caller.- Parameters:
self (
Database)- Return type:
None
- connect(*, read_only=False, timeout=None)[source]#
Open a database connection.
Enables foreign keys (the WAL journal mode is stamped once by
initand persists in the file header) and gives every handle a 30-second busy timeout (overridingsqlite3.connect’s 5-second default) so a contending writer waits for the lock under wide node fan-out instead of failing fast and aborting the loop.timeoutoverrides the default – a UI-thread reader passes a short one so a busy writer never stalls a refresh. Writable handles keep the close-time checkpoint from unlinking the WAL sidecars. Each caller is responsible for closing the returned connection.- Parameters:
read_only (
bool) – Open in read-only mode.timeout (
float|None) – Busy timeout in seconds (default 30).self (
Database)
- Return type:
Connection- Returns:
Database connection.
- transaction()[source]#
Open one
BEGIN IMMEDIATEtransaction over a dedicated connection.Yields a connection holding the database write lock for the whole block, so a read-decide-write transition commits or rolls back as one atomic unit; the table operations join it via
connection=. Every operation inside the block must pass it: a write withoutconnection=deadlocks against the block’s own lock until the busy timeout, and a read without it silently sees the pre-transaction snapshot.BEGIN IMMEDIATEtakes the lock up front – two concurrent transactions serialize (bounded by the busy timeout) instead of failing mid-block. Commits on clean exit; rolls back on any exception (re-raised).- Yields:
The transaction’s connection.
- Parameters:
self (
Database)
- read(table=None, *, query=None, params=(), where=None, limit=None, connection=None)[source]#
Read rows from a table.
Either
queryortablemust be provided.queryis mutually exclusive withwhereandlimit. Whenqueryis given, it is executed as raw read-only SQL withparamsbound positionally to its?placeholders. Otherwise, aSELECTis built fromtable,where, andlimit.Built
SELECT``s are ordered by ``rowid DESC– the true write order (a monotonic alias of theINTEGER PRIMARY KEY) – so the most recently written row is first, without assuming any particular timestamp column (tables name their startstarted_atorcreated_at). A rawquerykeeps its own ordering (none is imposed);limit=0is valid and returns no rows.- Parameters:
table (
str|None) – Table name.query (
str|None) – Raw SQL query (mutually exclusive withwhereandlimit).params (
tuple[Any,...]) – Parameters bound to thequery’s?placeholders.where (
dict[str,Any] |None) – Column filters (AND-joined equality).limit (
int|None) – Maximum rows to return.connection (
Connection|None) – Transaction to run inside (fromtransaction()).self (
Database)
- Return type:
list[dict[str,Any]]- Returns:
List of row dicts.
- write(data, table, *, connection=None)[source]#
Insert a row into a table.
- Parameters:
data (
dict[str,Any]) – Column values.table (
str) – Table name.connection (
Connection|None) – Transaction to run inside (fromtransaction()).self (
Database)
- Return type:
int- Returns:
Row ID of the inserted row.
- update(data, table, *, where, connection=None)[source]#
Update rows in a table.
- Parameters:
data (
dict[str,Any]) – Column values to set.table (
str) – Table name.where (
dict[str,Any]) – Column filters (AND-joined equality).connection (
Connection|None) – Transaction to run inside (fromtransaction()).self (
Database)
- Return type:
int- Returns:
Number of rows updated – the observable outcome of a compare-and-swap
where(0 means another writer won).
- merge(data, table, *, conflict=None, connection=None)[source]#
Insert a row, or upsert it on a unique conflict.
With
conflict(the unique column(s) identifying an existing row), an existing row is updated in place for only the columns indata– every other column keeps its stored value. This is a real upsert: a partial merge (e.g. juststatus) preserves the row’s other columns rather than wiping them the way a whole-rowINSERT OR REPLACEwould. A partial merge must still supply everyNOT NULLcolumn that lacks a SQL default: the baseINSERT’s constraints are checked before the conflict routes to the update, so omitting one fails (it is not backfilled). Withoutconflictit falls back toINSERT OR REPLACE(a full-row insert).- Parameters:
data (
dict[str,Any]) – Column values.table (
str) – Table name.conflict (
list[str] |None) – Unique column(s) to upsert on.Nonedoes a whole-row insert-or-replace.connection (
Connection|None) – Transaction to run inside (fromtransaction()).self (
Database)
- Return type:
int- Returns:
Row ID of the inserted, updated, or (on a no-op) existing row.
- delete(table, *, where, connection=None)[source]#
Delete rows from a table.
- Parameters:
table (
str) – Table name.where (
dict[str,Any]) – Column filters (AND-joined equality).connection (
Connection|None) – Transaction to run inside (fromtransaction()).self (
Database)
- Return type:
int- Returns:
Number of rows deleted – so callers can report a zero-match delete honestly instead of implying one landed.
- exists(table, *, where, connection=None)[source]#
Check whether a matching row exists.
- Parameters:
table (
str) – Table name.where (
dict[str,Any]) – Column filters (AND-joined equality).connection (
Connection|None) – Transaction to run inside (fromtransaction()).self (
Database)
- Return type:
bool- Returns:
Whether a matching row exists.
- count(table, *, where=None, connection=None)[source]#
Count rows in a table.
- Parameters:
table (
str) – Table name.where (
dict[str,Any] |None) – Column filters (AND-joined equality).connection (
Connection|None) – Transaction to run inside (fromtransaction()).self (
Database)
- Return type:
int- Returns:
Row count.
fractal.core.event module#
Base classes for events.
- class fractal.core.event.Event(source, message=None, **kwargs)[source]#
Bases:
objectPoint-in-time observability record with snapshot semantics.
Payload fields are bare class annotations on subclasses;
__init__extracts matching kwargs viatyping.get_type_hints, deep-copies the values, and shallow-copies the emitting source.- Parameters:
self (
Event)source (
Any)message (
str|list[str] |None)kwargs (
Any)
Initialize
Event.- Parameters:
source (
Any) – The object that emitted this event.message (
str|list[str] |None) – Optional caller-supplied free-form context to fold into the event description.self (
Event)kwargs (
Any)
- info: str | None = None#
- property name: str#
Return event name.
Default format is
EVENT_NAME, derived from the class name in screaming snake case. Wheninfois set, returnsEVENT_NAME (info)to qualify the event type with subclass-specific structured context.- Returns:
Event name with optional
(info)qualifier.- Parameters:
self (
Event)
- property description: str#
Return event description.
Composes
{name} : {source class name}plus optional message on new line whenmessageis set – the class name is the friendly label for every emitting source, never a raw object repr.- Returns:
Full event description for logging.
- Parameters:
self (
Event)
- class fractal.core.event.InitialEvent(source, message=None, **kwargs)[source]#
Bases:
EventEmitted at the start of a paired operation.
- Parameters:
self (
Event)source (
Any)message (
str|list[str] |None)kwargs (
Any)
Initialize
Event.- Parameters:
source (
Any) – The object that emitted this event.message (
str|list[str] |None) – Optional caller-supplied free-form context to fold into the event description.self (
Event)kwargs (
Any)
- class fractal.core.event.TerminalEvent(source, **kwargs)[source]#
Bases:
EventEmitted at the end of a paired operation; computes duration.
- Parameters:
self (
TerminalEvent)source (
Any)kwargs (
Any)
Initialize terminal event.
Pairs with the corresponding
initial_eventand computesdurationas the elapsed seconds between the two events.- Parameters:
source (
Any) – The object that emitted this event.self (
TerminalEvent)kwargs (
Any)
- initial_event: InitialEvent#
- class fractal.core.event.FailureEvent(source, **kwargs)[source]#
Bases:
TerminalEventEmitted when a paired operation fails.
- Parameters:
self (
TerminalEvent)source (
Any)kwargs (
Any)
Initialize terminal event.
Pairs with the corresponding
initial_eventand computesdurationas the elapsed seconds between the two events.- Parameters:
source (
Any) – The object that emitted this event.self (
TerminalEvent)kwargs (
Any)
- error: BaseException#
fractal.core.files module#
Implements Files class.
- class fractal.core.files.Files(node)[source]#
Bases:
objectExternal work-product surface (list/read/write/commit/archive).
Initialize
Files.- list(*, path=None, since=None)[source]#
List the node’s project files (git-tracked, machinery included).
The work-product surface: every git-tracked file in the worktree – the git-ignored runtime (
.db/.status/logs) never appears in a tracked listing, andwiki//.fractal/entries list like any other content (consumers filter or collapse them). Withsincethe set is instead the node’s own contribution: files its own commits touched (a first-parent walk from thesinceanchor – a tree atHEADcontains everything ever merged in, so without the walk content synced from the parent, and through it siblings, would read as this node’s output) carrying the net line counts of a<anchor>...HEADdiff.- Parameters:
path (
str|None) – Restrict to a worktree-relative subtree; all files ifNone.since (
str|None) – List changed files instead, frombase(the node’s fork point),commit(the previous commit),iteration, orrun; the full tracked listing ifNone.self (
Files)
- Return type:
list[dict[str,Any]]- Returns:
[{path, size}]sorted by path (pathworktree-relative). A changed listing’s entries also carrychange(added/modified/deleted) andadditions/deletionsline counts (Nonefor a binary file); a deleted file is kept (size0) so its removal can render, and a touched file with no net change (self-corrections cancel) drops out.
- read(path, *, max_lines=None, since=None, before=False)[source]#
Read a project file’s content (validated, capped).
Only files the project surface exposes are readable: the path must be structurally safe (
_validate_relpath()) and either git-tracked or, givensince, part of that anchor’s changed set – so a deleted file’s old content stays readable without exposing anything else.beforereads the file as it was at thesinceanchor (viagit show), for the old side of a before/after view; a side that does not exist (an added file has no before; a deleted file has no after) returnsexists=Falsewith empty content.- Parameters:
path (
str) – Worktree-relative file path.max_lines (
int|None) – Cap the returned text to this many lines (full ifNone); the cap preserves line terminators, so the included portion matches the raw bytes.since (
str|None) – Diff scope –base,commit,iteration, orrun(seelist()).before (
bool) – Read the file at thesinceanchor instead of the worktree.self (
Files)
- Return type:
dict[str,Any]- Returns:
{path, content, truncated, total_lines, size, binary, exists}. A non-UTF-8 file returnsbinary=Truewith emptycontent(callers download it viapath()instead).- Raises:
ValueError – If
beforeis set withoutsince, orpathis not a file the tracked set or the anchor’s changed set exposes.
- path(path)[source]#
Resolve a project file to its on-disk path (validated).
The download side of
read()– same validation, tracked membership, and containment – returning the absolute path so a caller streams the bytes straight from disk (e.g. an HTTP layer serving range requests) instead of buffering them through a read.- Parameters:
path (
str) – Worktree-relative file path.self (
Files)
- Return type:
Path- Returns:
The absolute on-disk path of the file.
- Raises:
ValueError – If
pathis not a readable project file.
- write(path, data)[source]#
Write a file into the worktree at
path(validated, uncommitted).The upload side of the project surface: raw bytes land at a validated worktree-relative path (parents created), joining the tracked listing only once committed – via
commit(), promptly: on a scoped node an uncommitted out-of-scope upload fails the loop’s next commit scope check, which diffs untracked files too. Uploads validate at the write tier (_validate_writable()):.fractalpaths refuse, the wiki accepts.- Parameters:
path (
str) – Worktree-relative destination path.data (
bytes) – Raw bytes to write.self (
Files)
- Return type:
dict[str,Any]- Returns:
{path, size}– the normalized path and bytes written.- Raises:
RuntimeError – If the node is paused.
ValueError – If
pathescapes the worktree or names machinery.
- commit(paths, message)[source]#
Stage and commit specific worktree paths (no lint, scope, or push).
A narrow pathspec commit for bringing user files (e.g. uploaded inputs) into the tree – allowed on the user node, unlike the locked
--initbaseline, and without the loop’s full commit machinery. Each path is validated like a write, then staged and committed with a pathspec, so nothing else the worktree has staged is swept in. Nocommitevent is logged: an upload has no run lineage (the same reason the commit script skips the event for--init), and auto-resolved lineage during a live run would silently shift theiteration/rundiff anchors.- Parameters:
- Return type:
dict[str,Any]- Returns:
{committed, sha, paths}– whether a commit was made (Falsewith aNonesha when the paths held no change) and the normalized paths.- Raises:
RuntimeError – If the node is paused.
ValueError – If
pathsis empty,messageis blank, or a path escapes the worktree or names machinery.
- archive(*, path=None, since=None)[source]#
Bundle the node’s project files into a zip archive.
Read-only: zips the
list()set (full or changed); the worktree is never modified. Arcnames are the listing’s validated worktree-relative paths, and a changed listing’s deletions (nothing on disk) are skipped.
- history(path, *, since='base')[source]#
List the node’s own commits that touched one file, newest first.
The per-file trail behind a changed listing: the same first-parent, no-merges walk as
list()’s membership, scoped to one path – so history shows exactly the commits that made the file a member, never a merge or merged-in side history. Each entry carries the commit’s own line counts for the file (unlike the listing’s net counts), so the trail sums the work over time.- Parameters:
- Return type:
list[dict[str,Any]]- Returns:
[{sha, instant, subject, additions, deletions}]newest first –instantis the commit’s author time (ISO 8601), the counts per-commit numstat (Nonefor binary); empty when the scope has no anchor or no own commit touched the file.
fractal.core.loop module#
Autonomous iteration loop for a fractal node.
- class fractal.core.loop.Step(path, *, number=0, frontmatter=None)[source]#
Bases:
objectOne discovered step file.
Carries the step’s number, name, path, and frontmatter overrides (requires_approval, agent, provider, model, effort, timeout, detached).
- Parameters:
self (
Step)path (
Path)number (
int)frontmatter (
dict[str,str] |None)
Initialize
Step.- Parameters:
self (
Step)path (
Path)number (
int)frontmatter (
dict[str,str] |None)
- classmethod load(path, *, number=0)[source]#
Parse the strict flat-scalar frontmatter grammar.
The block opens with
---on the first line and closes at the next---; each line inside matchingkey: value(lowercase key, non-empty scalar) contributes one override, first occurrence winning. Anything else – including a file with no opening fence – contributes nothing.- Parameters:
path (
Path) – Step markdown file.number (
int) – 1-based step number within the iteration (0for the SYNC pseudo-step).
- Return type:
- Returns:
The parsed step.
- property name: str#
the filename stem past its
NN-prefix.- Type:
Return the step name
- Parameters:
self (
Step)
- property requires_approval: bool#
Return whether the
requires_approval: truefrontmatter is set. :type self:Step:param self:
- property agent: str | None#
Return the
agent:frontmatter override, if any. :type self:Step:param self:
- property provider: str | None#
Return the
provider:frontmatter override, if any. :type self:Step:param self:
- property model: str | None#
Return the
model:frontmatter override, if any. :type self:Step:param self:
- property effort: str | None#
Return the
effort:frontmatter override, if any. :type self:Step:param self:
- class fractal.core.loop.StepResult(status, exit_code=0, reason=None, session=None, cost=None, budget_stopped=False)[source]#
Bases:
objectOutcome of one step launch: status, reason, session, cost.
The in-process attribution record for a launch – deadline overruns, budget skips, pause aborts, and failures land here as statuses, never as process exit sentinels or marker files.
statusis one of'completed' | 'timed out' | 'skipped' | 'paused' | 'failed';reasoncarries the fractal-owned label that lands in step-row metadata ('agent error (exit N)'/'stream error'), suffixed with the one-line cause when the launch captured one (the stderr tail or the stream exception) – the iter/run rollups keep the bare label (a budget skip’s'over budget'label is applied at step close, not carried here). A budget-cap stop is a cleancompletedwithbudget_stoppedset, never a failure.- Parameters:
status (
str)exit_code (
int)reason (
str|None)session (
str|None)cost (
float|None)budget_stopped (
bool)
- status: str#
- exit_code: int = 0#
- reason: str | None = None#
- session: str | None = None#
- cost: float | None = None#
- budget_stopped: bool = False#
- class fractal.core.loop.Loop(node, *, agent_command=None, continue_=False, resume=False, render=None)[source]#
Bases:
objectOne node’s agent iteration loop, run inside the node’s tmux pane.
Owns preflight, run adoption, step execution, budget/deadline policy, and the terminal cascade. All state flows through
Node(rows, signals, config) and the Agent seam (invocation, streaming, cost); shell remains only at the process boundary (setup/lint hooks, the agent binary itself). Observability rides the on_iteration/on_step event pairings – override them (calling super and returning the event) to ship structured loop progress to a host application; stdout stays the byte-exact operator transcript.- Parameters:
self (
Loop)node (
Node)agent_command (
str|None)continue_ (
bool)resume (
bool)render (
Callable[[StreamEvent],None] |None)
Initialize
Loop.- Parameters:
node (
Node) – The node whose loop to run.agent_command (
str|None) – Agent invocation (e.g.'claude'); defaults to the node’s configured agent.continue_ (
bool) – Continue a stopped/exited node (clean worktree, further iterations).resume (
bool) – Resume a paused node (adopt its open run where the pause left it).render (
Callable[[StreamEvent],None] |None) – Presentation callback for parsed agent stream events (the pane rendering);Nonerenders nothing.self (
Loop)
- Raises:
ValueError – If no agent is configured or the agent command names no registered backend, or a run parameter is invalid (a malformed duration, conflicting schedules).
- log(message, level=20)[source]#
Log a message to this loop’s logger.
- Parameters:
self (
Loop)message (
str)level (
int)
- Return type:
None
- run()[source]#
Execute the loop end to end and return the process exit code.
Body is guarded by try/finally ->
_on_exit(the EXIT-trap mirror); no signal handlers are installed – SIGTERM kill classification belongs to kill.sh’s Python and out-of-band deaths toNode._reconcile_status.- Parameters:
self (
Loop)- Return type:
int
- on_iteration(message=None, *, logging_level=20, **kwargs)[source]#
Handle iteration event.
Fired when an iteration begins.
- Parameters:
self (
Loop)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- on_iteration_success(message=None, *, logging_level=10, **kwargs)[source]#
Handle iteration success event.
Fired when an iteration ends in-band with an honest attribution.
- Parameters:
self (
Loop)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- on_iteration_failure(message=None, *, logging_level=30, **kwargs)[source]#
Handle iteration failure event.
Fired when an iteration fails.
- Parameters:
self (
Loop)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- on_step(message=None, *, logging_level=20, **kwargs)[source]#
Handle step event.
Fired when a step (or SYNC) launch begins.
- Parameters:
self (
Loop)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- on_step_success(message=None, *, logging_level=10, **kwargs)[source]#
Handle step success event.
Fired when a step ends with a non-failure attribution.
- Parameters:
self (
Loop)message (
str|list[str] |None)logging_level (
int)kwargs (
Any)
- Return type:
- class fractal.core.loop.LoopIterationEvent(source, message=None, **kwargs)[source]#
Bases:
InitialEventEmitted when an iteration begins.
- Parameters:
self (
Event)source (
Any)message (
str|list[str] |None)kwargs (
Any)
Initialize
Event.- Parameters:
source (
Any) – The object that emitted this event.message (
str|list[str] |None) – Optional caller-supplied free-form context to fold into the event description.self (
Event)kwargs (
Any)
- iteration: int#
- run_id: int#
- iter_id: int#
- class fractal.core.loop.LoopIterationSuccessEvent(source, **kwargs)[source]#
Bases:
TerminalEventEmitted when an iteration ends in-band.
Any honest attribution: completed, paused, stopped.
- Parameters:
self (
TerminalEvent)source (
Any)kwargs (
Any)
Initialize terminal event.
Pairs with the corresponding
initial_eventand computesdurationas the elapsed seconds between the two events.- Parameters:
source (
Any) – The object that emitted this event.self (
TerminalEvent)kwargs (
Any)
- outcome: str#
- class fractal.core.loop.LoopIterationFailureEvent(source, **kwargs)[source]#
Bases:
FailureEventEmitted when an iteration fails.
A setup crash-loop, a fatal SYNC timeout, or an unhandled loop error.
- Parameters:
self (
TerminalEvent)source (
Any)kwargs (
Any)
Initialize terminal event.
Pairs with the corresponding
initial_eventand computesdurationas the elapsed seconds between the two events.- Parameters:
source (
Any) – The object that emitted this event.self (
TerminalEvent)kwargs (
Any)
- class fractal.core.loop.LoopStepEvent(source, message=None, **kwargs)[source]#
Bases:
InitialEventEmitted when a step (or SYNC) launch begins.
- Parameters:
self (
Event)source (
Any)message (
str|list[str] |None)kwargs (
Any)
Initialize
Event.- Parameters:
source (
Any) – The object that emitted this event.message (
str|list[str] |None) – Optional caller-supplied free-form context to fold into the event description.self (
Event)kwargs (
Any)
- step: str#
- class fractal.core.loop.LoopStepSuccessEvent(source, **kwargs)[source]#
Bases:
TerminalEventEmitted when a step ends with a non-failure attribution.
Completed, skipped, paused, or a budget stop.
- Parameters:
self (
TerminalEvent)source (
Any)kwargs (
Any)
Initialize terminal event.
Pairs with the corresponding
initial_eventand computesdurationas the elapsed seconds between the two events.- Parameters:
source (
Any) – The object that emitted this event.self (
TerminalEvent)kwargs (
Any)
- result: StepResult#
- class fractal.core.loop.LoopStepFailureEvent(source, **kwargs)[source]#
Bases:
FailureEventEmitted when a step fails (agent error / stream error / timeout).
- Parameters:
self (
TerminalEvent)source (
Any)kwargs (
Any)
Initialize terminal event.
Pairs with the corresponding
initial_eventand computesdurationas the elapsed seconds between the two events.- Parameters:
source (
Any) – The object that emitted this event.self (
TerminalEvent)kwargs (
Any)
- result: StepResult#
fractal.core.node module#
Implements Node class.
- class fractal.core.node.Node(path, *, branch=None)[source]#
Bases:
objectAn autonomous agent node in a git worktree.
Tracks status in a
.statusfile and the tree’s central database (hosted in the root node’s data directory); delegates shell-native work (git, tmux) to_scripts/.- Parameters:
self (
Node)path (
str|PathLike)branch (
str|None)
Initialize
Node.- Parameters:
path (
str|PathLike) – Worktree directory (or repo root for init).branch (
str|None) – Pin the node’s branch instead of reading it from git. Only for resolving a node whose worktree is checked out to a different branch (the user node from a non-init checkout, viaresolve_user());Nonereads the live git branch.self (
Node)
- property db: Database[source]#
Return the central database, hosted in the root node’s data directory.
Resolved through the
rootconfig key (written at init, inherited from the parent) and the root’s.worktrees/.project/<root>cache, so any node in the tree opens the same.dbwithout a worktree lookup.
- property record: Record[source]#
Return the node execution record (run/iter/step spans, events, signals).
- property time: Time[source]#
Return the node deadline accounting (config timeouts over row instants).
- log(message, level=20)[source]#
Log a message to this node’s logger.
- Parameters:
self (
Node)message (
str)level (
int)
- Return type:
None
- property branch: str[source]#
Return the current git branch name (one
git rev-parse, cached).A worktree’s checked-out branch is fixed for the life of a
Nodehandle – teardown/re-init paths construct fresh handles, so a freshNodeis a fresh read.
- property repo_dir: Path[source]#
Return the main git repo root (through worktrees; one probe, cached).
- property node_dir: Path#
Return the node data directory.
Under a sub-project (per the
.worktrees/.project/<branch>cache) the dir nests at<worktree>/<project>/.fractal/<branch>. :type self:Node:param self:
- property project_path: str#
Return the worktree-relative project sub-path (
'.'for a repo-root node).Cached per-branch at
.worktrees/.project/<branch>(written at init); absent for a repo-root project, which reads as'.'. :type self:Node:param self:
- property parent: Node | None#
Return the parent by dotted-branch derivation (
Nonefor the user node).Also
Nonewhen the parent’s worktree is gone (pruned out of band), matching the ancestor walk’s skip. :type self:Node:param self:
- property tmux_session: str#
Return the node’s tmux session name (via
tmux_session_name()). :type self:Node:param self:
- classmethod resolve_caller()[source]#
Resolve the calling node from the environment.
When running inside a node (
_NODEenv var set), returns aNodebound to the caller’s worktree. ReturnsNoneoutside a node context.- Return type:
Node|None
- classmethod resolve_user(path)[source]#
Resolve the repo’s user (root) node by config, not the checkout.
A bare
Nodekeys on the repo’s current branch, so on a non-init checkout (the user on their own branch while nodes run) the user node reads as uninitialized even though the fractal exists. Scan the repo’s fractal data dirs (top-level and each sub-project) for theconfig.jsonmarkeduser: trueand pin aNodeto that branch, independent of the git checkout.- Parameters:
path (
str|PathLike) – Any path inside the repo.- Return type:
Node|None- Returns:
The user (root) node, branch-pinned, or
Nonewhen there is no user node to find (no fractal, or no git repo at all).
- exists()[source]#
Return whether this node has been initialized.
- Parameters:
self (
Node)- Return type:
bool
- init(name=None, *, path=None, title=None, scope=None, base=None, meta=None, inherit=None, agent=None, provider=None, model=None, effort=None, max_iters=None, max_depth=None, max_children=None, max_descendants=None, timeout=None, iter_timeout=None, step_timeout=None, step_retries=None, step_retry_backoff=None, interval=None, sleep=None, wait=None, max_cost=None, max_iter_cost=None, max_step_cost=None, reserve_budget=None, sync=None, detached=None, local=None, blind=False, reset=False, user=False)[source]#
Initialize an autonomous node.
Creates a git worktree on a new branch named
<name>(or<parent>.<name>if the current branch is a node) and populates a.fractal/<branch>/data directory with steps, hooks, skills, and configuration from the skill source.- Parameters:
name (
str|None) – Node name (current branch for user node).path (
str|PathLike|None) – Project path (relative to repo) for user node; for a child, a non-root value selects its sub-project (default: inherit the parent’s). A path under.worktrees/(the cwd resolving into a worktree) names the parent instead.title (
str|None) – Human-readable display name (defaults to the de-slugged name).scope (
list[str] |None) – Subdirectory scope within the worktree.base (
str|None) – Branch to start from.meta (
str|None) – Target node branch for meta-configuration.inherit (
list[str] |None) – Surfaces to seed from the parent node instead of the package seed (steps,scripts,skills,config, orall).configcopies the parent’s preference keys only – budget-class caps never inherit.agent (
str|None) – Agent type.provider (
str|None) – Provider route for the agent (e.g.openrouter; default: the vendor-native endpoint, inherited from the nearest ancestor).model (
str|None) – Model override (passed via the agent CLI’s model flag).effort (
str|None) – Reasoning-effort override (passed via the agent CLI’s effort flag).max_iters (
int|None) – Maximum number of iterations.max_depth (
int|None) – Maximum child node nesting depth.max_children (
int|None) – Maximum direct child nodes.max_descendants (
int|None) – Maximum total descendant nodes.timeout (
str|None) – Timeout per run (e.g.30m).iter_timeout (
str|None) – Timeout per iteration (e.g.30m).step_timeout (
str|None) – Timeout per step (e.g.30m).step_retries (
int|None) – Retries per failed step (default 1; 0 disables).step_retry_backoff (
str|None) – Delay before each step retry (e.g.10s; default10s).interval (
str|None) – Fixed iteration schedule (e.g.30m).sleep (
str|None) – Delay between iterations (e.g.10s).wait (
str|None) – Sleep between approval-wait sync invocations (e.g.5m).max_cost (
float|None) – Maximum cost in USD.max_iter_cost (
float|None) – Maximum cost per iteration in USD.max_step_cost (
float|None) – Maximum cost per step in USD (warn-only when unenforceable).reserve_budget (
float|None) – USD reserved for cleanup; shifts when the node enters reserve mode (not enforced).sync (
bool|None) – Run sync mode before each step.detached (
bool|None) – Separate agent invocation per step.local (
bool|None) – Skip pushing to remote after each commit.blind (
bool) – Subscribe to no channels (the parent still reads it).reset (
bool) – Delete all node files and reinitialize.user (
bool) – Initialize as a user node (DB + radio only).self (
Node)
- Return type:
str- Returns:
Script output.
- start(*, continue_run=False, clean=False, max_cost=None)[source]#
Launch the node in a tmux session.
Creates a tmux session (or window if already inside tmux) that runs the iteration loop. All run parameters are read from
config.json(set at init or edited before launch);continue_run(with its optionalmax_costretune) is the only launch-time action.A continue re-enters the unsettled pool, so it re-checks the width/descendant gates (
_enforce_rearm_limits()) and re-arms toidleunder the.worktreesflock; the loop stampsactiveat boot just as a fresh start does. A budget-ended run (the run row’sexited/0 landing) never re-arms silently: a bare continue refuses, naming the spent and armed figures, and an explicitmax_costis applied through the parent’s retune (child_retune()) before the launch.Every successful launch logs a completed
startevent (metadatacontinueon a continue), so the event log carries the node’s restart chain with the actor column answering who re-armed.- Parameters:
continue_run (
bool) – Continue a stopped/exited node.clean (
bool) – Acknowledge that the continue’s worktree restore discards uncommitted project files (required when any exist).max_cost (
float|None) – New cost cap in USD for the continued run, applied through the parent’s retune; required when the last run ended on its cost budget.self (
Node)
- Return type:
str- Returns:
Script output, prefixed by any launch-time notices (the retune echo, the continue-from-killed countermand).
- finish(reason=None)[source]#
Finish the node and its active descendants (children first).
Each loop stops after its current iteration. On the user (root) node the fan-out covers the whole tree with no self signal – the user node has no loop of its own.
- Parameters:
reason (
str|None) – Optional reason for finishing.self (
Node)
- Return type:
str- Returns:
Confirmation message.
- finish_cancel(reason=None)[source]#
Withdraw this node’s pending
finishsignal.Deletes the signal rows for the current run, so the loop’s boundary checks no longer see a pending finish. Descendants are untouched: a subtree
finishfans out, but its cancel must not – finishing is a descendant’s normal completion path, not something to withdraw.- Parameters:
reason (
str|None) – Optional reason for the cancellation.self (
Node)
- Return type:
str- Returns:
Confirmation message.
- stop(reason=None)[source]#
Stop the node and its active descendants (children first).
Each loop stops after its current step. On the user (root) node the fan-out covers the whole tree with no self signal – the user node has no loop of its own.
- Parameters:
reason (
str|None) – Optional reason for stopping.self (
Node)
- Return type:
str- Returns:
Confirmation message.
- kill(reason=None)[source]#
Kill the node and its active or paused descendants (children first).
Reaps each tmux session and marks its active rows
killed. Paused nodes are killable – the escape hatch for a parked subtree; with no loop alive the kill is pure bookkeeping (kill.shno-ops and the open rows closekilled). A booting node – session up,activenot yet stamped – is killable too (_killable()), so a spawn in flight when the kill lands is reaped, not skipped.- Parameters:
reason (
str|None) – Optional reason for killing.self (
Node)
- Return type:
str- Returns:
Confirmation message.
- pause(reason=None)[source]#
Pause the node and its active descendants (parent-first).
Signals
pausethen aborts each node’s in-flight agent invocation (pause.shreaps the recorded step process group), so every loop reclassifies the abort and parks: it exits with statuspaused, leaving its run and iteration rows open forresume()to adopt. Fans out parent-first – the inverse of every other signal – so a parent parked before its children can never drain-complete over them, and re-enumerates until the subtree is fully signaled, catching children spawned mid-fan-out (init/startrefuse new work under the pause latch). On the user (root) node, which has no loop of its own, the fan-out covers the whole tree with no self signal – the tree-wide brake – and latches the root first, so even a depth-1 start racing the sweep refuses until resume.- Parameters:
reason (
str|None) – Optional reason for pausing.self (
Node)
- Return type:
str- Returns:
Confirmation message.
- resume()[source]#
Resume the node and its paused descendants (leaf-first).
Relaunches each paused loop, which adopts its open run where the pause left it: same budgets and iteration count, the interrupted step re-entered (resuming the recorded agent session when one exists, re-orienting fresh otherwise), and run/iteration deadlines credited for the paused span. Leaf-first so every child reads
activeagain before its parent’s drain-waits can look. A node still parking (activewith a pending pause signal) gets its pause withdrawn instead – the live loop then never parks. On the user (root) node the fan-out covers the whole tree with no self relaunch – the tree-wide release, which also lifts the root latch.- Return type:
str- Returns:
Confirmation message.
- Parameters:
self (
Node)
- pause_latched(*, tree_only=False, skip_self=False)[source]#
Return the branch of the nearest paused or pausing node at-or-above.
The pause latch: a paused subtree admits no new work, so
init(spawn),start, and a targetedresume()refuse – and a booting loop parks – while any ancestor (or the node itself) ispausedor stillactivewith a pendingpausesignal, or while the tree-wide latch (a user-node pause) is set. Walks by name so a pruned intermediate never hides a paused ancestor.- Parameters:
tree_only (
bool) – Check only the tree-wide latch, skipping the ancestor walk – the resume-boot variant, where paused ancestors are the leaf-first fan-out’s normal state but a NEW tree-wide brake must still park the boot.skip_self (
bool) – Skip the node itself in the walk – the resume-verb variant, where the target is legally paused but a frozen ancestor (or the tree-wide brake) must still refuse the relaunch.self (
Node)
- Return type:
str|None- Returns:
The latching node’s branch (the root branch for the tree-wide latch), or
Nonewhen the path is clear.
- merge()[source]#
Squash-merge the node’s branch into its merge target.
merge.shresolves the target – the node’s configuredbaseif set (e.g. a meta node merging back into the node it optimizes), else the dotted parent (the branch minus its last segment) – runs the squash in the target’s worktree, and logs themergeevent there so the record survives this node’s later deletion.The full commit history is preserved on the node’s branch; only a single squash commit lands on the target.
Refuses while the target is active or paused – the squash, index refresh, and recovery
reset --hardall mutate the target worktree – except from inside the target’s own loop, which merges its settled children as part of its normal iteration.- Return type:
tuple[str,str]- Returns:
Tuple of script output and collected stderr notices (e.g. a skipped merge-base advance).
- Parameters:
self (
Node)
- delete()[source]#
Recursively remove the node and its whole subtree.
Tears down every descendant too (deepest first), then the node itself: each live worktree via
delete.sh(worktree + branch + remote), and the subtree’s registry rows and subscriptions are cleared from the central database – its history rows (runs, steps, messages, …) persist. Refuses if the node or any descendant is active or paused – stop, resume, or kill the subtree first.- Return type:
tuple[str,str]- Returns:
Tuple of per-node script output (deletion order) and collected stderr notices (e.g. unmerged-work warnings).
- Parameters:
self (
Node)
- deregister(name)[source]#
Deregister an orphaned (worktree-less) node from the registry.
For a node whose worktree was removed out of band,
deletecannot run – it needs the worktree.nameis the orphan’s branch or its bare short name (trailing segment), matched against this node’s registered subtree. This prunes the orphan’s branch and project-cache entry (plus any descendants the flat registry still lists) and clears the whole subtree from the central registry.selfmust be an ancestor (e.g. the user node) that still lists the orphan.- Parameters:
name (
str) – Branch (or bare short name) of the orphaned node to deregister.self (
Node)
- Return type:
str- Returns:
Confirmation message.
- Raises:
LookupError – If
namematches no registered node, or matches more than one (the candidates are listed).
- reconcile()[source]#
Record orphaned descendants (worktree removed out of band) as events.
Cleaning up a node’s worktree/branch with plain git instead of
deletelegitimately leaves its registry rows behind, but nothing records the removal (listflags such rows display-only). Logs oneorphanevent per newly observed orphan, giving out-of-band cleanup an audit trail. Registry rows are kept –delete --force(deregister) remains the removal path.
- retire(reason=None)[source]#
Mark the node as retired.
Retired nodes are hidden from
list()by default and cannot be started. The current status rides the retire event (with the reason appended, when given) sounretirecan restore it.- Parameters:
reason (
str|None) – Optional reason for retiring.self (
Node)
- Return type:
str- Returns:
Confirmation message.
- unretire()[source]#
Remove retired status from the node.
Restores the status the node held before it was retired (recorded on the retire event); when no retire event recorded one (e.g. a
.statusfile set by hand) the node resets toidle.An
idlerestore re-enters the unsettled pool, so it re-checks the width/descendant gates (_enforce_rearm_limits()) under the.worktreesflock; a settled restore holds no slot and passes ungated.- Return type:
str- Returns:
Confirmation message.
- Parameters:
self (
Node)
- static destroy(path)[source]#
Destroy the repo’s fractal – the full inverse of
fractal init.Tears down every node worktree and local branch, removes
.worktrees/, deletes the user node’s data directory, and strips fractal’s block from the repo’sinfo/exclude. Committed artifacts (the project wiki, baseline commits) and remote branches are left in place. Refuses while any node’s tmux session is alive; paused nodes are killed as part of the teardown – the caller’s confirmation authorized discarding the frozen mid-step work their parked worktrees hold.- Parameters:
path (
Path) – Git repository root.- Return type:
str- Returns:
Script output.
- static reset(path)[source]#
Remove every node worktree, keeping the project and its history.
The middle rung between
delete(one subtree) anddestroy(the full inverse of init): tears down all node worktrees and local branches and clears the node registry, while the user node’s data – config, memory, and the central database with every history row – plus the wiki and baseline commits survive, so fresh nodes spawn immediately after. Refuses while any node’s tmux session is alive; paused nodes are killed as part of the teardown – the caller’s confirmation authorized discarding the frozen mid-step work their parked worktrees hold.- Parameters:
path (
Path) – Git repository root.- Return type:
str- Returns:
Script output.
- status()[source]#
Return the node’s current status.
Reads the node’s
.statusfile – lifecycle state is kept out ofconfig.jsonso config is purely user settings.- Return type:
str- Returns:
Status string (
idlewhen no.statusfile exists).- Parameters:
self (
Node)
- status_display()[source]#
Return the status decorated with a pending signal or end reason.
active (pausing)/active (stopping)/active (finishing)when a pause/stop/finish signal is pending on an active node;exited (<reason>)when the latest run row recorded why the run ended (the run row is the single source – a reconcile-healed crash records no reason and stays bare); else the bare status. The suffix is display-only – the stored status (and the value status filters match on, via the first space-delimited chunk) stays bare – but a crashed-but-active node is reconciled (persisted) first: reads are where staleness is observed, and the probe is a no-op unless the stored status isactive.- Return type:
str- Returns:
Status string, possibly with a pending-signal or end-reason suffix.
- Parameters:
self (
Node)
- status_set(status)[source]#
Set the node’s status.
Validates against the set of known status values, writes the node’s
.statusfile, and updates the node’s row in the centralnodesregistry.- Parameters:
status (
str) – Status value to set.self (
Node)
- Return type:
None
- title_set(title)[source]#
Set the node’s human-readable title.
The display label consumers show in place of the branch slug. It lives in both the node’s
config.jsonand its central registry row (init seeds them together), so both are written – a registry-only update would leave the config stale and the two out of sync. On the user node, which has no registry row, the config write is the whole effect.- Parameters:
title (
str) – The display name to store.self (
Node)
- Return type:
None
- list(*, all_nodes=False, retired_only=False, max_depth=None, status=None, live=False, decorated=False)[source]#
List registered child nodes.
Queries the
nodestable with optional depth and status filters. A crashed-but-active row (worktree present, no live tmux session) is reconciled – persisted via_reconcile_status(), not just relabeled – before listing, so the fleet’s default steering read never echoes a dead loop asactive. Cap columns render each present child’s live config values; a gone worktree keeps the registry caps. Thelastcolumn renders each row’s newest activity instant as a compact age, flagged (12m!) when an active node has sat quiet pastmax(step_timeout, 5m).- Parameters:
all_nodes (
bool) – Include retired nodes in output.retired_only (
bool) – Show only retired nodes.max_depth (
int|None) – Maximum depth relative to this node.status (
str|None) – Filter to a status, or several comma-separated (overrides the retired/all default).live (
bool) – Reconcile each row against the child’s real.status(), dropping descendants whose worktree is gone, relabeling a crashedactivenode (no live tmux session) toexited, and a bootingidlenode (live session, the loop not yet stamped) toactive(the authoritative view). Read-only – it does not persist the relabel.decorated (
bool) – Append each active descendant’s pending stop/finish signal (active (stopping)) and each exited one’s recorded end reason (exited (<reason>)) to its displayed status; display-only, gated off for hot paths such as--count.self (
Node)
- Return type:
list[dict]- Returns:
List of child node records.
- child_add(name, *, title=None, max_cost=None, max_depth=None, max_children=None, max_descendants=None)[source]#
Register a child node.
Caps land on the row verbatim,
Noneincluded: a--resetre-init upserts over the old row, and an omitted cap must clear there just as it does in the reseededconfig.json– reconcile leaves config-absent keys alone, so a stale registry cap would otherwise survive every future heal and misreport an uncapped node as capped.- Parameters:
name (
str) – Child node name.title (
str|None) – Child’s display name.max_cost (
float|None) – Child’s maximum cost in USD.max_depth (
int|None) – Child’s maximum nesting depth.max_children (
int|None) – Child’s maximum direct child nodes.max_descendants (
int|None) – Child’s maximum total descendant nodes.self (
Node)
- Return type:
int- Returns:
Node ID.
- child_update(name, *, title=None, max_cost=None, max_iter_cost=None, max_step_cost=None, reserve_budget=None, step_timeout=None, max_depth=None, max_children=None, max_descendants=None)[source]#
Update a child node’s configuration.
Updates both the parent’s
nodestable and the child’sconfig.json.- Parameters:
name (
str) – Child node name.title (
str|None) – New display name.max_cost (
float|None) – New maximum cost in USD.max_iter_cost (
float|None) – New per-iteration cost cap in USD; lives only in the child’sconfig.json(thenodestable has no column).max_step_cost (
float|None) – New per-step cost cap in USD; lives only in the child’sconfig.json(thenodestable has no column).reserve_budget (
float|None) – New cleanup reserve in USD; lives only in the child’sconfig.json(thenodestable has no column).step_timeout (
str|None) – New per-step time budget; lives only in the child’sconfig.json(thenodestable has no column).max_depth (
int|None) – New maximum nesting depth.max_children (
int|None) – New maximum direct child nodes.max_descendants (
int|None) – New maximum total descendant nodes.self (
Node)
- Return type:
None
- child_retune(name, *, title=None, max_cost=None, max_iter_cost=None, max_step_cost=None, reserve_budget=None, step_timeout=None, max_depth=None, max_children=None, max_descendants=None)[source]#
Retune a child’s configuration, validating the merged result.
The policy half over
child_update()(which writes the registry row and the child’sconfig.jsontogether): a default-mode reserve (equal to what init’s default fraction materialized for the old cap) retunes to track a new cap instead of going stale; per-iter/step caps are validated against the effective cap; the merged config is validated as init would (Config.validate()); priors are captured before the write.- Parameters:
name (
str) – Child node name.title (
str|None) – New display name.max_cost (
float|None) – New maximum cost in USD.max_iter_cost (
float|None) – New per-iteration cost cap in USD.max_step_cost (
float|None) – New per-step cost cap in USD.reserve_budget (
float|None) – New cleanup reserve in USD, already resolved – the CLI’sN%grammar never enters core.step_timeout (
str|None) – New per-step time budget (a running loop picks it up at its next iteration top).max_depth (
int|None) – New maximum nesting depth.max_children (
int|None) – New maximum direct child nodes.max_descendants (
int|None) – New maximum total descendant nodes.self (
Node)
- Return type:
dict[str,tuple[Any,Any]]- Returns:
{key: (old, new)}for every PROVIDED key –old == newincluded, and an implicitly retuned reserve counts as provided – so a confirmation echo shows every requested write.- Raises:
ValueError – On a violated cap invariant (user-facing flag spellings), or when the child is missing.
- child_approve(child, *, step_id=None)[source]#
Approve a gated step in a child, recording
approveon both event logs.selfis the parent. Enforces direct parentage, writes the approval to the child’s step (the source of truth), and logs anapproveevent on the parent (naming the child + step) and on the child (its own step). The events are best-effort audit.- Parameters:
- Return type:
int- Returns:
The approved step’s id.
- Raises:
PermissionError – If
childis not a direct child ofself.ValueError – If there is no active step to approve, or the step does not exist or does not require approval.
- child_pending()[source]#
List direct children’s steps awaiting this node’s approval.
One row per direct-child step awaiting approval –
approved=''on anactiveorpausedstep row – as{'branch', 'step_id', 'step', 'step_name'}. Only direct children are listed – the steps this node can actually approve.
- commit(message=None, *, init=False, check=False, ignore_scope=False, force=False)[source]#
Commit the current iteration’s work.
Delegates to the
core.commitpipeline (scope check, lint, stage, commit, and push unlesslocalis set).- Parameters:
message (
str|None) – Short description appended to the commit message.init (
bool) – Use theinitlabel instead ofiteration <run>.<iter>.check (
bool) – Error if uncommitted changes exist instead of committing.ignore_scope (
bool) – Commit out-of-scope changes but still lint (a narrower escape hatch thanforce).force (
bool) – Bypass scope and lint checks and git hooks.self (
Node)
- Return type:
str- Returns:
Commit output and notices.
- Raises:
RuntimeError – If called on a user node without
init.ValueError – If flags conflict or
messageis missing withoutcheck.
- agent(command=None, provider=None)[source]#
Return this node’s agent backend bound to this node.
The accessor is a resolver over a keyed family, not a bound singleton: per-step
agent:overrides mean one node may run several agents in one iteration.- Parameters:
command (
str|None) – Full agent command (e.g.'claude --some-flag'); defaults to the node’s effective configured agent (own config else the nearest ancestor’s,agent_effective()).provider (
str|None) – Provider-route override (per-step frontmatter); defaults to the node’s effective configured provider.self (
Node)
- Return type:
- Returns:
The registered backend, bound to this node and
command.- Raises:
ValueError – If no agent is configured and none is given, or the base command has no registered backend.
- chat(prompt, *, session=None, current=False, resume=False, model=None, render=None)[source]#
Send one prompt to the node’s agent and stream the reply.
Nothing is inferred by default, so a bare chat is always fresh – a brand-new session whose prompt is seeded with the node’s
NODE.mdandmodes/CHAT.md.currentforks the node’s live loop session;sessionforks a given id (or, withresume, continues it in place). Forking leaves the source session untouched, so a running loop is never perturbed. Codex can resume in place but cannot fork. Streams with no cost or session side effects and returns the resulting id.- Parameters:
prompt (
str) – The prompt to send.session (
str|None) – A session id to fork (or continue in place withresume).current (
bool) – Fork the node’s live loop session (mutually exclusive withsession/resume).resume (
bool) – Continuesessionin place (same id) instead of forking.model (
str|None) – Model override; defaults to the node’s configured model.render (
Callable[[StreamEvent],None] |None) – Presentation callback receiving every parsed stream event (the CLI passes its ANSI renderer).self (
Node)
- Return type:
str|None- Returns:
The agent’s session id, or
Noneif the stream carried none.- Raises:
ValueError – No agent configured; incompatible flags;
currentwith no live session; resuming the live loop session; or forking a codex session.RuntimeError – The agent exited with a non-zero status.
- chat_command(prompt, *, session=None, current=False, resume=False, model=None)[source]#
Resolve and validate one chat turn into its agent invocation.
The build half of
chat: the same validation (incompatible flags, no live session, codex cannot fork) and the same prompt seeding (NODE.md+CHAT.mdfresh,CHAT.mdon a fork, nothing on a resume) – without spawning anything. A caller that streams the agent output itself (the TUI) spawns the returned invocation.- Parameters:
prompt (
str) – The prompt to send.session (
str|None) – A session id to fork (or continue in place withresume).current (
bool) – Fork the node’s live loop session (mutually exclusive withsession/resume).resume (
bool) – Continuesessionin place (same id) instead of forking.model (
str|None) – Model override; defaults to the node’s configured model.self (
Node)
- Return type:
- Returns:
The spawnable agent invocation.
- Raises:
ValueError – No agent configured; incompatible flags;
currentwith no live session; resuming the live loop session; or forking a codex session.
- render_template(template, *, overrides=None)[source]#
Substitute the node’s
$VARplaceholders intotemplate.The variable map is
render.variables()(overrideswin). Substitution matches GNUenvsubst($NAME/${NAME}only; unknown placeholders and$$pass through verbatim) – the grammar the renderer is pinned against.- Parameters:
template (
str) – The text to render.overrides (
dict[str,str] |None) – Variable values layered over (and winning against) the derived map – the loop passes live run state; a chat passes chat sentinels.self (
Node)
- Return type:
str- Returns:
The rendered text.
- build_prompt(step_file, *, overrides=None)[source]#
Assemble and render a step’s full prompt (
render.prompt()).- Parameters:
step_file (
str) – The step markdown file.overrides (
dict[str,str] |None) – Run-scoped variable values (seerender_template()).self (
Node)
- Return type:
str- Returns:
The rendered prompt.
- agent_effective()[source]#
Return the effective agent command: own config, else an ancestor’s.
The spawn-time inheritance walk (
init()) read back at steering time – what the node’s loop actually runs when its own config names no agent.- Return type:
str|None- Returns:
The effective agent command, or
Nonewhen no node up the chain configures one.- Parameters:
self (
Node)
- provider_effective()[source]#
Return the effective provider route: own config, else an ancestor’s.
The spawn-time inheritance walk (
init()) read back at steering time – the route a routed backend binds when the node’s own config names none;Nonemeans the vendor’s own endpoint.- Return type:
str|None- Returns:
The effective provider route, or
Nonewhen no node up the chain configures one.- Parameters:
self (
Node)
- fractal.core.node.node_dir(worktree, project, branch)[source]#
Return the node data directory for explicit inputs (the one derivation).
Pure path arithmetic – no git, no subprocess – so the TUI’s poll path composes it with cached inputs. Under a sub-project the directory nests at
<worktree>/<project>/.fractal/<branch>; a repo-root project ('.') puts it at<worktree>/.fractal/<branch>.- Parameters:
worktree (
str|PathLike) – The node’s worktree path.project (
str) – Project sub-path within the worktree ('.'for repo-root).branch (
str) – The node’s branch.
- Return type:
Path- Returns:
The node data directory.
- fractal.core.node.tmux_session_name(repo_dir, branch)[source]#
Return the tmux session name for a branch.
Format is
<repo_name> (<branch>). tmux munges.and:in session names (target syntax), so the repo name flattens both to dashes and the branch flattens dots (git refs forbid:) – must matchstart.sh. Pure string arithmetic (no git, no subprocess) for the TUI’s poll path.- Parameters:
repo_dir (
str|PathLike) – Main repo root.branch (
str) – The node’s branch.
- Return type:
str- Returns:
The tmux session name.
fractal.core.plan module#
Implements Plans class.
- class fractal.core.plan.Plans(node)[source]#
Bases:
objectPlan files under the node’s
plans/directory.Initialize
Plans.- init(*, iter_ref, name, title=None, timestamp=None)[source]#
Create a plan file seeded with its H1 and return its path.
Names the file
{timestamp}-{run.iter}-{name}.md– the timestamp defaults to the current UTC time, so two plans written in the same iteration get distinct names – and seeds# {run.iter} {title}as the first line so the run/iteration is human-readable in the file (the title defaults to the de-sluggedname). Plans are found later by globbing the{run.iter}segment (seelist()).- Parameters:
iter_ref (
str) – The{run}.{iter}reference (e.g.12.5).name (
str) – Short descriptive slug for this plan (snake_case).title (
str|None) – H1 title; defaults to the de-sluggednamewhen omitted.timestamp (
str|None) – Filename timestamp prefix; defaults to the current UTC time.self (
Plans)
- Return type:
Path- Returns:
Absolute path to the created plan file.
- list(*, iter_ref)[source]#
List an iteration’s plan files.
Resolves “this iteration’s plans” by globbing the
{run.iter}segment, so it returns every plan the iteration wrote – zero, one, or many – regardless of each plan’s own timestamp and without relying on modification time. Returns an empty list when the node has no plans directory yet.
fractal.core.pricing module#
Functions for model pricing via the LiteLLM price table.
- fractal.core.pricing.update(max_age=None)[source]#
Refresh the cached LiteLLM pricing file.
The file is fetched to a temp path and swapped in atomically, so an interrupted download never leaves a corrupt cache.
- Parameters:
max_age (
str|None) – If given (e.g.24h), skip the fetch when the cache is newer than this duration.- Return type:
str- Returns:
'fresh'(cache new enough, no fetch),'fetched'(downloaded),'stale'(fetch failed but a cache exists), or'missing'(fetch failed and no cache exists).
fractal.core.radio module#
Implements Radio class.
- class fractal.core.radio.Radio(node)[source]#
Bases:
objectInter-node messaging for autonomous agent nodes.
Provides message sending, channels, subscriptions, threads, reactions, and read tracking in the tree’s central database.
Initialize
Radio.- init()[source]#
Seed default channels and auto-subscribe to parent/children.
Called from
Node.initonce the node’s config is written. Does:Insert default channels.
Derive the parent from the branch name; subscribe to its readable channels.
Query the
nodestable for direct children; subscribe to each child’s readable channels.
A blind node (config
blind) seeds channels only – steps 2 and 3 are skipped, so it reads nothing; the parent’s own subscription to it (child_add) is unaffected.- Parameters:
self (
Radio)- Return type:
None
- send(node=None, channel='inbox', *, parent=False, subject, data, priority, post=False)[source]#
Write a message to a node’s channel.
The message lands in the target’s channel-space (the row’s
nodecolumn names the host). Checkswrite_onlypermission and rejects ifwrite_only = 1and the writer is not the owner.The permission check is best-effort, not atomic across connections: the
write_onlyread and thedb.writeuse separate connections, so a concurrentchannel_deleteof the target channel can race between them. Strict atomicity is intentionally not provided.- Parameters:
node (
str|None) – Target node branch. Defaults to self.channel (
str) – Channel name. Defaults to'inbox'.parent (
bool) – Send to the parent node (mutex withnode).subject (
str) – Message subject.data (
str) – Message data.priority (
int) – Priority (0-10).post (
bool) – Channel-class guard:Truerequires a publicly readable channel (read_only = 0, thepostverb);False(thesendverb and internal callers) skips the check.self (
Radio)
- Return type:
tuple[str,str,str]- Returns:
Tuple of the message’s 8-char UUID and the resolved destination
(node, channel), symmetric withreply, so callers can surface the routing without re-deriving it.- Raises:
ValueError – If
parentandnodeare both set, the parent or target node is not found,priorityis out of range, the channel is not found, orpostis set and the channel is privately readable.PermissionError – If the target channel is write-only and the sender is not the owner.
- unsend(message_uuid, *, force=False)[source]#
Delete a sent message and its replies.
Only the sender can unsend. A message with replies is refused unless
forceis set, since deleting a thread also removes other nodes’ replies. Withforce, cascades to replies, reacts, and read receipts (the schema has noON DELETE CASCADE).The cascade is best-effort, not atomic across connections: each
db.deleteopens its own connection, so a reply that arrives mid-cascade from another node can race it (the pre-delete re-check narrows but does not close the window). Strict atomicity is intentionally not provided.- Parameters:
message_uuid (
str) – 8-char message UUID.force (
bool) – Delete the whole thread even if it has replies.self (
Radio)
- Raises:
ValueError – If the message is not found, or it has replies and
forceis not set.PermissionError – If caller is not the sender.
- Return type:
None
- messages(*, channel=None, limit=None, since=None, read=None, recent=False)[source]#
Query this node’s messages with reply/react counts.
Listing is always passive – it never writes read receipts, so the unread view is stable across calls;
readis the receipt-writing surface.- Parameters:
channel (
str|None) – Filter by channel name.limit (
int|None) – Maximum rows to return.since (
str|None) – Only messages after this ISO 8601 timestamp.read (
bool|None) –None= all,False= unread only,True= read only.recent (
bool) – Sort bycreated_at DESCinstead ofpriority DESC.self (
Radio)
- Return type:
list[dict[str,Any]]- Returns:
List of message dicts with
replies,pos_reacts, andneg_reactscounts.
- sent(*, channel=None, limit=None, since=None, recent=False)[source]#
Query messages this node sent, across every host’s channel-space.
Each row’s
nodecolumn names the recipient (the channel host). Replies list first-class – unlike the mailbox listings, an authored reply that threads in place is not hidden behind its parent’s reply count.- Parameters:
channel (
str|None) – Filter by channel name.limit (
int|None) – Maximum rows to return.since (
str|None) – Only messages after this ISO 8601 timestamp.recent (
bool) – Sort bycreated_at DESCinstead ofpriority DESC.self (
Radio)
- Return type:
list[dict[str,Any]]- Returns:
List of message dicts with
replies,pos_reacts, andneg_reactscounts.
- feed(*, node=None, channel=None, limit=None, since=None, read=None, recent=False)[source]#
Fan out reads across subscriptions and merge the rows.
Reads this node’s subs, optionally filtered by target node and/or channel. For each matching subscription, queries the target’s channel where
read_only = 0. Merges and re-sorts by priority DESC / created_at ASC. Each row’snodecolumn names its source. Listing is always passive – it never writes read receipts, so the unread view is stable across calls;read(feed=True)is the receipt-writing surface.- Parameters:
node (
str|None) – Filter subscriptions by target node.channel (
str|None) – Filter subscriptions by channel.limit (
int|None) – Maximum total rows to return.since (
str|None) – Only messages after this ISO 8601 timestamp.read (
bool|None) –None= all,False= unread only,True= read only.recent (
bool) – Sort bycreated_at DESCinstead ofpriority DESC.self (
Radio)
- Return type:
list[dict[str,Any]]- Returns:
Merged and sorted list of message dicts.
- read(*message_uuids, node=None, channel=None, feed=False, unread=False)[source]#
Read messages by UUID and/or selector, receipting each as this node.
The body surface: reading is the act that consumes unread state (listing never does), and every returned row lands this node’s receipt in the
readstable – receipts always attribute to the actual reader, never to the mailbox viewed. UUIDs resolve globally (unique across the tree) and return in argument order; selector rows follow priority DESC / created_at ASC and mirror the listings’ row sets (thread roots plus cross-space replies); duplicates collapse to their first occurrence. Receipts land only after every lookup resolves, so a failed UUID receipts nothing.- Parameters:
*message_uuids (
str) – 8-char message UUIDs.node (
str|None) – Mailbox node branch the selectors view. Defaults to self.channel (
str|None) – Read this channel’s messages.feed (
bool) – Read messages from the subscribed nodes.unread (
bool) – Restrict the selectors to rows this reader has no receipt for (explicit UUIDs always return).self (
Radio)
- Return type:
list[dict[str,Any]]- Returns:
Full message dicts with data and reply/react counts.
- Raises:
ValueError – If a message or the mailbox node is not found.
PermissionError – If a channel is read-only and the reader is not the owner.
- thread(message_uuid, *, read=None)[source]#
Return the full reply tree for a message.
Finds root message (walks up
parent_message_id), collects all replies recursively. Returns flat list ordered bycreated_atASC withdepthfield for indentation.- Parameters:
message_uuid (
str) – 8-char message UUID.read (
bool|None) –None= all,False= unread only,True= read only.self (
Radio)
- Return type:
list[dict[str,Any]]- Returns:
Flat list of message dicts with
depthfield.- Raises:
ValueError – If the message is not found.
PermissionError – If the channel is read-only and the reader is neither the owner nor a thread participant.
- reply(message_uuid, data, *, priority=None)[source]#
Reply to a message.
Locates the parent by UUID (globally unique) and routes by where the parent sits: a reply to a message in the caller’s own inbox is a conversation turn, so it lands in the original sender’s inbox; so does a reply into another node’s write-only channel – the reaction to a broadcast reaches its author’s inbox rather than piercing the owner-only channel; any other reply is created in the parent’s host channel-space.
threadwalks parent ids regardless of host, so a rerouted conversation still renders as one thread. Inherits subject (Re: ...) and priority from the parent if not specified. Also marks the parent read (likereact) so a replied-to message does not resurface every SYNC.The channel-flag probe is best-effort, not atomic across connections: the channel read and the
db.writeuse separate connections, so a concurrentchannel_deleteorunsendof the parent can race between them. Strict atomicity is intentionally not provided.- Parameters:
message_uuid (
str) – 8-char UUID of the parent message.data (
str) – Reply data.priority (
int|None) – Reply priority (0-10). Inherited if omitted.self (
Radio)
- Return type:
tuple[str,str,str]- Returns:
Tuple of the reply’s 8-char message UUID and the resolved destination
(node, channel), so callers can surface the routing the waysenddoes.- Raises:
ValueError – If the parent message is not found, or
priorityis out of range.PermissionError – If the parent’s channel is read-only and the replier is neither the host owner nor the original sender.
- react(message_uuid, value)[source]#
React to a message with 1 or -1.
Locates the message by UUID (globally unique) and upserts into
reacts, so re-reacting changes the value. Also marks the message read (areadsreceipt), mirroringread, so an ack clears it from SYNC.- Parameters:
message_uuid (
str) – 8-char message UUID.value (
int) – Reaction value (1or-1).self (
Radio)
- Raises:
ValueError – If
valueis not1or-1, or the message is not found.PermissionError – If the channel is read-only and the reactor is not the owner.
- Return type:
None
- save(message_uuid)[source]#
Save a message to the archive.
Locates the message by UUID (globally unique) and copies it into the
archivetable – an owned snapshot that survives unsend. Re-saving is idempotent.- Parameters:
message_uuid (
str) – 8-char message UUID.self (
Radio)
- Raises:
ValueError – If the message is not found.
PermissionError – If the channel is read-only and the saver is not the owner.
- Return type:
None
- unsave(message_uuid)[source]#
Remove a message from the archive.
- Parameters:
message_uuid (
str) – 8-char message UUID.self (
Radio)
- Raises:
ValueError – If this node has no archived copy of the message.
- Return type:
None
- saved(*, node=None, channel=None, limit=None, since=None, recent=False)[source]#
List saved messages from the archive.
- Parameters:
node (
str|None) – Filter by the copy’s source host (theownercolumn).channel (
str|None) – Filter by channel name.limit (
int|None) – Maximum rows to return.since (
str|None) – Only messages saved after this ISO 8601 timestamp.recent (
bool) – Sort bycreated_at DESCinstead ofpriority DESC.self (
Radio)
- Return type:
list[dict[str,Any]]- Returns:
List of archived message dicts.
- channel(name, *, read_only=False, write_only=False)[source]#
Register a custom channel.
Defaults to public (both
False= everyone can read and write). Rejects default channel names as reserved, and an already-registered channel name as a duplicate – create never silently overwrites an existing channel’s flags (usechannel_deletethen recreate to change them).- Parameters:
name (
str) – Channel name.read_only (
bool) – Only owner can read.write_only (
bool) – Only owner can write.self (
Radio)
- Raises:
ValueError – If the channel is reserved or already exists.
- Return type:
None
- channel_delete(name, *, force=False)[source]#
Delete a custom channel and all of its data.
Rejects default channel names as reserved. A channel that still holds messages is refused unless
forceis set, since the cascade also removes other nodes’ replies (mirroringunsend). Withforce, cascades to the channel’s messages, reacts, and read receipts (the schema has noON DELETE CASCADE) before removing its subscriptions and the channel row, so no orphaned message outlives the channel definition that gates its read-only access. Scoped to this node’s channel-space – another node’s same-named channel is untouched.The cascade is best-effort, not atomic across connections: each
db.deleteopens its own connection, so a send/reply into the channel that races the cascade can leave a row behind the channel row’s removal. Strict atomicity is intentionally not provided.- Parameters:
name (
str) – Channel name.force (
bool) – Delete the channel even if it still holds messages.self (
Radio)
- Raises:
ValueError – If the channel is reserved, not found, or it holds messages and
forceis not set.- Return type:
None
- channels()[source]#
List this node’s channels.
- Parameters:
self (
Radio)- Return type:
list[dict[str,Any]]
- subscribe(node, *, channel=None)[source]#
Subscribe to a node’s channel.
If
channelis omitted, subscribes to all readable (read_only = 0) channels on the target node.- Parameters:
node (
str) – Target node branch.channel (
str|None) – Channel name. All readable if omitted.self (
Radio)
- Return type:
None
- unsubscribe(node, *, channel=None)[source]#
Unsubscribe from a node’s channel.
If
channelis omitted, removes all subscriptions to the target node.- Parameters:
node (
str) – Target node branch.channel (
str|None) – Channel name. All if omitted.self (
Radio)
- Return type:
int- Returns:
Number of subscriptions removed – 0 when nothing matched, so a mis-pathed unsubscribe is visible to the caller.
fractal.core.record module#
Implements Record class.
- class fractal.core.record.Record(node)[source]#
Bases:
objectRow-accounting surface over the tree’s central database.
The loop-facing persistence API: every lifecycle transition funnels through first-writer-wins fenced updates, events are point-in-time log entries, and signals are consumable control rows.
Initialize
Record.- resolve_context(run_id=None, iter_id=None, step_id=None, *, active=False)[source]#
Resolve the
(step_id, iter_id, run_id)context.Explicit ids win wholesale: they are returned verbatim (after the chain check) and the current context – even when one exists – is not consulted. Otherwise
run_idis the latest active run, else the most recent run (the run container persists across iterations) – unlessactive, which returnsNoneforrun_idwhen no run is active (for events that do not belong to the node’s own run).step_id/iter_idare the in-flight (active) step/iteration, orNonewhen nothing is running – a row only pins to something currently active. AllNoneif the node is not initialized or has no runs.- Parameters:
run_id (
int|None) – Explicit run id, returned verbatim.iter_id (
int|None) – Explicit iteration id (requiresrun_id).step_id (
int|None) – Explicit step id (requiresiter_idandrun_id).active (
bool) – Resolverun_idactive-only (no most-recent fallback).self (
Record)
- Return type:
tuple[int|None,int|None,int|None]- Returns:
(step_id, iter_id, run_id), eachNonewhere not applicable.- Raises:
ValueError – When the explicit lineage does not chain (
step_idwithoutiter_id/run_id, oriter_idwithoutrun_id).
- close_open(status, *, skip_event=None, metadata=None, connection=None)[source]#
Close every still-open run/iteration/step row with a terminal.
Stamps
status/exit_code/ended_aton the node’s runs/iters/steps rows that are still open (ended_at IS NULL), first-writer-wins so a clean end’s rows are left untouched. Shared byrun_start(),Loop._on_exit(),Node._reconcile_status(), andNode._mark_active_killed()so the cascade has one definition and the DB can never diverge from the.statusfile. The row cascade – the unpriced pre-read, the three closes, and the marker stamps – commits as one transaction, so a crash mid-cascade can never leave e.g. a closed run over still-active iteration/step rows.Every close here is abnormal (exit 1), so an open step whose stream opened (session recorded) but whose cost never flushed gets the
unpricedmetadata marker, mirroringstep_end().With
skip_event– the kill cascade – any still-active event is closed bystatustoo (events have noended_at), skipping the in-flight kill event itself (Node._killfinalizes that one viaevent_end()).- Parameters:
status (
str) – Terminal status for the open rows.skip_event (
int|None) – The in-flight kill event to leave untouched; when given, stray active events are swept as well.metadata (
str|None) – Stamped on the closing run rows only (the close’s attribution);Noneleaves metadata untouched.connection (
Connection|None) – Transaction to run the row cascade inside (fromDatabase.transaction()); opens its own when omitted.self (
Record)
- Return type:
None
- run_start()[source]#
Create a run row with
status='active'.Reconciles a stranded lifecycle first: any run (and its still-open iteration/step) left active by a dead loop is stamped
exited, so run resolution stays unambiguous. This is safe becausestart.shrefuses to launch while the node’s tmux session exists – one loop per node – so an open row here is provably orphaned.The row stamps the cost cap armed at launch (
max_cost, NULL when uncapped), so the arm survives later config retunes. The reconcile and the insert commit as one transaction, so a crash between them can never close the stranded rows without their successor.- Return type:
int- Returns:
Run ID.
- Parameters:
self (
Record)
- run_end(*, run_id, status, exit_code, metadata=None)[source]#
End a run.
First-writer-wins: stamps
status,exit_code, andended_atonly while the run is still open (ended_at IS NULL), so a racing kill and the loop’s own end can’t overwrite each other. Duration is derived fromstarted_at/ended_at; cost rolls up fromsteps– neither is stored on the run.- Parameters:
run_id (
int) – Run to end.status (
str) – Final status.exit_code (
int) – Process exit code.metadata (
str|None) – Optional short reason the run ended (e.g.Reached max iterations) for visibility innode activity; the metadata column is left untouched whenNone.self (
Record)
- Return type:
int- Returns:
Rows transitioned – 0 when another writer already closed it.
- run_open()[source]#
Resolve the open run and re-entry context a resume boot adopts.
Pause parks with the run row open; the resume boot reuses it instead of opening a fresh one (which would re-arm the budget). The run’s newest iteration decides the re-entry: an open one is adopted, re-entering at the first step that never completed – derived from the completed step rows, since a checkpoint or drain park writes no paused row and rows from an earlier pause cycle are stale – while a closed one (a boundary pause) only anchors the numbering. A step paused awaiting approval that was approved while parked is skipped past instead of re-run (
'; unpriced'may have been appended, so the marker matches as a prefix).- Return type:
dict[str,Any] |None- Returns:
{'run_id', 'iter', 'iter_id', 'resume_step'}–iter_idandresume_stepareNoneat a boundary,iterisNonewhen the run has no iterations – orNonewhen no run is open.- Parameters:
self (
Record)
- run_latest(*, branch=None)[source]#
Return the latest run ID for a branch – active first, else most recent.
Mirrors the current-run resolution the cost family uses, but keyed by an explicit
branchso it also answers for a deleted node: registry rows die with the node while its runs persist (keyed by branch text), and this is the entry point for reading them.- Parameters:
branch (
str|None) – Branch to resolve; this node’s own branch if omitted.self (
Record)
- Return type:
int|None- Returns:
Run ID, or
Nonewhen the branch has no recorded runs.
- run_branches()[source]#
Return every branch with a recorded run, one entry per branch.
The companion to
run_latest()for deleted nodes: registry rows die with the node while its runs persist keyed by branch text, so this is the name pool a short-name lookup expands against.- Return type:
list[str]- Returns:
Distinct branch names from the runs table.
- Parameters:
self (
Record)
- iter_start(*, run_id, iter)[source]#
Create an iteration row with
status='active'.- Parameters:
run_id (
int) – Parent run.iter (
int) – Iteration number within the run.self (
Record)
- Return type:
int- Returns:
Iteration ID.
- iter_end(*, iter_id, status, exit_code, metadata=None)[source]#
End an iteration.
First-writer-wins via the
ended_at IS NULLguard. Duration is derived fromstarted_at/ended_atand cost rolls up fromsteps– neither is stored. Records the default agent’s session (continuous mode) so the iteration stays resumable. An iteration whose configured model was unset inherits the steps’ recorded (stream-reported) model when every step agrees, so a defaulted spawn’s model stays recoverable from the row.- Parameters:
iter_id (
int) – Iteration to end.status (
str) – Final status.exit_code (
int) – Iteration exit code.metadata (
str|None) – Optional short failure reason (e.g.timed out) for visibility innode activity; the metadata column is left untouched whenNone.self (
Record)
- Return type:
int- Returns:
Rows transitioned – 0 when another writer already closed it.
- step_start(*, iter_id, run_id, step, step_name)[source]#
Create a step row with
status='active'.The agent and its real session are recorded later by
step_session(), captured from the agent’s output stream (so they are set in detached mode too, enabling after-the-fact resume).- Parameters:
iter_id (
int) – Parent iteration.run_id (
int) – Parent run.step (
int) – Step number within the iteration.step_name (
str) – Step name (e.g.EXECUTE).self (
Record)
- Return type:
int- Returns:
Step ID.
- step_cost(*, step_id, cost)[source]#
Record cost for a step.
A figure landing on a row already marked
unpriced(the per-frame flush racing a kill) strips the stale marker.- Parameters:
step_id (
int) – Step to update.cost (
float) – Cost in USD.self (
Record)
- Return type:
None
- step_session(agent, *, step_id, model, session)[source]#
Record the agent, model, and real session for a step.
Captured from the agent’s output stream by the stream driver for both agents, so it is recorded even in detached mode (enabling after-the-fact resume and per-agent cost attribution).
- Parameters:
agent (
str) – Agent that ran the step (e.g.claudeorcodex).step_id (
int) – Step to update.model (
str|None) – The model that actually ran the step – stream-reported where the agent names it, else the configured model (frontmatter or node default), orNonewhen neither names one.session (
str) – Real, agent-specific session.self (
Record)
- Return type:
None
- step_end(*, step_id, status, exit_code, metadata=None)[source]#
End a step.
First-writer-wins via the
ended_at IS NULLguard, so a kill racing the loop’s own end can’t overwrite the outcome. Duration is derived fromstarted_at/ended_at; thecostcolumn is left untouched (recorded separately bystep_cost(), possibly after the step has ended).An abnormal end (any terminal but
completed) whose row has a session but no cost appendsunpricedto its metadata: the agent stream opened, so spend plausibly burned before the first usage flush – the marker lets ledgers tell “free step” from “unpriced step”. Cost stays NULL (Cost.unpriceddiscloses the gap count); a flush that lands later strips the marker.- Parameters:
step_id (
int) – Step to end.status (
str) – Final status.exit_code (
int) – Agent exit code.metadata (
str|None) – Optional short, fractal-owned failure reason (e.g.timed out/agent error) for visibility innode activity; the metadata column is left untouched whenNone.self (
Record)
- Return type:
int- Returns:
Rows transitioned – 0 when another writer already closed it.
- step_pending(*, step_id)[source]#
Mark a step as requiring approval.
The
approvedcolumn has three states: NULL (does not require approval),''(pending approval), and an ISO 8601 timestamp (approved). This method transitions from NULL to''– the guard admits no other starting state, so a stray re-pend can never demote an approval back to pending.The fresh row supersedes any earlier still-pending row of the same iteration step (a re-run after an unapproved pause): the stale row would otherwise sit in
pendingforever, silently swallowing approvals aimed at it. Superseding is gated on this row winning the mark, and voiding guards on the twin still being pending – a stray re-pend can never void another row’s live gate, and an approval landing between the read and the void survives.- Parameters:
step_id (
int) – Step to mark.self (
Record)
- Return type:
None
- step_approve(*, step_id)[source]#
Approve a step (set
approvedto the current UTC timestamp).The low-level write; the step is validated (exists and requires approval) by the sole caller
Node.child_approvebefore the approve event is logged. The stamp is a compare-and-swap on the live gate (approved = ''): first-approval-wins, so a re-approve keeps the original instant and an approval aimed at a superseded (voided) gate writes nothing instead of resurrecting it.- Parameters:
step_id (
int) – Step to approve.self (
Record)
- Return type:
int- Returns:
Rows transitioned – 0 when the gate was not pending.
- step_approved(*, step_id)[source]#
Check whether a step is approved.
Returns
Trueif the step does not require approval (approvedis NULL) or has been approved (approvedis a timestamp). ReturnsFalseonly when the step requires approval but has not been approved yet (approvedis an empty string).- Parameters:
step_id (
int) – Step to check.self (
Record)
- Return type:
bool- Returns:
Whether the step is approved or requires no approval.
- event_start(event, *, metadata='', run_id=None, iter_id=None, step_id=None)[source]#
Log an event.
A caller that knows the event’s lineage passes it explicitly (the loop’s commit step, for example); otherwise the lineage resolves via
resolve_context()(active-only):run_idis the active run (NULL when none is active – an event carries a run only if it fired inside one), andstep_id/iter_idare the in-flight step/iteration, so akillnames the interrupted step. Every row stamps itsactor: the calling node’s branch when the write comes from inside a node (loop-written events self-attribute), the operator otherwise. No-op if the node is not initialized.- Parameters:
event (
str) – Event type (one offractal.constants.EVENTS).metadata (
str) – Free-form context string (e.g. a child branch, a commit sha, or a reason).run_id (
int|None) – Run the event belongs to (skips resolution when any lineage id is passed).iter_id (
int|None) – Iteration the event belongs to.step_id (
int|None) – Step the event belongs to.self (
Record)
- Return type:
int|None- Returns:
Event ID, or
Noneif the node is not initialized.- Raises:
ValueError –
eventis not a known type, or the explicit lineage does not chain (step_idwithoutiter_id/run_id, oriter_idwithoutrun_id).
- event_end(*, event_id, status, exit_code=None)[source]#
End an event.
Events are point-in-time log entries (no
ended_at); this just records the action’s finalstatusand optionalexit_code.- Parameters:
event_id (
int) – Event to end.status (
str) – Final status.exit_code (
int|None) – Event exit code.self (
Record)
- Return type:
None
- signal_get(signal, *, run_id=None)[source]#
Return signal metadata, or
Noneif not set.Auto-resolves
run_idfrom the latest run if not provided.- Parameters:
signal (
str) – Signal identifier.run_id (
int|None) – Run to check. Auto-resolved if omitted.self (
Record)
- Return type:
str|None- Returns:
Signal metadata string, or
None.
- signal_set(signal, metadata='')[source]#
Append a signal to the database.
Resolves
run_idfrom the latest active run, falls back to the most recent run regardless of status. No-op if no runs exist.- Parameters:
signal (
str) – Signal identifier (finish,stop,kill,pause,exit).metadata (
str) – Text payload.self (
Record)
- Return type:
None
- signal_clear(signal, *, run_id=None)[source]#
Delete a run’s rows for one signal.
Signals are append-only except the deliberate withdrawals:
finish_cancel, and the resume boot clearing thepauserows that parked the run it adopts (it would otherwise re-park on them at its first checkpoint).- Parameters:
signal (
str) – Signal identifier.run_id (
int|None) – Run to clear. Auto-resolved if omitted.self (
Record)
- Return type:
None
- runs(*, status=None, limit=None)[source]#
List this node’s run rows, newest first.
- Parameters:
status (
str|None) – Include only runs with this status.limit (
int|None) – Maximum rows to return.self (
Record)
- Return type:
list[dict[str,Any]]- Returns:
Run rows.
- iters(*, run_id=None, status=None, limit=None)[source]#
List this node’s iteration rows, newest first.
- Parameters:
run_id (
int|None) – Include only iterations of this run.status (
str|None) – Include only iterations with this status.limit (
int|None) – Maximum rows to return.self (
Record)
- Return type:
list[dict[str,Any]]- Returns:
Iteration rows.
- steps(*, run_id=None, iter_id=None, status=None, limit=None)[source]#
List this node’s step rows, newest first.
- Parameters:
run_id (
int|None) – Include only steps of this run.iter_id (
int|None) – Include only steps of this iteration.status (
str|None) – Include only steps with this status.limit (
int|None) – Maximum rows to return.self (
Record)
- Return type:
list[dict[str,Any]]- Returns:
Step rows.
- events(*, run_id=None, event=None, status=None, limit=None)[source]#
List this node’s event rows, newest first.
- Parameters:
run_id (
int|None) – Include only events of this run.event (
str|None) – Include only events of this type.status (
str|None) – Include only events with this status.limit (
int|None) – Maximum rows to return.self (
Record)
- Return type:
list[dict[str,Any]]- Returns:
Event rows.
- signals(*, run_id=None, signal=None, limit=None)[source]#
List this node’s signal rows, newest first.
- Parameters:
run_id (
int|None) – Include only signals of this run.signal (
str|None) – Include only signals with this identifier.limit (
int|None) – Maximum rows to return.self (
Record)
- Return type:
list[dict[str,Any]]- Returns:
Signal rows.
- activity(*, limit=None)[source]#
Return this node’s activity-view rows, most recent first.
The
activityview unifies entity start/end rows (with deriveddurationand rolled-upcoston the end rows) and the node’s point-in-time events, each carrying its run/iter/step lineage. The LEFT JOINs add the display numbers the surrogate ids stand for – the iteration-relativestepnumber,step_name, and the run-relativeiter.- Parameters:
limit (
int|None) – Maximum rows to return.self (
Record)
- Return type:
list[dict[str,Any]]- Returns:
Activity rows.
fractal.core.render module#
Functions for template rendering and prompt assembly.
- fractal.core.render.render(text, variables)[source]#
Substitute
$VARplaceholders intotext.Substitution matches GNU
envsubst($NAME/${NAME}only; unknown placeholders and$$pass through verbatim) – the grammar the renderer is pinned against.- Parameters:
text (
str) – The text to render.variables (
dict[str,str]) – Aname -> valuemap of template variables.
- Return type:
str- Returns:
The rendered text.
- fractal.core.render.variables(node, overrides=None)[source]#
Derive a node’s full template variable map (
overrideswin).The static variables (paths, git, config, modes) are the single derivation the loop and every other renderer share. Run-scoped variables (step/iteration/budget and the continue/resume/reserve modes) are not derived here – a caller supplies them via
overrides(the loop with live state, a chat withN/A (chat)); any placeholder left unsupplied stays verbatim.- Parameters:
node (
Node) – The node whose variables to derive.overrides (
dict[str,str] |None) – Variable values layered over (and winning against) the derived map.
- Return type:
dict[str,str]- Returns:
A
name -> valuemap of the template variables.
- fractal.core.render.prompt(node, step_text, overrides=None)[source]#
Assemble and render a step’s full prompt.
The prompt is the
NODE.mdcharter, the step body (frontmatter stripped), and every active mode doc, joined by blank lines and substituted in one pass – one merged variable map (variables()) both selects the active modes and renders the text. A mode doc<NAME>.mdactivates when<NAME>_MODEistruein that map: static modes (DETACHED/META) derive from config, run-scoped modes (CONTINUE/RESUME/RESERVE) ride in as overrides.SYNC.mdnever joins – sync runs as its own step.- Parameters:
node (
Node) – The node whose prompt to assemble.step_text (
str) – The step markdown text.overrides (
dict[str,str] |None) – Run-scoped variable values (seevariables()).
- Return type:
str- Returns:
The rendered prompt.
- fractal.core.render.chat_seed(node, *, fresh)[source]#
Render the chat framing prepended to a chat turn.
Always the
modes/CHAT.mdmode; for a fresh chat it is preceded by the node’sNODE.mdcharter. Rendered with the node’s template variables – real paths/limits, plus chat sentinels (N/A (chat)) for the run-scoped fields a chat has no value for.- Parameters:
node (
Node) – The node being chatted with.fresh (
bool) – Also prepend the node’sNODE.mdcharter (for a fresh chat; a fork already carries the node’s context).
- Return type:
str- Returns:
The rendered seed text, or
''when the files are absent.
- fractal.core.render.strip_frontmatter(text)[source]#
Return
textwith a leading---frontmatter block removed.Byte-conservative line handling: lines split on the newline byte alone (a CRLF file keeps its carriage returns, so a CRLF fence line is not a fence), the fence check trims leading whitespace only, and a block that never closes swallows the rest of the text; every surviving line comes back newline-terminated, so a file without a trailing newline gains one.
- Parameters:
text (
str) – The markdown text.- Return type:
str- Returns:
The body below the frontmatter.
fractal.core.session module#
Implements Sessions class.
- class fractal.core.session.Sessions(node)[source]#
Bases:
objectPer-iteration agent->session-id map plus transcript resolution.
Initialize
Sessions.- get(agent)[source]#
Read an agent’s session for the current iteration.
The
.sessionmap holds the real, resumable, agent-specific session per agent. A missing key means the agent has no session started this iteration.- Parameters:
agent (
str) – Agent name.self (
Sessions)
- Return type:
str|None- Returns:
The agent’s session, or
Noneif not started.
- set(agent, session)[source]#
Record an agent’s session for the current iteration.
- Parameters:
agent (
str) – Agent name.session (
str) – Real, agent-specific session.self (
Sessions)
- Return type:
None
- clear()[source]#
Reset the per-iteration session map (start of each iteration).
- Parameters:
self (
Sessions)- Return type:
None
- transcript(agent, session)[source]#
Read an agent session’s transcript for this node.
Delegates to the agent backend, which owns the id validation, the provider’s transcript layout, and the ownership-gated fallback discovery.
- Parameters:
agent (
str) – Agent name (e.g.claude).session (
str) – The agent session id (asessionvalue off a step or iteration row).self (
Sessions)
- Return type:
dict[str,Any]- Returns:
{agent, session, path, exists, content}–contentis the raw JSONL text (empty when absent).- Raises:
ValueError – If
agentis unsupported, orsessionis not a bare session id (anything but[A-Za-z0-9-]could escape the transcript directory).
fractal.core.time module#
Implements Time class.
- class fractal.core.time.Time(node)[source]#
Bases:
objectDeadline accounting over config timeouts and row instants.
Initialize
Time.- remaining(*, scope=None, run_id=None)[source]#
Compute seconds left before a timeout fires.
Derives each deadline from the configured timeout and the relevant
started_at(mirroring howCost.remainingreads persisted state), so it works for any node, not only the running one. Therunscope (--timeout) is anchored on the active run’sstarted_at; theiterscope (--iter-timeout) on its active iteration’s; thestepscope (--step-timeout) on its active step’s. With noscopethe soonest of the configured deadlines is returned – the time until the next timeout fires.- Parameters:
scope (
str|None) –'run','iter', or'step'to query one level; the soonest of all if omitted.run_id (
int|None) – Run to query. Auto-resolved if omitted.self (
Time)
- Return type:
float|None- Returns:
Remaining seconds (clamped at
0), orNoneif no timeout is configured for the scope or no run/iteration is active.
- limit(scope=None)[source]#
Return the configured limit seconds for a scope (
runistimeout).Pure config – no row reads – so it answers for an idle node too: the
runscope reads the whole-runtimeoutkey,iter/steptheir per-level timeouts; an unsetiter_timeoutfalls back tointerval(the loop bounds an interval iteration by its slot). With noscopethe soonest applicable limit (the smallest configured) is returned.- Parameters:
scope (
str|None) –'run','iter', or'step'to query one level; the soonest of all if omitted.self (
Time)
- Return type:
float|None- Returns:
Limit in seconds, or
Nonewhen nothing applicable is configured (or a stored duration does not parse).
fractal.core.worktree module#
Functions for the node tree’s worktree, registry, and provisioning machinery.
- fractal.core.worktree.lock(repo_dir)[source]#
Hold the tree-wide
.worktreesflock for a critical section.Serializes worktree add/remove and every cap gate –
git worktree addis not parallel-safe; anfcntl.flockis a kernel lock, auto-released if the holder dies. Callers re-read live state under the lock; each re-read keeps its race-naming comment at the call site.- Parameters:
repo_dir (
Path) – Main git repo root.- Return type:
Iterator[None]
- fractal.core.worktree.validate_name(name, parent_branch=None)[source]#
Validate a node name (and, with
parent_branch, its composed branch).- Parameters:
name (
str) – Node name (a single branch segment).parent_branch (
str|None) – Parent branch to compose the child branch under;Nonechecks the segment rules only.
- Raises:
ValueError – If the name breaks a charset, segment, or branch-length rule.
- Return type:
None
- fractal.core.worktree.derive_project_name(path)[source]#
Derive a fractal project name from a directory’s basename.
Converts dashes to underscores and validates the result as an ASCII identifier (approximating
Wiki.validate_name’s ascii/identifier predicates;wiki initadditionally enforces reserved-name and structural rules), so a bad directory name fails before any partial init is written. The project name doubles as the wiki name and, when bootstrapping a fresh repo, the initial branch name.- Parameters:
path (
Path) – Repository (or target folder) directory.- Return type:
str- Returns:
The sanitized project name.
- Raises:
ValueError – If the directory name cannot yield a valid project name.
- fractal.core.worktree.ensure_git_repo(path)[source]#
Bootstrap a git repo at
pathwhen it has no born branch yet.fractal initanchors the user node at the git root, so the target must be a git repo whose branch is born (_init_userresolvesself.branchwithcheck=True). When the target is not inside any repo, initialize one on a branch named after the project – the sanitized directory name, which also becomes the wiki name. Then, unless the branch is already born, birth it with an initial commit (an empty.gitignore); this also completes a prior bootstrap whose commit failed (a fresh.gitwith an unborn branch), so the re-run the identity-error message promises actually works. A repo whose branch is already born (this folder or an ancestor) is left untouched.- Parameters:
path (
str|PathLike) – Repo root or sub-project folder (absolute or relative).- Raises:
ValueError – If the directory name cannot yield a valid project name.
RuntimeError – If the initial commit fails (e.g. no git identity).
- Return type:
None
- fractal.core.worktree.project_path(repo_dir, branch)[source]#
Return a branch’s project sub-path (
'.'for a repo-root node).Cached per-branch at
.worktrees/.project/<branch>(written at init); absent for a repo-root project, which reads as'.'.- Parameters:
repo_dir (
Path) – Main git repo root.branch (
str) – The node’s branch.
- Return type:
str- Returns:
Project sub-path within the worktree.
- fractal.core.worktree.set_project_path(repo_dir, branch, project)[source]#
Record a branch’s project sub-path in the
.projectcache.The single writer (user-node init); every consumer reads through
project_path().- Parameters:
repo_dir (
Path) – Main git repo root.branch (
str) – The node’s branch.project (
str) – Project sub-path within the worktree.
- Return type:
None
- fractal.core.worktree.exclude_update(repo_dir, *, track=False, seed_dir=None)[source]#
Write fractal’s ignore patterns into the repo-local
info/exclude.Fractal’s runtime artifacts – worktrees, databases, status files, agent logs – are local and ephemeral, so they belong in the repo’s
.git/info/exclude(shared across all worktrees), not the user’s committed.gitignore. The patterns live in a marker-delimited block; every prior fractal block is replaced (so new patterns propagate on re-init) and all otherinfo/excludecontent is preserved.Idempotent and concurrency-safe: the common-dir
info/excludeis shared by every worktree, so siblinginit/startfan-out races on it. The rewrite computes the new content from a clean read and commits it with an atomic unique-tempos.replace– a racing writer can never observe a truncated file and drop the user’s lines, and a crash mid-write cannot orphan a half-block.- Parameters:
repo_dir (
Path) – Main git repo root.track (
bool) – The tree tracks.fractal/on the top-level branch, so the seed dir is not ignored.seed_dir (
str|None) – The user node’s own seed dir (<project>/.fractal/<branch>), prepended so the top-level branch ignores it; child seeds (.fractal/<branch>.<child>) stay tracked so meta and merge-up keep working –fractal trackopts the top-level branch back in.Nonecarries the existing block’s seed-dir line forward, so a rewrite that cannot resolve the user node never flips the tracking choice.
- Return type:
None
- fractal.core.worktree.exclude_tracks(repo_dir, seed_dir)[source]#
Return whether the tree tracks the user seed dir on the top-level branch.
Tracking truth lives in the fractal block’s own seed-dir ignore line – the exclude state
fractal track/untracktoggles – so every block rewrite preserves the current choice. The probe is scoped to the block itself, nevergit check-ignore: check-ignore also matches a user.gitignoreentry, which would makefractal tracksilently non-stick. With no block yet (first-ever init) the tree is untracked by definition.- Parameters:
repo_dir (
Path) – Main git repo root.seed_dir (
str) – The user node’s own seed dir (<project>/.fractal/<branch>).
- Return type:
bool- Returns:
Truewhen a fractal block exists without the seed-dir ignore line;Falsewhen the line is present or no block exists.
- fractal.core.worktree.exclude_strip(repo_dir)[source]#
Strip fractal’s block from the shared
info/exclude.The exact inverse of
exclude_update()’s block write: same whole-line markers, all other content preserved.- Parameters:
repo_dir (
Path) – Main git repo root.- Return type:
None
- fractal.core.worktree.ensure_project_wiki(worktree, repo_dir, path, name)[source]#
Create the project wiki if missing; report whether it was created.
The wiki lives at
<worktree>/wiki(repo root) or<worktree>/<project>/wiki(sub-project).nameis the validated display name; the wiki is seeded with the strict ASCII-identifier naming policy. A failedwiki initis surfaced, not swallowed, and a pre-existing directory that is not a wiki is refused, never adopted.- Parameters:
worktree (
Path) – The user node’s worktree root.repo_dir (
Path) – Main git repo root (compared againstnameto note a derived-name adjustment).path (
str) – Project path (.for the repo root).name (
str) – Validated wiki display name.
- Return type:
bool- Returns:
Trueif the wiki was created,Falseif it already existed.
- fractal.core.worktree.verify_hook_formatters(repo_dir)[source]#
Name the formatter-safe lanes when host pre-commit hooks exist.
Hooks that rewrite
wiki/or.fractal/corrupt generated pages and their commits fail loud (mdformat escapes wikilinks and mangles nav delimiters). Stays silent once either safe lane is taken:mdformat-wikiwired in, or a.fractalmention (the shape of any path filter steering hooks off the generated paths). Verify-only: the user’s config is never edited, and no exclude is ever prescribed.- Parameters:
repo_dir (
Path) – Main git repo root.- Return type:
None
- fractal.core.worktree.run_script(package_dir, script, *args)[source]#
Run a bundled
_scripts/script.- Parameters:
package_dir (
Path) – Root of the installedfractalpackage.script (
str) – Script filename in_scripts/.*args (
str) – Arguments to pass to the script.
- Return type:
CompletedProcess[str]- Returns:
Completed process result.
- Raises:
RuntimeError – If the script exits non-zero.
- fractal.core.worktree.cleanup_failed_worktree(repo_dir, branch, *, created_branch=True)[source]#
Roll back a worktree/branch left by a failed child init (best-effort).
A child init that fails after
git worktree add(in init.sh, or in the registration that follows) would otherwise strand a live worktree with no registry row. Remove the worktree and the.projectcache entry so a retry starts clean. The branch is deleted only when this init created it (created_branch); init.sh reuses a pre-existing branch in place, and its committed history must survive a failed init.- Parameters:
repo_dir (
Path) – Main git repo root.branch (
str) – The failed child’s branch.created_branch (
bool) – This init created the branch (else it was reused).
- Return type:
None
- fractal.core.worktree.prune_branch(repo_dir, branch)[source]#
Delete a worktree-less node’s git branch and project-cache entry.
Used for a phantom node (registry row present, worktree already gone) that
delete.shcannot tear down. Best-effort: a missing branch is not an error.- Parameters:
repo_dir (
Path) – Main repo root.branch (
str) – Branch to prune.
- Return type:
None
Module contents#
Core classes for fractal.