Hosted Runtime¶
This document is the implementation-focused companion to architecture.md.
It answers a narrower question:
What is actually implemented today for TerraFin's hosted runtime, and where do the important seams live?
Reference Boundary
TerraFin's transcript-first session persistence follows the same core shape
used by OpenClaw and Claude Code: append-only per-session transcripts with a
separate session index and explicit rewrite paths. TerraFin's runtime
controller, financial capability layer, task/approval flow, widget, and API
integration remain TerraFin-specific. The orchestrator-agent-with-persona-
subagents pattern also takes inspiration from the role-separation style in
ai-hedge-fund, but TerraFin keeps shared capabilities and prompt-level
persona policy instead of hardcoded per-guru analysis modules — see the
diagrams in architecture.md
for the authoritative shape.
Current runtime shape¶
Today the hosted runtime has:
- a shared financial capability layer
- a hosted agent definition registry
- a policy-enforcing runtime controller
- a hosted tool adapter
- a provider-agnostic hosted loop
- a provider registry with OpenAI and Gemini adapters
- Python, CLI, HTTP, notebook, and browser widget adapters
- transcript-first local session history
- hidden persona subagents (Buffett / Marks / Druckenmiller) reached by
the main orchestrator agent via
consult_<persona>tool-calls (see the architecture diagrams in architecture.md) - a structured internal tool-result/error protocol
- transcript normalization and repair before model calls
- a proactive context-budget manager with reactive fallback retries
Transcript-first persistence¶
Conversation history is no longer stored inside the hosted session record.
Instead TerraFin now splits local hosted state into two layers:
- transcript JSONL files: durable source of truth for message history
- session index JSON: summary metadata for history/list/delete behavior
Default layout under the unified TerraFin state dir:
Transcript events are append-only and currently include:
session_headermessageruntime_modelcustom_titlecompact_boundary
session_header carries agentName, plus origin and parentSessionId when
the creator supplied them in session metadata:
origin |
Who created the session |
|---|---|
dashboard |
a person typing in the browser widget |
pipeline |
a batch stage calling the agent as an LLM |
guru |
a hidden worker spawned by a turn; also carries parentSessionId |
The names live in agent/storage/transcript_store.py (SESSION_ORIGIN_*).
An absent origin means unknown, which covers the CLI and every transcript
written before the field existed; an unrecognised one is recorded and logged at
warning level. A corpus reader gets both fields from the file's first line, and
parentSessionId is what makes a turn and its hidden workers one tree — the
session record also holds them, but the idle sweep deletes it.
message events now carry structured internal content blocks as well as the
public role/content shape. In practice that means TerraFin can persist:
- assistant text
- hidden internal tool-use turns
- tool results
- retryable tool-error results
without exposing the hidden internal turns in the browser widget or public session APIs.
Important consequences:
- session list/history is transcript-derived
- reopening a session reconstructs the conversation from transcript events
- deleting a session archives the transcript file with a
.deleted.<timestamp>suffix and removes it from active history - legacy embedded conversation blobs are ignored and not migrated
- hidden internal guru sessions can still be recorded for runtime/debug purposes, but they are filtered out of normal public session history
- hidden guru sessions are also blocked from normal public read/delete/task/approval routes even when a caller knows a session id
- deleting a public parent session cascades hidden guru child cleanup
Tasks, approvals, audit, and published view context still live in the hosted runtime/session store. Only conversation history moved to transcript files.
Important files¶
Module paths use the post-refactor canonical locations. Old top-level paths
(e.g. agent/loop.py, agent/transcript_store.py) remain as compatibility
shims — see architecture.md § Current code map
for the full old → new mapping.
| File | What it owns |
|---|---|
src/TerraFin/agent/runtime/capability.py |
capability registry, session context, task registry, artifact tracking |
src/TerraFin/agent/contracts/definitions.py |
hosted agent definitions and allowlists |
src/TerraFin/agent/runtime/hosted.py |
session lifecycle, policy enforcement, task dispatch, transcript-aware session access |
src/TerraFin/agent/runtime/loop.py |
hosted loop, immediate message append flow, provider state persistence |
src/TerraFin/agent/contracts/conversation.py |
internal message/block protocol and conversation dataclasses |
src/TerraFin/agent/guru/worker.py |
route planning, hidden guru execution, and structured memo synthesis |
src/TerraFin/agent/tools/execution.py |
structured tool execution outcomes and tool-result message creation |
src/TerraFin/agent/runtime/transcript_normalizer.py |
transcript repair, tool-use/tool-result pairing, internal/public view split |
src/TerraFin/agent/runtime/context_budget.py |
proactive prompt-budget estimation and compaction levels |
src/TerraFin/agent/runtime/recovery.py |
per-turn recovery budget / repeated-error policy |
src/TerraFin/agent/storage/transcript_store.py |
append-only transcript store, sessions.json index, transcript readers, archive/rewrite helpers |
src/TerraFin/agent/storage/session_store.py |
non-transcript hosted state: tasks, approvals, audit, view context, transient conversation attachment |
src/TerraFin/agent/models/runtime.py |
provider registry, runtime-model binding, canonical provider/model refs |
src/TerraFin/agent/models/providers/*.py |
provider adapters for OpenAI and Gemini |
src/TerraFin/agent/tools/adapter.py |
function-callable tool definitions and tool execution bridge |
src/TerraFin/agent/service/client.py |
Python transport adapter (TerraFinAgentClient) |
src/TerraFin/agent/cli/main.py |
CLI adapter (terrafin-agent) |
src/TerraFin/interface/agent/data_routes.py |
HTTP runtime endpoints |
src/TerraFin/interface/frontend/src/agent/GlobalAgentWidget.tsx |
floating assistant widget |
src/TerraFin/interface/frontend/src/AppRouter.tsx |
mounts the widget across the main pages |
Runtime endpoint family¶
Hosted runtime endpoints live under /agent/api/runtime/*.
Current routes:
GET /agent/api/runtime/agentsPOST /agent/api/runtime/sessionsGET /agent/api/runtime/sessionsGET /agent/api/runtime/sessions/{session_id}DELETE /agent/api/runtime/sessions/{session_id}POST /agent/api/runtime/sessions/{session_id}/messagesGET /agent/api/runtime/sessions/{session_id}/approvalsGET /agent/api/runtime/tasks/{task_id}POST /agent/api/runtime/tasks/{task_id}/cancelGET /agent/api/runtime/approvals/{approval_id}POST /agent/api/runtime/approvals/{approval_id}/approve
Recovery architecture¶
The hosted loop now follows a stricter internal recovery path inspired by the kind of guardrails Claude Code uses internally, while keeping TerraFin's own surface and naming:
- retryable tool/input failures stay inside the loop as structured tool-error results
- fatal upstream auth/quota/provider failures are the main class still surfaced to users
- transcripts are normalized before provider calls so orphaned tool results do not leak into the next turn
- context is proactively compacted before provider calls, with reactive retry levels still kept as a last resort
POST /agent/api/runtime/approvals/{approval_id}/denyPUT /agent/api/runtime/view-contexts/{context_id}GET /agent/api/runtime/view-contexts/{context_id}
The browser widget, notebook helpers, CLI runtime commands, and Python client all sit on top of this same contract.
The runtime catalog intentionally exposes only public agents in the normal adapter surfaces. Hidden guru roles are internal runtime definitions, not default user-facing choices, and the public session-create route rejects them directly.
Browser behavior¶
The hosted runtime is not exposed through a dedicated /agent page anymore.
Instead:
- the browser UI is a floating assistant widget
- it appears across main interface pages
- it calls the same hosted runtime endpoints as every other adapter
If the deployment does not expose /agent/api/runtime/*, the widget should fail
with a clear runtime error rather than silently hanging.
Current implementation status¶
What is already there:
- capability metadata and backgroundability markers
- hosted agent definition registry
- policy-enforced capability allowlists
- hosted tool adapter
- provider-backed model loop
- transcript-first local session history
- archived session delete behavior
- hidden persona subagents reached by the main orchestrator agent via
consult_<persona>tool-calls (authoritative shape: architecture.md § Orchestrator + persona subagents) - structured internal guru memos returned to the orchestrator through a dedicated memo tool-call contract, not JSON scraped from prose
- notebook helper surface
- browser widget over the runtime endpoints
What is still intentionally lighter:
- automatic transcript compaction
- richer task progress UX
- artifact history UI
- MCP-like external adapter layer
Regression surfaces¶
When touching hosted runtime code, the highest-signal regression surfaces are:
- transcript append order for
user -> assistant/tool -> assistant - session reopen/history summaries derived from transcript + index
- session delete/archive behavior
- response parsing from each provider
- semantic agreement between the surfaces a capability does expose: HTTP reaches nearly all of them, while the Python client and CLI expose a smaller subset under the same names and arguments
- widget integration over
/agent/api/runtime/*
Current tests:
tests/agent/test_runtime.pytests/agent/test_hosted_runtime.pytests/agent/test_tools.pytests/agent/test_loop.pytests/agent/test_transcript_store.pytests/agent/test_openai_model.pytests/agent/test_google_provider.pytests/agent/test_runtime_helpers.pytests/agent/test_client.pytests/agent/test_cli.pytests/interface/test_agent_api.py
Useful commands:
pytest tests/agent/test_runtime.py \
tests/agent/test_hosted_runtime.py \
tests/agent/test_tools.py \
tests/agent/test_loop.py \
tests/agent/test_transcript_store.py \
tests/agent/test_openai_model.py \
tests/agent/test_google_provider.py \
tests/agent/test_runtime_helpers.py \
tests/agent/test_client.py \
tests/agent/test_cli.py \
tests/interface/test_agent_api.py
npm run build
Read next¶
Background-task completion delivery¶
A capability registered with background_only=True is never exposed to the
model as a synchronous tool -- only start_<cap>_task is. Which capabilities
set it is a property of the registry, not of this document. The model kicks the
task off and keeps talking; the result arrives later. "Later" needs machinery,
because no model turn is running when the task finishes.
The path, end to end:
runtime.start_task(..., origin_tool_call_id=...)records which tool call started the task, so a completion can be traced back to its request.- The worker passes a
progressreporter into the capability handler (injected only for handlers that declare the parameter). Each call recordstask.progress.stageand renews the lease, so a run taking minutes is not re-claimed mid-flight by another worker. - On completion the worker writes a marker to
hosted_pending_completions-- a dedicated table, not part of the session payload. That isolation matters: an unrelated whole-recordpersist()would otherwise clobber a marker a sibling task had just written.(session_id, task_id)is the primary key, so a double-run collapses to one marker viaINSERT OR IGNORE. - On the user's next
submit_user_message, the loop drains pending completions BEFORE the model sees the turn, appending each as auser-role message flaggedinternalOnly.
Two details are load-bearing:
- Why a user-role message, not a synthesized tool_use/tool_result pair. A
user message is forwarded verbatim by every provider adapter. OpenAI Responses
drops assistant/tool_use turns and sends only user messages plus
function_call_output; a fabricatedfunction_call_outputwith no matching server-sidefunction_callis rejected with HTTP 400. - Peek-then-clear. A marker is deleted only AFTER its delivery message is
appended. A mid-drain failure leaves the marker intact for the next turn, so a
completion is never silently lost. Delivery is deduped twice: by the store's
primary key, and by scanning the transcript for an already-delivered
taskId.
Delivery is at-most-once on a best-effort basis, not guaranteed. It rides on the task layer's at-least-once semantics, so a lease-expiry re-claim can still, rarely, double-run and double-deliver.
Verification as a tool¶
verify_claims is the one capability that checks rather than fetches. It exists
because a gate that runs after generation can only delete a wrong number, never
correct it -- so on its own it can only lower answer quality. Exposed as a tool,
the same rules become an acceptance control the model iterates against: draft,
verify, read the repair hint, resubmit.
It is self-contained -- source_text in, verdicts out -- so it needs no session
state and works identically in-process, over /agent/api/verify-claims, and from
an external agent consuming skills/terrafin/SKILL.md.
Grounding is per (line item, period), never per table. A whole-table citation
launders every figure inside it: the prose scan admits any number appearing in a
verified quote, so net income passes as capital expenditure. facts.py exists to
make the line item itself the thing that must match, and
tests/agent/test_deepresearch_facts.py pins that difference.
Multi-step arithmetic is expressed by NESTING a derivation inside an operand,
with avg for a mean -- return on assets is
margin_pct(net_income, avg(assets_t, assets_t-1)). Dividing by a literal 2 is
rejected on purpose: the literal has no citation, and admitting uncited literals
would open a hole big enough to drive any number through.