Skip to content

Instantly share code, notes, and snippets.

@savarin
Last active June 27, 2026 22:01
Show Gist options
  • Select an option

  • Save savarin/b05d77ef56b219a8664cb3f846f2b822 to your computer and use it in GitHub Desktop.

Select an option

Save savarin/b05d77ef56b219a8664cb3f846f2b822 to your computer and use it in GitHub Desktop.
The Rewrite: Python Edition — Chapter 4 — The Single Construction Seam

← Back to Index

Chapter 4 — The Single Construction Seam

The construction seam is the one place in a system where all the abstractions are real simultaneously. Every seam you declared in core/contracts.ts exists as a type; the construction root is where they become objects, wired to each other in the only order the dependency graph allows. If you can hold the construction root in your head, you can hold the whole system in your head. This chapter looks at how v1 and v2 construct themselves — what each wires, what each exposes, and why v2's construction root has sharper separation between wiring and runtime even as it adds new explicit seams.

What a Construction Root Is

A construction root is a function (or class, or module) that has one job: instantiate every component and wire the dependencies. It is the only place in the system where imports touch implementations rather than interfaces. Outside the construction root, a module that needs a SandboxApi receives one — it never constructs one. Inside the construction root, you call new LocalSandbox(). The discipline matters because it means that every other module is testable with a stub: you pass a different implementation at construction time and the rest of the system does not know or care.

This is dependency injection without a framework: the function signature is the injection point. The v2 header makes this explicit: "Every dependency is injectable so a test can drive it with no network (inject model + a scripted stream_fn)." The defaulting pattern reinforces it: create_runtime() with no arguments yields a locally drivable runtime; create_runtime(store=test_store, model=stub_model) yields a fully instrumented one. The construction seam is also the testing seam.

Both v1 and v2 call this function create_runtime. Both return a Runtime object that is the entry point for everything that happens at runtime. The differences are in what Runtime exposes and what create_runtime wires — and those differences reveal a design evolution.

v1: A Runtime That Owns Its Sessions

v1's harness.ts is described in its header as playing "the same role for Cy" as flue's own Harness: it "wires the REAL component modules into a drivable Runtime." The wiring is:

v1 create_runtime wires:
  AgentExecutionStore       ← in-memory by default, injectable
  ModelProvider             ← resolve_model / resolve_api_key / stream_fn
  SandboxApi                ← LocalSandbox by default, injectable
  SessionEnv                ← cwd-scoped view of the sandbox per session
  PersonDirectory           ← in-memory stub by default
  ObservableRootSink        ← the event bus (root fan-out + subscribeEvents)
  ChatRuntime               ← the adapter registry (Slack / CLI / web)
  Session factory           ← get_session(key) → cached durable Session

The Runtime handle v1 returns carries those subsystems as fields and provides lifecycle methods: initialize() and shutdown() for the chat adapters, get_session() and drop_session() for the session map. The runtime is a long-lived object that manages all active sessions in an internal map.

Notice what that means: the session map is inside the runtime. When a new message arrives, the router calls runtime.get_session(key) and gets back a Session — constructed on the first call, retrieved from the internal map on subsequent calls. This is simple and it works, but it binds two concerns together: the runtime is both "where components are wired" and "where sessions are tracked."

v1 Runtime interface (selected fields):
  tenant_id: str
  store: AgentExecutionStore
  model: Model[Api]
  sandbox: SandboxApi
  session_env: SessionEnv
  tools: tuple[AgentTool, ...]
  model_provider: ModelProvider
  sink: ObservableRootSink       ← the event bus (v2 drops this)
  get_session(key, on_event=None) ← builds or retrieves a cached Session
  drop_session(key): None
  initialize(): Coroutine[None]   ← starts chat adapters
  shutdown(): Coroutine[None]     ← stops chat adapters

The runtime has eight concerns. Some — initialize, shutdown, get_session, drop_session — are session-lifecycle and adapter operations. Others — store, model, sandbox — are infrastructure the sessions use. They coexist in the same object because the v1 design did not yet distinguish "the wired component graph" from "the thing that drives conversations."

The consequence is coupling at the boundary: to test a session's behavior, you need a runtime, which means you need adapters initialized. To test adapter initialization, you need a session factory. The construction root is entangled with the operational lifecycle.

v2: A Runtime That Exposes a Coordinator

v2's runtime.ts is described in its header as "the minimal analog of v1's harness.ts." What is dropped: "the chat-adapter registry, the people directory, the event bus / sink fan-out + subscribeEvents, per-session model/credential override complexity."

What v2 adds: Coordinator, WorkflowRegistry, ToolRegistry, and SessionFactory — four primitives that did not exist as explicit seams in v1. Here is the actual dependency graph from the construction root:

