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 Agent class registered for a base command.

Consults the deployment hook file under root once per process (its subclasses register by name), 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 Agent subclass.

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]) – Agent subclass, 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: object

A spawnable agent invocation.

An _invocation hook sets env to only its reserved provider keys (or None); the public invocation verb composes the full Popen environment (os.environ, then the caller overlay, then those reserved keys), so a composed invocation’s env passes straight to Popen and None inherits the parent’s. A reserved key valued None means unset: the verb pops it from the merged environment, so a hook can scrub keys the ambient environment inherited from an ancestor’s route. session carries 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: object

One semantic frame parsed from an agent’s output stream.

kind is one of: 'session' | 'text' | 'tool' | 'tool_result' | 'cost' | 'result' | 'error'. Fields are populated per kind; absent facts stay None (a codex result carries no cost). Consumers (renderer, TUI bubbles, record verbs) branch on kind only – 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: object

Outcome 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: object

Incremental 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 across feed calls so callers can attribute a step after draining.

Parameters:

Initialize StreamParser.

Parameters:
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:
Return type:

list[StreamEvent]

class fractal.core.agent.Agent(node, command=None, provider=None)[source]#

Bases: object

Base 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 the on_verb hooks (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.

Parameters:
  • self (Agent)

  • node (Node)

  • command (str | None)

  • provider (str | None)

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; None means native (Node.agent threads 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.

None means the vendor’s own endpoint. Node.agent threads 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.

property logger: Logger[source]#

Return the stdlib logger named for the concrete class.

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=None starts fresh; a session resumes in place, or forks with fork=True (the backend’s NotImplementedError surfaces 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; None starts fresh.

  • fork (bool) – Fork session instead 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 – base os.environ, then the overlay, then _invocation’s provider-specific keys, with any key whose merged value is None popped; spawn kwargs stay supervision-only.

  • self (Agent)

Return type:

Invocation

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 passes start_new_session=True and a stderr file; chat inherits stderr; the TUI pipes it). All launch surfaces route Popen through here, so overriding _spawn is the single sanctioned way for a host to redirect execution (a wrapper binary, a container, a remote executor).

Parameters:
Return type:

Popen

parser(*, model=None)[source]#

Return a fresh stream parser (constructed from __parser__).

Parameters:
  • self (Agent)

  • model (str | None)

Return type:

StreamParser

stream(lines, *, step_id=None, model=None, detached=False, render=None)[source]#

Drive a parser over the agent’s stdout lines.

Fires on_call at entry and on_call_success/ on_call_failure at exit; dispatches each parsed event to its hook (on_session once, on_action per tool, on_error per stream-borne failure, on_budget on a budget stop), the render callback, and – when step_id is 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; None records 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 .session map 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:

StreamResult

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_session with 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 .session map 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_cost with 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. None is 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. path rides 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 (a session value off a step or iteration row).

  • self (Agent)

Return type:

dict[str, Any]

Returns:

{agent, session, path, exists, content}content is the raw JSONL text (empty when absent).

Raises:

ValueError – If session is 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, fires on_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:

AgentCallEvent

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:

AgentCallSuccessEvent

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:

AgentCallFailureEvent

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:

AgentSpawnEvent

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:

AgentActionEvent

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:

AgentSessionEvent

on_record_session(message=None, *, logging_level=20, **kwargs)[source]#

Handle session record event.

Fired by record_session with exactly the persisted values.

Parameters:
  • self (Agent)

  • message (str | list[str] | None)

  • logging_level (int)

  • kwargs (Any)

Return type:

AgentSessionEvent

on_record_cost(message=None, *, logging_level=10, **kwargs)[source]#

Handle cost record event.

Fired by record_cost with the settled figure that lands on the ledger.

Parameters:
  • self (Agent)

  • message (str | list[str] | None)

  • logging_level (int)

  • kwargs (Any)

Return type:

AgentCostEvent

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:

AgentBudgetEvent

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:

AgentErrorEvent

on_preflight(message=None, *, logging_level=20, **kwargs)[source]#

Handle agent preflight event.

Fired before a preflight probe runs.

Parameters:
  • self (Agent)

  • message (str | list[str] | None)

  • logging_level (int)

  • kwargs (Any)

Return type:

AgentPreflightEvent

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: InitialEvent

Emitted 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: TerminalEvent

Emitted after an agent stream drains cleanly.

Parameters:

Initialize terminal event.

Pairs with the corresponding initial_event and computes duration as 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: FailureEvent

Emitted when an agent stream fails.

Parameters:

Initialize terminal event.

Pairs with the corresponding initial_event and computes duration as 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: Event

Emitted 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: Event

Emitted 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: Event

Emitted 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: Event

Emitted 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: Event

Emitted 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: Event

Emitted 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: Event

Emitted 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 unless local is set.

Parameters:
  • node (Node) – The node whose worktree to commit.

  • message (str) – Short description appended to the commit message.

  • init (bool) – Use the init label instead of iteration <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; a force commit’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, else 0, 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 a message carrying the subject’s own labels (the branch name or iteration).

fractal.core.commit.commit_user_init(node, message)[source]#

Commit a user node’s baseline: the project wiki (and node data when tracked).

The --init baseline 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 .gitattributes merge attribute wiki init wrote; on a tree opted in via fractal track the 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: object

Read/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.

Parameters:

Initialize Config.

Parameters:
  • node (Node) – The owning Node instance.

  • self (Config)

property node: Node#

Return the owning node. :type self: Config :param self:

property path: Path#

Return the path to the node’s config.json. :type self: Config :param self:

load()[source]#

Read and parse the node’s config.json.

A hand-corrupted config otherwise yields a context-free Expecting value: line 1 column 1 from 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.json does 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 default if 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 key is fixed at init (IMMUTABLE_KEYS) and value would 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) in config, so it can be called with a node’s effective (merged) config; None validates the stored config.json – the Node.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 broken step <= iter <= run cost ordering, a non-integer or degenerate integer cap (a non-positive max_iters reads as unlimited in the loop), a non-bool mode flag (the loop’s bool() coercion reads a hand-edited "false" string as True), 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 stored config.json if 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 the nodes registry 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 update writes 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: object

Cost ledger readers over config caps and step-cost rows.

Parameters:

Initialize Cost.

Parameters:
  • node (Node) – The owning Node instance.

  • self (Cost)

property node: Node#

Return the owning node. :type self: Cost :param self:

property db: Database#

Return the central database. :type self: Cost :param self:

remaining(*, run_id=None, iter_id=None, step_id=None)[source]#

Compute remaining cost budget for a run, iteration, or step.

--max-cost is a per-run ceiling: by default this returns max_cost minus the current run’s subtree spend (own steps plus descendants chained by parent_run_id; the active run, else the most recent), so a budget drained in one run is fresh again in the next. Pass run_id for a specific run. iter_id/step_id instead 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’s max_iter_cost headroom.

  • step_id (int | None) – Scope to a step’s max_step_cost headroom.

  • self (Cost)

Return type:

float | None

Returns:

Remaining cost in USD, or None if 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 --run scoping. Pass run_id for a specific run. Includes child node costs: a child counts only the runs it spawned under this run’s lineage, chained via parent_run_id (the per-run subtree) – a deleted child’s recorded runs still count. Pass max_depth=0 for this node only, max_depth=1 to include children, etc.

When iter_id (or step_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. None includes all descendants, 0 is 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 NULL cost, so its spend sums to 0 yet is not actually $0. Returns True when the scope has unknowable (NULL-cost) steps and none recorded a real figure, letting the CLI show untracked instead of $0 so 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 mirrors spent: it walks the per-run subtree (to max_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 (None all descendants, 0 this node only).

  • self (Cost)

Return type:

bool

Returns:

True if the scope has NULL-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 (marked unpriced on 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 mirror spent.

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 (None all descendants, 0 this node only).

  • self (Cost)

Return type:

int

Returns:

Number of ended steps in the scope with NULL cost.

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_id chain from run_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 as 0). Powers fractal 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 for max_depth=0), each still-registered descendant follows with its cap (idle children spend 0.0), then any lineage descendant whose registry row is gone (max_cost None, deleted True) – its spend would otherwise vanish from the table while still counting in spent(), so the rows always sum to it. When deleted names 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 via parent_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_id walk.

The DB-authoritative all-time counterpart to run_spend(): it walks the persisted runs table, so a deleted or orphaned descendant’s runs still count – matching Cost.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: object

Thin SQLite wrapper for the tree’s central database.

Provides generic table operations – init, read, write, update, merge, delete, exists, and count – with no domain-specific logic. Each operation runs on its own one-shot handle unless passed a transaction() connection via connection=, which makes a multi-statement block one atomic unit.

Parameters:
  • self (Database)

  • path (Path)

  • schema (Path)

Initialize Database.

Parameters:
  • path (Path) – Path to the .db file.

  • schema (Path) – Path to the .sql schema executed by init.

  • self (Database)

property path: Path#

Return the database file path (read-only). :type self: Database :param self:

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 EXISTS DDL 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 .db is 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 init and persists in the file header) and gives every handle a 30-second busy timeout (overriding sqlite3.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. timeout overrides 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 IMMEDIATE transaction 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 without connection= deadlocks against the block’s own lock until the busy timeout, and a read without it silently sees the pre-transaction snapshot. BEGIN IMMEDIATE takes 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 query or table must be provided. query is mutually exclusive with where and limit. When query is given, it is executed as raw read-only SQL with params bound positionally to its ? placeholders. Otherwise, a SELECT is built from table, where, and limit.

Built SELECT``s are ordered by ``rowid DESC – the true write order (a monotonic alias of the INTEGER PRIMARY KEY) – so the most recently written row is first, without assuming any particular timestamp column (tables name their start started_at or created_at). A raw query keeps its own ordering (none is imposed); limit=0 is valid and returns no rows.

Parameters:
  • table (str | None) – Table name.

  • query (str | None) – Raw SQL query (mutually exclusive with where and limit).

  • params (tuple[Any, ...]) – Parameters bound to the query’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 (from transaction()).

  • 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 (from transaction()).

  • 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 (from transaction()).

  • 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 in data – every other column keeps its stored value. This is a real upsert: a partial merge (e.g. just status) preserves the row’s other columns rather than wiping them the way a whole-row INSERT OR REPLACE would. A partial merge must still supply every NOT NULL column that lacks a SQL default: the base INSERT’s constraints are checked before the conflict routes to the update, so omitting one fails (it is not backfilled). Without conflict it falls back to INSERT 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. None does a whole-row insert-or-replace.

  • connection (Connection | None) – Transaction to run inside (from transaction()).

  • 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 (from transaction()).

  • 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 (from transaction()).

  • 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 (from transaction()).

  • 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: object

Point-in-time observability record with snapshot semantics.

Payload fields are bare class annotations on subclasses; __init__ extracts matching kwargs via typing.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. When info is set, returns EVENT_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 when message is 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: Event

Emitted 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: Event

Emitted at the end of a paired operation; computes duration.

Parameters:

Initialize terminal event.

Pairs with the corresponding initial_event and computes duration as 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: TerminalEvent

Emitted when a paired operation fails.

Parameters:

Initialize terminal event.

Pairs with the corresponding initial_event and computes duration as 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: object

External work-product surface (list/read/write/commit/archive).

Parameters:

Initialize Files.

Parameters:
  • node (Node) – The owning Node instance.

  • self (Files)

property node: Node#

Return the owning node. :type self: Files :param self:

property db: Database#

Return the central database. :type self: Files :param self:

property worktree: Path#

Return the node’s resolved worktree path. :type self: Files :param self:

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, and wiki//.fractal/ entries list like any other content (consumers filter or collapse them). With since the set is instead the node’s own contribution: files its own commits touched (a first-parent walk from the since anchor – a tree at HEAD contains 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>...HEAD diff.

Parameters:
  • path (str | None) – Restrict to a worktree-relative subtree; all files if None.

  • since (str | None) – List changed files instead, from base (the node’s fork point), commit (the previous commit), iteration, or run; the full tracked listing if None.

  • self (Files)

Return type:

list[dict[str, Any]]

Returns:

[{path, size}] sorted by path (path worktree-relative). A changed listing’s entries also carry change (added/ modified/deleted) and additions/deletions line counts (None for a binary file); a deleted file is kept (size 0) 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, given since, part of that anchor’s changed set – so a deleted file’s old content stays readable without exposing anything else. before reads the file as it was at the since anchor (via git 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) returns exists=False with empty content.

Parameters:
  • path (str) – Worktree-relative file path.

  • max_lines (int | None) – Cap the returned text to this many lines (full if None); the cap preserves line terminators, so the included portion matches the raw bytes.

  • since (str | None) – Diff scope – base, commit, iteration, or run (see list()).

  • before (bool) – Read the file at the since anchor 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 returns binary=True with empty content (callers download it via path() instead).

Raises:

ValueError – If before is set without since, or path is 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 path is 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()): .fractal paths 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 path escapes 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 --init baseline, 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. No commit event 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 the iteration/run diff anchors.

Parameters:
  • paths (list[str]) – Worktree-relative paths to stage and commit.

  • message (str) – Short description appended to the commit message.

  • self (Files)

Return type:

dict[str, Any]

Returns:

{committed, sha, paths} – whether a commit was made (False with a None sha when the paths held no change) and the normalized paths.

Raises:
  • RuntimeError – If the node is paused.

  • ValueError – If paths is empty, message is 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.

Parameters:
  • path (str | None) – Restrict to a worktree-relative subtree; all files if None.

  • since (str | None) – Archive the changed set instead (see list()).

  • self (Files)

Return type:

bytes

Returns:

The zip archive bytes.

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:
  • path (str) – Worktree-relative file path.

  • since (str) – History scope – base (default), commit, iteration, or run (see list()).

  • self (Files)

Return type:

list[dict[str, Any]]

Returns:

[{sha, instant, subject, additions, deletions}] newest first – instant is the commit’s author time (ISO 8601), the counts per-commit numstat (None for 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: object

One 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 matching key: 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 (0 for the SYNC pseudo-step).

Return type:

Step

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: true frontmatter 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:

property timeout: str | None#

Return the timeout: frontmatter override, if any. :type self: Step :param self:

property detached_raw: str | None#

Return the raw detached: frontmatter value, 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: object

Outcome 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.

status is one of 'completed' | 'timed out' | 'skipped' | 'paused' | 'failed'; reason carries 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 clean completed with budget_stopped set, 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: object

One 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); None renders 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).

