Repository navigation
internals architecture
How Steiner's packages fit together and how one turn moves through them. Topic details live in the linked pages. Repo map and contributor rules are in AGENTS.md.
cmd/steiner wires dependencies and flags. It holds no business logic.
| Mode | Entry | Path to agent.Runner
|
|---|---|---|
| exec |
runExecMode (exec.go) |
cliRunner.RunWithHooks → cliRunner.run → Runner.Run
|
| interactive |
runInteractiveMode (interactive.go) |
interactive.Session.driverRun (driver_run.go) → sessionRunner.Run (interactive_session.go) → cliRunner.RunWithHooks → cliRunner.run → Runner.Run
|
| oneshot |
runOneshotTask, runOneshotResume (cmd_oneshot.go) |
oneshot orchestrator → phaseRunner.RunPhase → agent.NewConversationDriver (internal/agent/conversation_driver.go, called from phase_driver.go) → cliRunner.driverRun (runner_driver.go) → cliRunner.run → Runner.Run
|
defaultBuildRuntime (runtime.go) calls buildRuntimeWithRoots (runtime_build.go). It builds the config, sandbox (buildRuntimeSandbox), diagnostics, event sink, delegation supervisor, MCP and LSP managers (connectRuntimeMCP, connectRuntimeLSP), provider factory (buildRuntimeProviderFactory), tool registry (buildRuntimeRegistry), and history and session stores. The result is the cliRuntime struct.
| Layer | Packages | Key internal imports |
|---|---|---|
| Composition | cmd/steiner |
agent, interactive, oneshot, provider, session, tool, tui
|
| Orchestration |
interactive, oneshot, delegation
|
interactive: agent, delegation, session, prompt, tool. oneshot: agent, session, fsatomic, tool. delegation: agent, advisor, prompt, provider, tool
|
| Turn loop | agent |
config, diagnostics, output, prompt, provider, tool, tool/builtin, usagestats
|
| Context | prompt |
config, provider, skill
|
| Transport | provider |
config, diagnostics, metadata, oauth, quota, usagestats
|
| Tools |
tool (with tool/builtin), mcp, lsp, sandbox
|
tool: config, diagnostics, provider; sandbox: config; mcp, lsp: config, tool
|
| Persistence | session |
agent, delegation, fsatomic, prompt, tool
|
| Presentation |
output, tui
|
output: prompt, provider, tool, usagestats; tui: agent, interactive, output
|
tool and provider never import agent. prompt never imports delegation. session imports delegation only in history_render.go, for tool-argument summaries and delegation failure classification when archived history is rendered.
-
Runner.run(internal/agent/runner.go) validates the request, buildsRunState, and creates aturnProgressor(newTurnProgressor). The loop ends on a stop reason.stopRunBeforeTurnenforcesLimits.MaxTurns, andLimits.TurnTimeoutbounds each turn. -
turnProgressor.advanceprepares the turn (prepareTurn), compacts when the budget requires it (handleCompaction), and calls the model (executeModelCall) underLimits.ModelCallTimeout. -
executeChatRequest(model_call.go) callsChatCompletionfirst unlessRunRequest.StreamingPreferredis set, then falls back toStreamChatCompletion. Streamed chunks become output events. -
executeToolCalls(turn_progression_tools.go) walks the tool calls in order. Consecutive calls with the sameParallelClassOfclass form a batch if that class limit is above 1 (parallelRunLength).invokeParallelruns the batch behind asemaphore.Weightedsized byMaxParallelToolsorMaxParallelDelegations. Other calls run one at a time (executeSingleToolCall). - Each call runs through
tool.Executor.Execute.appendToolOutcomerecords the result as a tool message in the conversation. -
RunRequest.DrainInboxsupplies queued input for the run (nil for sub-agents). Runs end with aStopReason:complete,max_turns,max_tokens,cancelled,error,usage_limit, orworkflow_handoff. Normal, limit, and in-loop error endings callemitStop. Run-validation and post-ingestion failures setStopReasonErrorand return without it. - A transient provider error that survives provider-level retries gets a bounded per-turn retry (
handleTransientProviderRetry).
-
provider.Provider(interface.go) hasChatCompletion,StreamChatCompletion, andSupportsUsageStats.StatefulTranscriptmarks backends that own the transcript, so callers must not rewrite it. -
provider.Client(client.go) implementsProviderfor HTTP backends. It handles pacing (pace), retries, and diagnostics. It delegates format work to aWire(wire.go:Payload,HTTPRequest,DecodeResponse,DecodeStream). Wires areopenaiWire,responsesWire, andanthropicWire. - Retry:
Client.withRetry(retry.go) loops over attempts, waiting forretryBackoffDelayor the serverRetry-After.classifyRetryErrorcallsclassifyProviderError(client.go), which usesisRetryableHTTPStatusandisRetryableTransportError(http_error.go). Classification uses the original call context to tell caller cancellation apart from a provider-side timeout: a timeout error that matchescontext.DeadlineExceeded, such as anet/httpclient or response-header timeout, is retryable while the call context is still live and stays subject to the configured attempt and backoff limits. A closed connection (net.ErrClosed, or theuse of closed network connectiontext) is retryable on the same terms throughisRetryableClosedConnection.streamWithRetry(client.go) marks a partial stream once visible chunks (chunkVisible) have been forwarded; a deadline-shaped timeout or closed connection after visible output ends the call without another attempt because emitted chunks cannot be withdrawn, while pre-content timeouts and closed connections remain retryable.classifyStreamRetryErrorapplies this rule while the call context is live. Once the call context is done, the call ends without another retry. The wire may refine the decision throughRefineRetry. Usage-limit errors and non-transient decode errors (errDecodeChatCompletionResponse,errResponsesStreamFailed) are not retried. Transient decode failures (errDecodeStreamChunkUnexpected,errResponsesStreamFailedTransient) are retried (classifyProviderError). - Provider types are
config.ProviderTypevalues.claude_subscriptionruns Claude CLI processes (claudesub_*.go) instead ofClient. See claude-subscription.md.
A non-streaming 400 whose body mentions stream and must or required is matched by agent.IsStreamRequiredError (model_call.go). The turn falls back to streaming, the run latches skipNonStream so later turns stream directly, and a stream_required warning diagnostic is emitted. Stateful transcripts return the error instead of falling back.
-
tool.ToolDef(types.go) hasName,ParameterSchema,Timeout,ParallelSafe, and MCP provenance. It runs either an in-processHandleror a subprocess (ExecPath,Subcommand). -
tool.Registry(registry.go) holds definitions.NamesandDefinitionssort by name, so tool order is deterministic. - Sources: built-ins (
tool/builtin, for exampleNewBashTool), MCP tools (mcp.md), LSP tools (lsp.md), the advisor (advisor.md), and sub-agent tools fromdelegation. -
Executor.Execute→runPipeline(execution_pipeline.go):- Reject unparseable input (
KindInvalidToolInput) and resolve the definition. - Apply the execution mode. Plan mode narrows writes to the plan directories (
RestrictWritesTo), and a scoped executor then appliesWithWriteScope, so adocumentorchild'smutateonly accepts documentation file names. -
normalizeExecutionInputrunsPathPolicy.ValidateToolInput. A promptable out-of-project path raises anApprovalRequest(ApprovalKindPath) throughApprovalResponder. - Store the sandbox decision in context (
SandboxWrapperKey). -
executeToolruns the handler directly, or the subprocess under the tool timeout. Sandbox denials can re-enter approval (execution_denial.go).
- Reject unparseable input (
- Output bounding: captures use
boundedCapture(output.go). Bash keeps a head and tail (captureUntilMarker) and saves full output as an artifact (bashArtifactStore). -
fetch_urlnetworking:newHTTPGuardClient(tool/builtin/fetch_url.go) wrapswonton/httpguard, so every dialed address, redirect hops included, must be public. Redirects are capped byfetchRedirectLimit(9 followed, a tenth hop refused) with plain HTTP hops allowed, and the client carries a 15s total timeout because httpguard sets none itself. The httpguard transport setsProxy: nil, so ambientHTTP(S)_PROXYsettings cannot relay a request around the dial check; the user-facing tradeoff is in tools.md. Search backends (tool/builtin/brave_search.go,tool/builtin/search_backend.go) keep plainhttp.Clients and proxy support, and read non-200 response bodies throughsearchErrorBodyLimitbecause only the leading 100 bytes reach the error message. - Modes and sandboxing: execution-modes.md, sandboxing.md.
| Store | Location | Code | Holds |
|---|---|---|---|
| Sessions | ~/.config/steiner/sessions |
session.Store (store.go) |
session.Session: model, mode, skills, Lineage, cache key, sub-agent ledger |
| Compaction history | ~/.config/steiner/sessions/<sessionID>/history.md |
session.HistoryArchive (history_archive.go) |
Markdown source removed by compaction |
| Prompt history | ~/.config/steiner/history.log |
history.Writer |
Input history |
| Project directory |
.steiner/ in the project |
session.EnsureSteinerProjectDir |
Project state, with a .gitignore
|
-
Storewrites one file per session and an index, using atomic writes (writeAtomic). It evicts the oldest entries and removes each evicted session's history directory. -
HistoryPath,AppendHistory, andReadHistory(session/store.go) manage the per-session compaction archive. Deletion removes the archive directory with the session. - Resume:
Session.LoadSessionByID(persistence.go) →loadSession.replayLoadedTranscript(interactive/replay.go) re-emits events so the TUI shows the transcript.session.Forkcopies only the latest lineage generation; the parent's compaction archive remains with the parent. - Compaction adds a generation with
ConversationLineage.WithNewGeneration(compaction_escalation.go,interactive/compaction.go). - Other stores: diagnostics (diagnostics.md), usage stats (prompt-caching.md), quota snapshots (usage-dashboard.md), oneshot manifests (oneshot.md).
- Components emit
output.Eventvalues (event_types.go:Type,Timestamp,Payload,Scope) throughoutput.EventSink.Emit. Sub-agent events carryScope.AgentID. - Sinks:
output.NewStreamwrites terminal output,output.NewFileLogSinkwrites the log file, andoutput.NewMultiSinkfans out to both (runtime_build_logging.go). - TUI:
tui.eventBridge(event_bridge.go) buffers events and delivers them asruntimeEventMsg.Model.updateDispatch(model_update.go) routes each message to one handler.handleRuntimeEventMsgcallsapplyEvent(model_events.go), which routes sub-agent events toapplySubAgentEventand payload events toapplyPayloadEvent. - User input flows back as
interactive.Actionvalues (actions.go:SubmitPrompt,SubmitApproval,InterruptActiveRun,CancelDelegate), dispatched bySession.Handle(session_dispatch.go). - Approval round trip:
agent.NewEventingApprover(approval.go) emits an approval event. In interactive mode,approvalResponder.RequestApproval(interactive/wiring.go) registers the request withApprovalCoordinatorbefore the event is emitted. The TUI shows it and sendsSubmitApproval, andApprovalCoordinator.Submitreleases the waiting request. In exec mode,stdinApprovalResponder(cmd/steiner/approval.go) answers instead.
- Prompt source order and tool ordering: context-management.md, prompt-caching.md
- Cache hint preservation: prompt-caching.md
- Tool output bounds: Tools
- Approval gating for paths and MCP calls: execution-modes.md, mcp.md
- Concurrency limits and sub-agent nesting: sub-agent-delegation.md
- Retry layers: Provider
Other topic pages: configuration.md, desktop-notifications.md.
Get started
Using Steiner
- How to work with steiner
- Interactive mode
- CLI reference
- Skills
- Sub-agent delegation
- Oneshot
- Context management
- Advisor
- Security audit
Tools and integrations
Safety
Usage and cost
Reference
Internals and maintenance