Runtime  ← create_runtime(config)
 ├─ AgentExecutionStore                     durable { sessions, submissions }
 ├─ ModelProvider → Model + StreamFn        the LLM seam (one shared default model)
 ├─ Coordinator(store.submissions)          drives a Session's turn FSM; resume-with-repair
 ├─ WorkflowRegistry                        registered workflows → the `workflow` tool
 ├─ ToolRegistry                            name → ToolFactory (a session selects tools by name)
 ├─ sandbox_resolver(tenant_id) → SandboxApi  ONE sandbox backend per tenant
 └─ SessionFactory(all of the above)
       └─ Session = f(SessionContext, SessionSpec)
            • tools: list[AgentTool] = ToolRegistry.build(spec.tools, ToolContext)

The key structural change isn't that get_session disappears — it's that the session cache moves out of the Runtime and into SessionFactory. get_session survives as a thin delegate to factory.get_or_create_root; the factory is the seam, the runtime method is just the edge that calls it. Every session origin — a human prompt to the root, a workflow dispatching a node, a subagent being delegated to — flows through SessionFactory. There is one path for session construction, not three separate call sites.

Here is the v2 Runtime interface:

# apps/runtime/src/runtime.ts
from typing import Protocol
from collections.abc import Callable

class Runtime(Protocol):
    tenant_id: str
    store: AgentExecutionStore
    model: Model[Api]
    sandbox: SandboxApi
    session_env: SessionEnv
    tools: tuple[AgentTool, ...]
    model_provider: ModelProvider
    coordinator: Coordinator      # new in v2; drives all turns
    registry: WorkflowRegistry   # new in v2; registered workflow defs
    def session_key_for(self, thread_id: str) -> str: ...
    def create_workflow_executor(self, storage_key: str, depth: int = 0) -> WorkflowExecutor: ...
    async def drain_runnable(self) -> None: ...
    async def resume_pending(self) -> None: ...
    async def get_session(self, session_key: str, on_event: Callable[[RunEvent], None] | None = None) -> Session: ...
    def drop_session(self, session_key: str) -> None: ...

get_session still exists on v2's Runtime, but it is a thin wrapper over factory.get_or_create_root — not the session-map logic it was in v1. The session cache lives inside SessionFactory, not inside the runtime. The runtime is a component graph that delegates session operations to the factory.

No initialize / shutdown for adapters. The chat adapter is not part of the construction root — it is passed in by the router at the point where a session is started. This means you can construct a Runtime, run create_runtime(), and drive a session from a test without touching any platform adapter code.

The SessionFactory: The Single Construction Seam

The construction code in runtime.ts names SessionFactory explicitly as the system's single construction seam:

# apps/runtime/src/runtime.ts
# The GLOBAL session factory — the SINGLE construction seam for every session origin
# (a root chat thread, a workflow anchor/node, a dispatched subagent). It owns the
# per-process session cache, the context-bound toolset (tools are FACTORIES run over a
# ToolContext), lineage (deriving + persisting a child's SessionContext), and
# resume-with-repair on a live root build.
factory = SessionFactory(
    store=store,
    stream_fn=stream_fn,
    system_prompt=system_prompt,
    model=default_model,
    get_api_key=get_api_key,
    sandbox_resolver=sandbox_resolver,
    sandbox_root=sandbox_root,
    registry=registry,
    coordinator=coordinator,
    tenant_id=tenant_id,
    tool_registry=tool_registry,
)

Everything the factory needs is injected at construction time: the store, the stream function, the model, the key resolver, the sandbox resolver, the workflow registry, the Coordinator, the tenant, and the tool registry. The SessionFactory is the point where all the pieces converge. After this line, every new session is a function call on factory, not a fresh wiring exercise.

In v1, session construction was implicit: get_session(key) built a Session inline, reaching directly into the runtime's own model, sandbox, and tools. The construction logic was repeated at every call site. In v2, construction is explicit and named — the SessionFactory is the seam.

The layered architecture makes this visible:

EDGE       Chat (Web | Slack | Discord)      inbound → session key
RUNTIME    create_runtime(config) → Runtime   component graph
CONSTRUCT  SessionFactory                    ← the single construction seam
EXECUTE    Coordinator                       drives submissions to settlement
STATE      AgentExecutionStore               sessions + submissions
MODEL      ModelProvider                     LLM seam
TOOLS      execute_code · workflow · ...

SessionFactory sits between the runtime (which wires the graph) and the executor (which drives turns). It is the seam where "I have a component graph" becomes "I have a session."

The ToolRegistry: Tools Are Factories, Not Values

One subtle design in v2 that the construction root makes visible: tools are not static values. In v1, the toolset was built once per runtime from a profile and a context. In v2, tools are factories registered under names. A session's spec.tools is a list of names; the ToolRegistry builds the actual list[AgentTool] from those names at construction time, passing a ToolContext to each factory.