property logger: Logger[source]#

Return the stdlib logger named for the concrete class.

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 to Node._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:

LoopIterationEvent

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:

LoopIterationSuccessEvent

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:

LoopIterationFailureEvent

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:

LoopStepEvent

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:

LoopStepSuccessEvent

on_step_failure(message=None, *, logging_level=30, **kwargs)[source]#

Handle step failure event.

Fired when a step fails.

Parameters:
  • self (Loop)

  • message (str | list[str] | None)

  • logging_level (int)

  • kwargs (Any)

Return type:

LoopStepFailureEvent

class fractal.core.loop.LoopIterationEvent(source, message=None, **kwargs)[source]#

Bases: InitialEvent

Emitted 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: TerminalEvent

Emitted when an iteration ends in-band.

Any honest attribution: completed, paused, stopped.

Parameters:

Initialize terminal event.

Pairs with the corresponding initial_event and computes duration as 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: FailureEvent

Emitted when an iteration fails.

A setup crash-loop, a fatal SYNC timeout, or an unhandled loop error.

Parameters:

Initialize terminal event.

Pairs with the corresponding initial_event and computes duration as 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: InitialEvent

Emitted 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: TerminalEvent

Emitted when a step ends with a non-failure attribution.

Completed, skipped, paused, or a budget stop.

