Agent Runtime Architecture
Session execution, checkpoints, message persistence, security boundaries, and cross-platform behavior
The runtime is designed to execute every input in a deterministic order, expose intermediate model and Tool state, and recover Sessions after process interruption.
Session is the execution boundary
Each Session owns independent:
- ID, metadata, and model override.
- Message history and System Snapshot.
- Prompt/Command queue.
- Tool approval state.
- Composer and Executor.
- Live Mutation stream.
// Session 在创建时选择 Workspace。
const session = await agent.sessions.create({ workspace });
const turn = await session.prompt({ query: "Inspect and fix the project" });
const result = await turn.finished;prompt() returns a Turn Handle instead of direct text. finished resolves for both success and failure; inspect result.success and result.error for the outcome.
How one Prompt runs
The runtime guarantees:
- The User Message is persisted before the model call.
- Streaming Assistant output is first written as a recoverable draft.
- Text, Reasoning, Tool Calls, and Tool Results preserve canonical model-stream order.
- The Assistant Message enters Active history only after completion.
- An interrupted run can recover from the draft and Action state.
State checkpoints
Only one execution loop is active per Session. Every input that can affect the next Step enters one ordered queue:
- User Prompts.
- Workspace environment updates.
- Power registry updates.
- Session model updates.
- Explicit compaction.
Changes are committed at Step boundaries, never halfway through a model call or Tool execution. An active Step uses the stable execution view created for it.
Model resolution order is:
Session model > Agent modelAgent and Session hold model instances supplied by the host. The SDK does not select or restore model services from a string model ID.
Composer and Executor
SessionComposer owns model-context policy: combining system blocks, translating history, supplying Tools, creating compaction plans, and identifying context-limit errors.
Executor owns one run: preventing concurrent execution within a Session, creating the run context, calling Composer, driving the model and Tool Loop, and handling aborts, retries, and context-limit recovery.
Neither object knows the physical Session storage path, the Agent list, or HTTP/RPC transports.
System Snapshot
A Session supports three System semantics:
- Default: use the current Agent identity, Agent instruction, Power system, and Session context at execution time.
snapshot(): persist the current complete system for this Session.syncshot(): regenerate from current Agent state and replace an existing snapshot.
system() returns the effective structured System Snapshot. Updating the Agent instruction does not unconditionally overwrite a Session that has already been snapshotted.
Messages and persistence
Local state lives under the Agent-specific Workspace scope in the user-level Downcity directory:
~/.downcity/agents/<agent-id>/
├─ sessions/<session-id>/
│ ├─ instruction.md
│ ├─ meta.json
│ └─ messages/
│ ├─ active.jsonl
│ ├─ agent_message.json
│ └─ segments/*.jsonl
├─ archived-sessions/
├─ logs/
├─ schedule.jsonl
└─ sandbox/active.jsonlstores history since the last compaction.agent_message.jsonstores the latest complete streaming Assistant draft.segmentsstores immutable compacted history prefixes.instruction.mdstores an explicitly persisted System Snapshot.
Messages use monotonically increasing sequence values; streaming updates to the same Message use increasing revision values. JSON files are atomically replaced with a temporary file, fsync, and rename; cross-process writes are serialized through Workspace locks.
Hosts should read structured state through session.messages(), get_info(), and system() instead of depending on these physical file names.
Live observation
subscribe() emits Mutations that occur after subscription:
const unsubscribe = session.subscribe((mutation) => {
if (mutation.variant === "delta" && mutation.type === "text") {
process.stdout.write(mutation.delta);
}
});Mutations cover complete Messages, Assistant Parts, text/reasoning deltas, Turn state, and Session state. After a disconnect, reload the canonical snapshot with messages() before establishing a new subscription.
Power runtime
Powers belong to City, not Agent or Workspace. City owns one instance per Power ID and projects its Actions, System text, Hooks, Resolve Points, and HTTP routes into every Agent/Workspace execution entry.
PowerRegistry projection changes take effect at Session checkpoints. A Power initialization failure isolates that Power without blocking others. Agent disposal stops Sessions and ActionSchedule; City closes the Power initialize/dispose lifecycle.
ActionSchedule is a local delayed Power Action scheduler owned by the Agent. Events are stored in the Agent's storage scope; after restart, abandoned running entries recover to pending. It is not a distributed scheduler and provides no cross-machine leader election.
City Powers
Every Agent receives three City-owned powers. They are the single entry point for "what only the harness knows" plus the generation models City itself holds.
{ "action": "sandbox.explain_path", "args": { "path": "~/.downcity/powers/messenger" } }| Power | Action groups | Answers |
|---|---|---|
context | env, sandbox, workspaces, usage | Which Agent, Session, Workspace, model, timezone and local date this turn runs under; which sandbox backend and instance this Session runs in, which host directories are mounted, and why a path is blocked; every Workspace registered in this City; user-level token usage |
ai | image, sound | Generate images; transcribe audio and synthesize speech |
agent | agent | Every Agent registered in this City |
An action id is group.action, independent of which power owns it: env.get belongs to context, and both image.* and sound.* belong to ai. Calling a power with no arguments lists its actions; nothing here returns credential values, and each action declares read or write so you can see which ones consume quota or change state before calling them.
Availability follows the same rule as Powers: everything City registers is available to every Agent, with no per-Agent gating and no configuration file.
Image and sound
Both use the models available to your Federation account, so there is no separate configuration and nothing to set up. Because there is no configured default, pass model explicitly or take it from image.models / sound.models; asr and tts fall back to the first suitable model when none is given.
Image generation is a two-step job: create returns a job_id, result reads it. Creating an image consumes quota, so the model must confirm with you first. result writes the generated images into the power's private directory and returns their local paths; render them with .
Automatic transcription of inbound voice messages belongs to the Messenger power, which calls the ai power's sound.asr when a channel delivers a voice or audio attachment. If transcription fails for one attachment, the message still arrives.
Security model
Security is enforced at distinct capability boundaries:
| Capability | Enforced boundary | Permission semantics |
|---|---|---|
| File/Search Tools | Rooted LocalFileSystem | May actively access only the Workspace |
| Sandbox Shell | Workspace Sandbox | Commands run in the Workspace's isolated, persistent environment |
| Host Shell | Approval gateway | The user explicitly authorizes one host command with a recorded reason |
| Power | Power business authorization | Workspace does not own all Power resource access |
| City Power | Declared per action | Answers runtime facts and runs declared read actions; write actions consume quota or write files under the Agent's directory |
| Custom Tool | Registering host | Agent Core does not infer business permissions |
Explicit attachments may use an absolute path selected by the host because they are declared Prompt inputs. This does not expand where model-driven File/Search Tools may browse.
Cross-platform strategy
Node.js already normalizes files, paths, networking, JSON, timers, AbortController, and basic child processes. Agent, Session, Store, and Workspace therefore contain no operating-system branches.
Isolation differences are contained by the neutral Sandbox Provider protocol. Each Shell receives its Provider explicitly and owns the isolated, persistent Sandbox created when it binds a Workspace. City only manages Workspace resources and does not know whether their Shell uses a local container, microVM, remote host, or another backend. The default implementation uses microsandbox microVMs.
Lifecycle
Sessions wait for the Agent's City execution binding before execution. agent.dispose() does not stop City Power instances. city.close() closes Transports, Agents, Powers, Workspaces, and Shells after active Hook/Action leases finish.
Continue with Agent Lifecycle.