Runtime Control Plane v2
This document records the implementation contract for the breaking runtime
refactor. The host owns the harness. Agent Cores own authored files under
agent/.
Module Shape
The new deep Modules are:
RuntimeStore: SQLite event store and projection surface.RuntimeControlPlane: host-owned detached task seam.DurableWorkRuntime: lease/ack/terminal-state seam for unfinished host work.HostWorkLifecycleRuntime: unified lifecycle and observation seam for durable work, detached tasks, delivery, schedule fires, and task-completion notifications.SessionRuntime: session admission and session/turn/message projections.TurnEngine: foreground provider/tool loop for one Agent Core turn.SlotRuntime: phase-specific authored slot callable execution.
The detached-work task ledger model is:
TaskSpec -> Task -> Event -> Projection
Detached host work that is observable as a task enters through
RuntimeControlPlane.submit_task(). The current task-ledger kinds are
agent.spawn, terminal.exec, evolver.run, and schedule.fire. Foreground
Agent Core turns do not become task rows; they are projected through
SessionRuntime as turns and messages. Delivery, approval, tool-call, MCP,
state, and artifact facts should use their own projections or runtime events
rather than fake task submissions.
Storage
The runtime database is ~/.demiurge/runtime/runtime.sqlite3. It uses Python
stdlib sqlite3 with WAL mode. Old JSON/JSONL session, scheduler, and
background-task state is not migrated.
Unfinished host work is projected into runtime_work_items. This table is the
durable lease surface for work that has been created but has not reached a
terminal state, including delivery sends, schedule fires, and background-task
completion notifications. Callers should use DurableWorkRuntime instead of
writing ad hoc queued, running, sending, sent, or acknowledged state.
Expired running or claimed work can be reclaimed by a new claim token.
Expired sending work is marked unknown; the host must not blindly replay an
external send after a crash.
Runtime modules that need a cross-subsystem view use
HostWorkLifecycleRuntime. It wraps DurableWorkRuntime behind domain
methods for delivery sends, schedule fires, and task-completion notifications,
and it projects operator-readable status from runtime_work_items, tasks,
task_logs, outbox, scheduler_instances, and task-completion events. This
is an observation and lifecycle facade, not a replacement for the specialized
owners:
RuntimeTaskWorkerstill owns active process handles, live task objects, cancel callbacks, wait, and live completion subscribers.DeliveryRuntimestill owns channel dispatch, item dispatch status, message failure updates, and delivery event-log records.SchedulerRuntimestill owns cron calculation, due-instance records, and fresh scheduled sessions.DurableWorkRuntimestill owns the low-level claim token state machine.
Foreground Agent Core turns are not host work items and must not be promoted into task ids. Memory review, background review, curator behavior, and learning loops are also not harness modules; those capabilities belong in Agent Slot-driven packages using host-mediated tools and capabilities.
Agent Slot Layout
Each bootstrap, input, and output slot owns a slot.yaml manifest in its slot
directory. agent/pipelines.yaml is the single phase ordering graph:
schema_version: 1
bootstrap:
serial: []
input:
serial: [base_input]
parallel: []
output:
serial: [base_output]
parallel: []
Slot code and metadata stay in typed folders:
agent/bootstrap/<slot_id>/module.py
agent/bootstrap/<slot_id>/slot.yaml
agent/input/<slot_id>/module.py
agent/input/<slot_id>/slot.yaml
agent/output/<slot_id>/module.py
agent/output/<slot_id>/slot.yaml
base_input and base_output are ordinary editable seed slots. The host does
not treat them as built-ins and the loader does not require those ids.
Input slots build the current model context. ctx.input.raw_text is read-only.
Slots use ctx.input.add_context(text, role="user"|"system", write_history=...). Output slots read ctx.output.response_text and use
ctx.output.send_*. The author-facing delivery timing parameter is removed:
every send records a delivery intent immediately.
Serial slots can affect the main flow. Parallel slots are non-blocking background side-effect lanes and cannot modify prompt, assistant response, or session history.
Current Implementation Slice
The runtime store is now the hot-path source of truth for sessions, turns,
messages, foreground tool-call records, task status, task logs, scheduler
instances, artifacts, delivery outbox rows, runtime work items, and unique
channel conversation bindings. Foreground tool-call records are keyed by the
current turn_id and model-loop step_id; they are not task facts. Old JSON
session and scheduler files may still exist on disk from older installs, but
runtime code does not read, migrate, or dual-write them.
RuntimeTaskWorker is the live worker for active subprocess, terminal,
evolver, and child-agent work. It keeps only non-durable process handles,
cancel callbacks, and live completion subscribers in memory. Public task reads,
lists, logs, waits, cancellation results, and pending completion notifications
are rebuilt from RuntimeControlPlane / SQLite projections and runtime events.
Task-completion claim and acknowledgement flow through
HostWorkLifecycleRuntime, so bridges and operator surfaces share the same
claim/ack vocabulary.
Terminal task handles carry a Host-issued process spawn_id plus an OS
process-start marker; durable metadata contains PID/process-group/platform
identity but is never used alone to kill a process. Concurrent cancellation is
single-flight: one cleanup callback commits return code/exit reason in one
terminal event before completion-ready, and all callers observe that result.
DemiurgeApp.close() cancels and drains active runtime tasks and registered
foreground terminal owners before tool resources close. Task admission closes
before shutdown snapshots active work, so a late approval cannot start a new
background process during drain. Output-persistence
failure follows the same tree-cleanup path before task.failed is committed.
Full terminal streams are durable artifact rows/files; task logs and model
results remain bounded projections.
BackgroundWorkRuntime tracks in-process background coroutines created by
parallel slots and delivery dispatch. It composes those local tasks with the
durable RuntimeTaskWorker for drain and active-count behavior; the foreground
runner does not own a separate background-task ledger.
OperatorGatewayRuntime owns the local operator product surface for TUI and
future dashboard clients. It projects operator.ready, operator.status,
operator.history, operator.work.updated, operator.prompt.opened,
operator.approval.opened, operator.message, operator.deliver,
operator.error, and operator.shutdown events from the runtime store, session
runtime, conversation lifecycle, approval runtime, and
HostWorkLifecycleRuntime. The NDJSON entrypoint is only transport plumbing
over this operator module; messaging channels remain separate platform
adapters. Operator clients do not receive legacy interaction.* or
channel.* compatibility frames.
DeliveryRuntime dispatches queued delivery intents through the
session-scoped interaction router after claiming the matching durable work item.
The outbox lifecycle is queued -> sending -> sent/failed/unknown/unrouted.
unrouted means no live route is bound for the delivery session; failed means
a route existed and adapter delivery raised. Delivery failure can update a
previously persisted history row with explicit failure history text, but retries
must not rewrite the original history body.
SessionTurnStepRunner now delegates:
- session creation, update, turn lifecycle, and message persistence to
SessionRuntime; - foreground turn admission, including session/core resolution, route binding,
revision/capability pinning, and turn begin, to
TurnAdmissionRuntime; - authored input -> model/tool -> output execution, captured-route context,
bootstrap, owner-checked cancellation, delivery drain, and final cleanup to
TurnExecution; - foreground input records, assistant output records, display turns,
completion, and interruption to
TurnPersistenceRuntime; - provider/tool loop execution to
TurnEngine; - authored bootstrap/input/output slot callable loading and invocation to
SlotRuntime.
The model-facing delegation tools are:
delegate_task(goal, core_id=None, context_mode="isolated", notify_policy="return_to_parent", max_depth=None, tools="all", input_slots=["base_input"], output_slots=["base_output"], use_bootstrap=False);task_list(kind=None), scoped to the current session;task_status(task_id, view="model");task_control(task_id, command="cancel");yield_until(task_id, timeout_seconds=30).
delegate_task currently supports isolated and fork context modes, enforces
the default depth and child-count limits, and applies child tools selection
during visible-tool construction and dispatch. notify_policy accepts only
return_to_parent and silent; the former emits a completion event and the
latter suppresses it. Child output is evidence for the parent by default.
Child input/output selection defaults to base_input and base_output;
"all" runs the child core's full configured pipeline, and a list filters the
active pipeline while preserving order and serial/parallel groups. Bootstrap is
off by default for delegated child turns.
Foreground turns are not readable as task ids through the control-plane
projection. They remain traceable through the session, turn, message, event-log,
and runtime-event projections. Model-facing task tools only operate on detached
background task kinds, so ordinary turns neither appear in task_list nor
support task_status, task_control, or yield_until.