Architecture
This page explains how Terminal Agent's components fit together and how its event-driven workflows operate. The diagrams below reference real package, type, and function names so you can jump straight from a diagram to the code.
The central idea: both entrypoints — the CLI (internal/commands) and the
GUI (internal/gui) — depend on a single shared application-service layer,
internal/app.Service. That service is the boundary where prompts are resolved,
runs are orchestrated, events are streamed, and session logs are written.
Because both surfaces share it, a change in internal/app is a cross-surface
change even when only one entrypoint appears affected.
Component / layer map
The CLI and GUI talk only to app.Service. The service fans out to the agent
orchestrator, the LLM connectors, the tool implementations, and the session
logger.
flowchart TD
CLI["CLI<br/>internal/commands"]
GUI["GUI<br/>internal/gui"]
subgraph app["internal/app — shared Service"]
SVC["Service<br/>Ask · AskEvents · Chat · ChatEvents · TaskEvents"]
end
AGENT["internal/agent<br/>task orchestration + tool loop"]
CONN["internal/connector<br/>LLMConnector / ToolCallingConnector"]
TOOLS["internal/tools<br/>Tool / ToolProvider"]
LOG["internal/sessionlog<br/>Recorder (JSONL)"]
PROVIDERS["Providers:<br/>anthropic · openai · google · bedrock<br/>mistral · ollama · llama · codex · mimo"]
CLI --> SVC
GUI --> SVC
SVC --> AGENT
SVC --> CONN
SVC --> LOG
AGENT --> CONN
AGENT --> TOOLS
CONN --> PROVIDERS
SVC -. "Event channel<br/>(streamed back)" .-> CLI
SVC -. "Event channel<br/>(streamed back)" .-> GUI
Key types / files: Service interface and Event type in
internal/app/service.go; provider dispatch in
internal/connector/main.go (NewConnector); Tool / ToolProvider in
internal/tools.
Ask / chat flow
ask and chat are streaming, non-agentic flows: resolve a prompt, call the
connector, stream text deltas back as events. Only ask/chat may layer in
memory and context files; task does not. Session-log records are written
inline at request, completion, and failure.
sequenceDiagram
participant U as CLI / GUI
participant S as app.Service<br/>(AskEvents / ChatEvents)
participant P as Prompt resolution<br/>(+ memory / context for ask)
participant C as connector<br/>(Query + OnStream)
participant L as sessionlog.Recorder
U->>S: AskRequest / ChatRequest
S->>L: write meta + request record
S-->>U: EventStarted
S->>P: resolve system prompt
P-->>S: prompt
S->>C: Query(params, OnStream)
loop streaming
C-->>S: text chunk (OnStream)
S-->>U: EventOutputDelta
end
C-->>S: final response
S->>L: write completed (or failed) record
S-->>U: EventCompleted / EventFailed
Key types / files: internal/app/ask.go, internal/app/chat.go;
QueryParams.OnStream in internal/connector/types.go. Recording is inlined at
each call site (request / completed / failed).
Task event workflow
task is the agentic flow. The agent runs an iterative loop: ask the model,
and if it requests a tool, confirm and execute that tool, feeding results back
until the model returns a final answer. Every step is forwarded through the
OnStep callback into the session log, and live tool output is streamed as
EventOutputDelta. When a tool is invoked with final=true, its raw output is
returned directly without another summarization round (DirectRawOutput).
sequenceDiagram
participant U as CLI / GUI
participant S as app.Service<br/>(TaskEvents)
participant A as agent<br/>(runTaskLoop)
participant C as ToolCallingConnector<br/>(QueryWithTool)
participant T as tool<br/>(unix / python / file_edit ...)
participant L as sessionlog.Recorder
U->>S: TaskRequest
S-->>U: EventStarted
loop each iteration (until final_answer / limits)
S-->>U: EventTaskStatus (thinking)
A->>C: QueryWithTool(params, tools)
C-->>A: response (tool use? + tool input)
alt model returns final_answer
A->>L: OnStep (final_answer)
S-->>U: EventCompleted
else model requests a tool
A->>A: confirmTool (see permission flow)
A->>T: execute (ToolExecutionContext.Output sink)
loop live output
T-->>S: output chunk
S-->>U: EventOutputDelta (ToolName, ProcessID)
end
T-->>A: captured result
A->>L: OnStep (succeeded / failed)
note over A,U: if final=true → DirectRawOutput<br/>return raw output, skip summarization
end
end
Key types / files: runTaskLoop, handleTaskToolResponse,
executeTaskTool, TaskOptions.OnStep, taskStepToRecord in
internal/agent/task.go; the TaskEvents wiring in internal/app/task.go;
the live Output sink in tools.ToolExecutionContext.
Permission / confirmation flow
Whether a tool runs without prompting is decided in two stages. First, a
per-tool permission category sets the default: read generally does not
prompt, file_search prompts when the requested root is outside the current
read scope, write is allowed when the target resolves inside the workspace
root or an exact approved write path, execute always prompts, and any
undeclared tool is treated as execute. Second, the allow / deny / ask
rule engine overrides that default.
flowchart TD
START["tool call"] --> CAT{"permissionCategoryFor"}
CAT -->|read| READSCOPE{"read target<br/>inside scope?"}
READSCOPE -->|yes| AUTO["auto-allow"]
READSCOPE -->|no| RULES
CAT -->|write| INROOT{"path inside workspace<br/>or exact write scope?"}
INROOT -->|yes| AUTO
INROOT -->|no| RULES
CAT -->|execute / undeclared| RULES{"ConfirmWithPolicy<br/>allow / deny / ask"}
RULES -->|ask rule matches| PROMPT["prompt user"]
RULES -->|deny wins| BLOCK["denied"]
RULES -->|allow wins| AUTO
RULES -->|no rule matches| PROMPT
PROMPT -->|yes / no| RUN["run or skip"]
PROMPT -->|yes! / no!| REMEMBER["persist to nearest<br/>.terminal-agent.json"]
REMEMBER --> RUN
When a prompt is required, the agent core does not talk to the terminal or
window directly. It calls Interaction.Confirm, and the app layer turns that
into an event carrying a Reply callback, then blocks until the consumer
replies — the same channel-based transport works for both CLI and GUI.
sequenceDiagram
participant A as agent core<br/>(ConfirmationManager)
participant I as app.taskEventInteraction
participant U as CLI / GUI consumer
A->>I: Interaction.Confirm(req)
I-->>U: EventConfirmationNeeded<br/>(Action + Reply callback)
note over I: blocks on internal replies channel
U->>U: render prompt, read user decision
U->>I: Reply(response)
I-->>A: TaskConfirmationDecision<br/>(Allowed, Remember, Patterns)
note over A: decision cached for this run;<br/>Remember persists via rememberFunc
Key types / files: permissionCategoryFor and confirmTool in
internal/agent/task.go; ConfirmWithDefault / ConfirmWithPolicy and the
rule engine in internal/agent/confirmation.go; Confirm / Clarify
transport in internal/app/task.go; config discovery in
LoadPermissionRuleSets (internal/config/permissions.go), which walks from
the current directory up to the filesystem root.