# apps/runtime/src/runtime.ts
tool_registry = create_default_tool_registry(
    include_workflow=len(registry.names()) > 0
)
for tool in config.tools or []:
    tool_registry.register(tool.name, lambda t=tool: t)  # static tool → context-free factory
for name, factory_fn in (config.tool_factories or {}).items():
    tool_registry.register(name, factory_fn)              # dynamic tool → context-bound factory

This matters for context binding. An execute_code tool closes over ctx.sandbox — it needs to know which sandbox this session uses. A post_message tool closes over ctx.chat — it needs the platform binding. Static tools (those that don't depend on context) get wrapped as context-free factories. Context-bound tools get registered as proper (ToolContext) => AgentTool functions.

The consequence: every session gets a toolset assembled from its own context. If two sessions run with different sandbox backends, their execute_code tools point at different sandboxes. The ToolRegistry enforces this without any session needing to know about the others.

The Session Key: How Identity Flows Through Construction

One more piece the construction root owns: the session key derivation. Every durable session has a storage key — the stable address in AgentExecutionStore where its transcript lives. The v2 runtime exposes:

def session_key_for(self, thread_id: str) -> str:
    return f"{self.tenant_id}:{DEFAULT_HARNESS}:{thread_id}"

These are two levels: session_key_for derives the session name from the inbound thread id; create_session_storage_key wraps a session name into the durable storage key. The name feeds the key — it is not a wrapper of it:

# apps/runtime/src/durable/session-identity.ts
import json

SESSION_STORAGE_PREFIX = 'agent-session:'

def create_session_storage_key(
    instance_id: str,
    harness: str,
    session: str,
) -> str:
    return f"{SESSION_STORAGE_PREFIX}{json.dumps([instance_id, harness, session])}"
# yields: 'agent-session:["tenantId","default","threadId"]'

The key format is part of the durable contract — the store tests pin the exact string. The runtime owns the mapping from the inbound identity (a thread ID from the chat platform) to the durable address (the storage key). This derivation happens once per session, at construction time, and the same derivation function works for root sessions, child task sessions, and workflow anchor sessions:

def create_task_session_name(parent_session: str, task_id: str) -> str:
    return f"task:{parent_session}:{task_id}"
# child key: 'agent-session:["tenantId","default","task:threadId:task123"]'

The task: prefix is reserved — assertPublicSessionName rejects any attempt to create a public session with that prefix. This structural enforcement is part of what makes the construction seam trustworthy: the wrong identity is unrepresentable, not just discouraged.

Why Simplicity at Construction Time Matters

A construction root that does too much is a design smell. If create_runtime also manages chat adapter lifecycle (initialize, shutdown), that is two responsibilities in one function. The consequence is subtle: it becomes harder to test runtime construction separately from adapter initialization, and it becomes harder to see what the runtime actually depends on.

v2 separates these concerns precisely. create_runtime wires the component graph and returns it. The chat adapter is not part of that graph — it is passed in by the router at the point where a session is started. This means you can construct a Runtime without any chat adapter and drive it directly from a test. The construction seam is also the testing seam.

The practical payoff appears in the test patterns: a test that wants to drive a session with a fake stream function calls create_runtime(stream_fn=fake_stream_fn). A test that wants to inspect the durable store calls create_runtime(store=test_store). No adapter infrastructure, no event bus setup, no platform-specific initialization. The construction function's optional parameters are the injection points, and every one of them defaults to something useful.

The Invariant the Single Seam Enforces

Once you have one construction seam for every session origin, you get an invariant for free: every session is built the same way. That means:

  1. Every session gets the same tools (the ToolRegistry decides which are available; the session spec selects from that set by name).
  2. Every session's durability works the same way (all state is in AgentExecutionStore; none is in process memory outside the factory's session cache).
  3. Every session can be resumed by a fresh process (the Coordinator reconstructs it from the store; the SessionFactory re-hydrates the transcript).

In v1, some of these invariants held for root sessions but not for subagents, which were constructed inline inside the RunAgent primitive with a narrowed context but not via the same factory path as root sessions. v2 makes them hold everywhere, because "everywhere" now means "one path through SessionFactory."

The resume_pending method on Runtime shows this in action:

async def resume_pending(self) -> None:
    await register_pending_anchor_runners()
    await coordinator.resume_pending(lambda key: factory.resolve(key))

When a process restarts, resume_pending asks the Coordinator to resume every unsettled submission. For each one, it calls factory.resolve(key) — the same factory that builds new sessions builds resumed ones. The resumed session is indistinguishable from a newly constructed one; the construction seam is also the recovery seam.


You now understand what a construction root is, how v1 and v2 differ in what they wire and expose, and why SessionFactory as a single construction seam makes the system's invariants hold uniformly. The contracts from Chapter 3 are the vocabulary; the construction root is where that vocabulary becomes a running system. The next chapter descends into the turn itself: what happens between "the prompt arrives" and "the result is posted."

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment