thcode separates intelligence from authority. The reasoning model (Typhoon) runs remotely and proposes actions — but every proposed action is an untrusted suggestion until the local agent loop validates, authorizes, and executes it on your machine. The local loop owns the conversation history, context selection, tool definitions, permission enforcement, and stop conditions. Typhoon is never given direct access to your filesystem, shell, or network. This architecture is defined in ADR 0001: the CLI owns the outer loop and calls the selected reasoning provider directly. The thcode API handles specialist artifact processing through AI for Thai services. Neither component can execute repository, filesystem, shell, or test tools on your machine — only the local loop can.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Temicide/thcode/llms.txt
Use this file to discover all available pages before exploring further.
The AgentLoop class
AgentLoop in core/agent/loop.ts is the central orchestrator. It is constructed with an AgentDeps bundle that wires up the provider registry, tool registry, credential store, and workspace root.
AgentLoop only through typed inputs and an ApprovalCallback. The core never touches the terminal, filesystem, or network except through its declared dependencies.
runTurn() — the main entry point
Every user message dispatches a single runTurn() call:
TurnState
TurnState carries the full permission-relevant session context for the turn:
ApprovalCallback
TheApprovalCallback is the only bridge between the agent core and the developer for per-action approval. The UI resolves true (approve) or false (reject) — the core never renders prompts itself:
ask for a tool proposal, runTurn() emits an ask event and awaits this callback. If the callback returns false, the rejection is recorded in the transcript and the loop continues to the next iteration.
onToken streaming callback
Pass an optional onToken callback to receive streaming token deltas from the provider as they arrive. This enables the UI to render partial responses in real time without waiting for the full completion.
TurnResult
runTurn() returns a TurnResult with the final assistant text, the updated conversation history (including the new turn), and the full sequence of events that occurred:
TurnEvent — the discriminated union
Every significant event during a turn is recorded as aTurnEvent. The events array gives UI layers a complete audit trail of what the agent did, in order.
- text
- tool-start
- tool-result
- ask
How a turn executes
The loop runs for at mostMAX_TOOL_ITERATIONS = 8 iterations. Each iteration follows this sequence:
Build Active Model Context
DeterministicContextBuilder assembles the conversation history and new input into a bounded context. If utilization exceeds 70%, compactContext() is called. If compaction fails or context capacity is unavailable, the turn terminates with a descriptive text event — never a crash.Check provider availability
The credential store is queried for the selected provider’s API key. If the provider is unavailable or unconfigured,
runTurn() returns a clear "Provider unavailable" message immediately without entering the iteration loop.Call Typhoon (the reasoning provider)
The finalized request — messages, tool schemas for the active Work Mode, and output token budget — is dispatched to the provider adapter. The tool schemas are filtered by
ToolRegistry.listForMode(mode) so mutating tools are never offered to the model in Plan mode.Final text or tool-call proposal
If the provider returns a final text result, the turn ends immediately with a
text event. If the provider proposes a tool call, the loop continues to authorization.Structural Plan-mode enforcement
ToolRegistry.resolveForMode(toolName, mode) is called first. If the tool is mutating and mode is plan, PlanModeToolRefusedError is thrown. The refusal is recorded as a tool-result event with ok: false, fed back as [tool refused], and the loop continues to the next iteration.Permission engine evaluation
evaluatePermission() is called with the tool’s mutating and sensitive attributes and the current mode/profile state. The result is allow, ask, or deny.deny: recorded as atool-resultwithok: false, fed back, loop continues.ask:askevent emitted,ApprovalCallbackawaited. If rejected, recorded and loop continues. If approved, execution proceeds.allow: execution proceeds immediately.
Tool execution
The tool’s
execute() method is called with the input and workspace context. Success or failure is recorded as a tool-result event. The output is fed back into pendingInput for the next iteration.Conversation history is immutable
Thetranscript inside runTurn() is rebuilt immutably at each step — no in-place mutation. The state.history passed in is never modified. The TurnResult.history returned is a new array that includes the new turn, ready to be passed as state.history on the next call.
Provider error handling
Provider network errors are classified by the adapter’sclassifyError() method and surfaced as a structured text event. The loop never crashes on a provider error — it returns a descriptive message that includes the error kind and whether the error is retryable.
The CoreApp facade
UI layers never interact with AgentLoop directly for filesystem, network, or shell access. The CoreApp facade is the sanctioned surface — UI components dispatch typed intents and receive typed results. This means that any terminal UI, IDE extension, or headless test harness can drive the same agent core without re-implementing permission logic, containment checks, or provider dispatch.
The
EffectExecutor.authorize() method in permissions/pep.ts is the only sanctioned local effect-authorization surface. Concrete tool adapters never evaluate the PEP themselves or branch on outcome === 'allow' to invoke effects directly — they always go through the executor.