Parameters:

Initialize terminal event.

Pairs with the corresponding initial_event and computes duration as 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: FailureEvent

Emitted when a step fails (agent error / stream error / timeout).

Parameters:

Initialize terminal event.

Pairs with the corresponding initial_event and computes duration as 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: object

An autonomous agent node in a git worktree.

Tracks status in a .status file 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, via resolve_user()); None reads the live git branch.

  • self (Node)

property is_user: bool#

Return whether this is a user (root) node. :type self: Node :param self:

property db: Database[source]#

Return the central database, hosted in the root node’s data directory.

Resolved through the root config 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 .db without a worktree lookup.

property radio: Radio[source]#

Return the node radio.

property config: Config[source]#

Return the node configuration surface.

property cost: Cost[source]#

Return the node cost ledger (config caps over step-cost rows).

property files: Files[source]#

Return the node work-product surface (project files).

property plans: Plans[source]#

Return the node plan files.

property record: Record[source]#

Return the node execution record (run/iter/step spans, events, signals).

property sessions: Sessions[source]#

Return the node per-iteration agent session map.

property time: Time[source]#

Return the node deadline accounting (config timeouts over row instants).

property logger: Logger[source]#

Return the stdlib logger named for the concrete class.

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 Node handle – teardown/re-init paths construct fresh handles, so a fresh Node is a fresh read.

property worktree: Path#

Return the node’s resolved worktree path. :type self: Node :param self:

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 (None for the user node).

Also None when 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 (_NODE env var set), returns a Node bound to the caller’s worktree. Returns None outside 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 Node keys 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 the config.json marked user: true and pin a Node to 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 None when 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, or all). config copies 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; default 10s).

  • 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 optional max_cost retune) 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 to idle under the .worktrees flock; the loop stamps active at boot just as a fresh start does. A budget-ended run (the run row’s exited/0 landing) never re-arms silently: a bare continue refuses, naming the spent and armed figures, and an explicit max_cost is applied through the parent’s retune (child_retune()) before the launch.

Every successful launch logs a completed start event (metadata continue on 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).

attach()[source]#

Attach to the node’s tmux session.

Parameters:

self (Node)

Return type:

None

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 finish signal.

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 finish fans 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.sh no-ops and the open rows close killed). A booting node – session up, active not 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 pause then aborts each node’s in-flight agent invocation (pause.sh reaps the recorded step process group), so every loop reclassifies the abort and parks: it exits with status paused, leaving its run and iteration rows open for resume() 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/start refuse 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 active again before its parent’s drain-waits can look. A node still parking (active with 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 targeted resume() refuse – and a booting loop parks – while any ancestor (or the node itself) is paused or still active with a pending pause signal, 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 None when the path is clear.

merge()[source]#

Squash-merge the node’s branch into its merge target.

merge.sh resolves the target – the node’s configured base if 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 the merge event 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 --hard all 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, delete cannot run – it needs the worktree. name is 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. self must 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 name matches 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 delete legitimately leaves its registry rows behind, but nothing records the removal (list flags such rows display-only). Logs one orphan event per newly observed orphan, giving out-of-band cleanup an audit trail. Registry rows are kept – delete --force (deregister) remains the removal path.

Return type:

list[str]

Returns:

Branches newly recorded as orphaned.

Parameters:

self (Node)

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) so unretire can 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 .status file set by hand) the node resets to idle.

An idle restore re-enters the unsettled pool, so it re-checks the width/descendant gates (_enforce_rearm_limits()) under the .worktrees flock; 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’s info/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) and destroy (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 .status file – lifecycle state is kept out of config.json so config is purely user settings.

Return type:

str

Returns:

Status string (idle when no .status file 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 is active.

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 .status file, and updates the node’s row in the central nodes registry.

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.json and 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 nodes table 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 as active. Cap columns render each present child’s live config values; a gone worktree keeps the registry caps. The last column renders each row’s newest activity instant as a compact age, flagged (12m!) when an active node has sat quiet past max(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 crashed active node (no live tmux session) to exited, and a booting idle node (live session, the loop not yet stamped) to active (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, None included: a --reset re-init upserts over the old row, and an omitted cap must clear there just as it does in the reseeded config.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 nodes table and the child’s config.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’s config.json (the nodes table has no column).

  • max_step_cost (float | None) – New per-step cost cap in USD; lives only in the child’s config.json (the nodes table has no column).

  • reserve_budget (float | None) – New cleanup reserve in USD; lives only in the child’s config.json (the nodes table has no column).

  • step_timeout (str | None) – New per-step time budget; lives only in the child’s config.json (the nodes table 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’s config.json together): 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’s N% 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 == new included, 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_list(*, max_depth=None)[source]#

List registered children.

Parameters:
  • max_depth (int | None) – Maximum depth relative to this node. 1 lists direct children only, 2 includes grandchildren, None lists all descendants.

  • self (Node)

Return type:

list[dict] | None

Returns:

List of child records, or None if the node is not initialized.

child_approve(child, *, step_id=None)[source]#

Approve a gated step in a child, recording approve on both event logs.

self is the parent. Enforces direct parentage, writes the approval to the child’s step (the source of truth), and logs an approve event on the parent (naming the child + step) and on the child (its own step). The events are best-effort audit.

Parameters:
  • child (Node) – The direct child whose step to approve.

  • step_id (int | None) – The child’s step to approve; defaults to the child’s active (gated) step.

  • self (Node)

Return type:

int

Returns:

The approved step’s id.

Raises:
  • PermissionError – If child is not a direct child of self.

  • 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 an active or paused step row – as {'branch', 'step_id', 'step', 'step_name'}. Only direct children are listed – the steps this node can actually approve.

Return type:

list[dict]

Returns:

Pending-approval rows across the direct children.

Parameters:

self (Node)

commit(message=None, *, init=False, check=False, ignore_scope=False, force=False)[source]#

Commit the current iteration’s work.

Delegates to the core.commit pipeline (scope check, lint, stage, commit, and push unless local is set).

Parameters:
  • message (str | None) – Short description appended to the commit message.

  • init (bool) – Use the init label instead of iteration <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 than force).

  • 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 message is missing without check.

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:

Agent

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.md and modes/CHAT.md. current forks the node’s live loop session; session forks a given id (or, with resume, 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 with resume).

  • current (bool) – Fork the node’s live loop session (mutually exclusive with session/resume).

  • resume (bool) – Continue session in 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 None if the stream carried none.

Raises:
  • ValueError – No agent configured; incompatible flags; current with 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.md fresh, CHAT.md on 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 with resume).

  • current (bool) – Fork the node’s live loop session (mutually exclusive with session/resume).

  • resume (bool) – Continue session in place (same id) instead of forking.

  • model (str | None) – Model override; defaults to the node’s configured model.

  • self (Node)

Return type:

Invocation

Returns:

The spawnable agent invocation.

Raises:

ValueError – No agent configured; incompatible flags; current with no live session; resuming the live loop session; or forking a codex session.

render_template(template, *, overrides=None)[source]#

Substitute the node’s $VAR placeholders into template.

The variable map is render.variables() (overrides win). Substitution matches GNU envsubst ($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 (see render_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 None when 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; None means the vendor’s own endpoint.

Return type:

str | None

Returns:

The effective provider route, or None when 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 match start.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: object

Plan files under the node’s plans/ directory.

Parameters:

Initialize Plans.

Parameters:
  • node (Node) – The owning Node instance.

  • self (Plans)

property node: Node#

Return the owning node. :type self: Plans :param self:

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-slugged name). Plans are found later by globbing the {run.iter} segment (see list()).

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-slugged name when 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.

Parameters:
  • iter_ref (str) – The {run}.{iter} reference (e.g. 12.5).

  • self (Plans)

Return type:

list[Path]

Returns:

Matching plan paths, sorted by name (chronological by timestamp).

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.pricing.has_model(model, /)[source]#

Return whether a model is present and priced in the cache.

Parameters:

model (str)

Return type:

bool

fractal.core.pricing.rates(model, /)[source]#

Return a model’s per-token rate entry, or None when unpriced.

Return type:

dict[str, Any] | None

Returns:

The LiteLLM rate entry for model, or None when the model is absent from the cache or carries no rate keys.

Parameters:

model (str)

fractal.core.radio module#

Implements Radio class.

class fractal.core.radio.Radio(node)[source]#

Bases: object

Inter-node messaging for autonomous agent nodes.

Provides message sending, channels, subscriptions, threads, reactions, and read tracking in the tree’s central database.

Parameters:

Initialize Radio.

Parameters:
  • node (Node) – The owning Node instance.

  • self (Radio)

property node: Node#

Return the owning node. :type self: Radio :param self:

property db: Database#

Return the central database. :type self: Radio :param self:

init()[source]#

Seed default channels and auto-subscribe to parent/children.

Called from Node.init once the node’s config is written. Does:

  1. Insert default channels.

  2. Derive the parent from the branch name; subscribe to its readable channels.

  3. Query the nodes table 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 node column names the host). Checks write_only permission and rejects if write_only = 1 and the writer is not the owner.

The permission check is best-effort, not atomic across connections: the write_only read and the db.write use separate connections, so a concurrent channel_delete of 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 with node).

  • subject (str) – Message subject.

  • data (str) – Message data.

  • priority (int) – Priority (0-10).

  • post (bool) – Channel-class guard: True requires a publicly readable channel (read_only = 0, the post verb); False (the send verb 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 with reply, so callers can surface the routing without re-deriving it.

Raises:
  • ValueError – If parent and node are both set, the parent or target node is not found, priority is out of range, the channel is not found, or post is 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 force is set, since deleting a thread also removes other nodes’ replies. With force, cascades to replies, reacts, and read receipts (the schema has no ON DELETE CASCADE).

The cascade is best-effort, not atomic across connections: each db.delete opens 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 force is 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; read is 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 by created_at DESC instead of priority DESC.

  • self (Radio)

Return type:

list[dict[str, Any]]

Returns:

List of message dicts with replies, pos_reacts, and neg_reacts counts.

sent(*, channel=None, limit=None, since=None, recent=False)[source]#

Query messages this node sent, across every host’s channel-space.

Each row’s node column 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 by created_at DESC instead of priority DESC.

  • self (Radio)

Return type:

list[dict[str, Any]]

Returns:

List of message dicts with replies, pos_reacts, and neg_reacts counts.

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’s node column 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 by created_at DESC instead of priority 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 reads table – 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 by created_at ASC with depth field 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 depth field.

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. thread walks 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 (like react) 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.write use separate connections, so a concurrent channel_delete or unsend of 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 way send does.

Raises:
  • ValueError – If the parent message is not found, or priority is 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 (a reads receipt), mirroring read, so an ack clears it from SYNC.

Parameters:
  • message_uuid (str) – 8-char message UUID.

  • value (int) – Reaction value (1 or -1).

  • self (Radio)

Raises:
  • ValueError – If value is not 1 or -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 archive table – 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 (the owner column).

  • 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 by created_at DESC instead of priority 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 (use channel_delete then 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 force is set, since the cascade also removes other nodes’ replies (mirroring unsend). With force, cascades to the channel’s messages, reacts, and read receipts (the schema has no ON 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.delete opens 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 force is 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 channel is 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 channel is 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.

subs()[source]#

List this node’s subscriptions.

Parameters:

self (Radio)

Return type:

list[dict[str, Any]]

fractal.core.record module#

Implements Record class.

class fractal.core.record.Record(node)[source]#

Bases: object

Row-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.

Parameters:

Initialize Record.

Parameters:
  • node (Node) – The owning Node instance.

  • self (Record)

property node: Node#

Return the owning node. :type self: Record :param self:

property db: Database#

Return the central database. :type self: Record :param self:

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_id is the latest active run, else the most recent run (the run container persists across iterations) – unless active, which returns None for run_id when no run is active (for events that do not belong to the node’s own run). step_id/iter_id are the in-flight (active) step/iteration, or None when nothing is running – a row only pins to something currently active. All None if 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 (requires run_id).

  • step_id (int | None) – Explicit step id (requires iter_id and run_id).

  • active (bool) – Resolve run_id active-only (no most-recent fallback).

  • self (Record)

Return type:

tuple[int | None, int | None, int | None]

Returns:

(step_id, iter_id, run_id), each None where not applicable.

Raises:

ValueError – When the explicit lineage does not chain (step_id without iter_id/run_id, or iter_id without run_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_at on 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 by run_start(), Loop._on_exit(), Node._reconcile_status(), and Node._mark_active_killed() so the cascade has one definition and the DB can never diverge from the .status file. 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 unpriced metadata marker, mirroring step_end().

With skip_event – the kill cascade – any still-active event is closed by status too (events have no ended_at), skipping the in-flight kill event itself (Node._kill finalizes that one via event_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); None leaves metadata untouched.

  • connection (Connection | None) – Transaction to run the row cascade inside (from Database.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 because start.sh refuses 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, and ended_at only 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 from started_at/ended_at; cost rolls up from steps – 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 in node activity; the metadata column is left untouched when None.

  • 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_id and resume_step are None at a boundary, iter is None when the run has no iterations – or None when 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 branch so 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 None when 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 NULL guard. Duration is derived from started_at/ended_at and cost rolls up from steps – 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 in node activity; the metadata column is left untouched when None.

  • 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. claude or codex).

  • 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), or None when 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 NULL guard, so a kill racing the loop’s own end can’t overwrite the outcome. Duration is derived from started_at/ended_at; the cost column is left untouched (recorded separately by step_cost(), possibly after the step has ended).

An abnormal end (any terminal but completed) whose row has a session but no cost appends unpriced to 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.unpriced discloses 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 in node activity; the metadata column is left untouched when None.

  • 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 approved column 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 pending forever, 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 approved to the current UTC timestamp).

The low-level write; the step is validated (exists and requires approval) by the sole caller Node.child_approve before 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 True if the step does not require approval (approved is NULL) or has been approved (approved is a timestamp). Returns False only when the step requires approval but has not been approved yet (approved is 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_id is the active run (NULL when none is active – an event carries a run only if it fired inside one), and step_id/iter_id are the in-flight step/iteration, so a kill names the interrupted step. Every row stamps its actor: 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 of fractal.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 None if the node is not initialized.

Raises:

ValueErrorevent is not a known type, or the explicit lineage does not chain (step_id without iter_id/ run_id, or iter_id without run_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 final status and optional exit_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 None if not set.

Auto-resolves run_id from 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_id from 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 the pause rows 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 activity view unifies entity start/end rows (with derived duration and rolled-up cost on 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-relative step number, step_name, and the run-relative iter.

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 $VAR placeholders into text.

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]) – A name -> value map 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 (overrides win).

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 with N/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 -> value map 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.md charter, 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>.md activates when <NAME>_MODE is true in that map: static modes (DETACHED/META) derive from config, run-scoped modes (CONTINUE/RESUME/RESERVE) ride in as overrides. SYNC.md never 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 (see variables()).

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.md mode; for a fresh chat it is preceded by the node’s NODE.md charter. 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’s NODE.md charter (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 text with 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: object

Per-iteration agent->session-id map plus transcript resolution.

Parameters:

Initialize Sessions.

Parameters:
property node: Node#

Return the owning node. :type self: Sessions :param self:

get(agent)[source]#

Read an agent’s session for the current iteration.

The .session map 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 None if 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 (a session value off a step or iteration row).

  • self (Sessions)

Return type:

dict[str, Any]

Returns:

{agent, session, path, exists, content}content is the raw JSONL text (empty when absent).

Raises:

ValueError – If agent is unsupported, or session is 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: object

Deadline accounting over config timeouts and row instants.

Parameters:

Initialize Time.

Parameters:
  • node (Node) – The owning Node instance.

  • self (Time)

property node: Node#

Return the owning node. :type self: Time :param self:

property db: Database#

Return the central database. :type self: Time :param self:

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 how Cost.remaining reads persisted state), so it works for any node, not only the running one. The run scope (--timeout) is anchored on the active run’s started_at; the iter scope (--iter-timeout) on its active iteration’s; the step scope (--step-timeout) on its active step’s. With no scope the 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), or None if 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 (run is timeout).

Pure config – no row reads – so it answers for an idle node too: the run scope reads the whole-run timeout key, iter/ step their per-level timeouts; an unset iter_timeout falls back to interval (the loop bounds an interval iteration by its slot). With no scope the 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 None when 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 .worktrees flock for a critical section.

Serializes worktree add/remove and every cap gate – git worktree add is not parallel-safe; an fcntl.flock is 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; None checks 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 init additionally 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 path when it has no born branch yet.

fractal init anchors the user node at the git root, so the target must be a git repo whose branch is born (_init_user resolves self.branch with check=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 .git with 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 .project cache.

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 other info/exclude content is preserved.

Idempotent and concurrency-safe: the common-dir info/exclude is shared by every worktree, so sibling init/start fan-out races on it. The rewrite computes the new content from a clean read and commits it with an atomic unique-temp os.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 track opts the top-level branch back in. None carries 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/untrack toggles – so every block rewrite preserves the current choice. The probe is scoped to the block itself, never git check-ignore: check-ignore also matches a user .gitignore entry, which would make fractal track silently 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:

True when a fractal block exists without the seed-dir ignore line; False when 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). name is the validated display name; the wiki is seeded with the strict ASCII-identifier naming policy. A failed wiki init is 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 against name to note a derived-name adjustment).

  • path (str) – Project path (. for the repo root).

  • name (str) – Validated wiki display name.

Return type:

bool

Returns:

True if the wiki was created, False if 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-wiki wired in, or a .fractal mention (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 installed fractal package.

  • 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 .project cache 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.sh cannot 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.