016 · Engine
Agent engine: turns, plan mode, steering, side questions, compaction (spec_engine_016)
| Status | Implemented. Reverse-engineered from 53f8c53 (2026-09-24) and kept with the code since; its paths and names checked 2026-09-27 (//tools/specs). |
| Source | pkg/engine/runtime/engine.go, plan.go, steer.go, aside.go, compact.go, search.go, images.go, usage.go |
| Tests | pkg/engine/runtime/engine_test.go, plan_test.go, steer_test.go, aside_test.go, compact_test.go, bulkhead_test.go, telemetry_test.go, runtime_security_test.go, mcp_agents_test.go |
| Related | spec_models_015, spec_agents_014, spec_workspace_018 |
The Engine wraps the Google ADK runner (google.golang.org/adk/v2). It builds the agent tree, runs one prompt as a turn, enforces per-turn limits and read-only modes, delivers user steering mid-turn, answers side questions without touching history, compacts history, runs sub-agents, and records usage and traces. It is safe for concurrent use.
- ENG-01 The active agent is the root
llmagent; every other registered agent is a sub-agent. If the active agent is unknown the engine falls back toblitz(error if absent). - ENG-02 Root instruction = the agent prompt interpolated with the configured agency level + (if skills are enabled and any exist) “## Available Agent Skills” listing name and description with a hint to use
activate_skill+ the image instruction + extra instructions (memory, reply language). Sub-agents use their own spec’s agency level. - ENG-03 The image instruction (“You can see images…”, plus a
view_imagehint when the agent has that tool) is added only when image support is on. - ENG-04 Each agent gets the registry tools named in its spec and the MCP toolsets offered to it (root: primary=true).
- ENG-05 Generation config:
temperatureandmax_tokensfrom[blitz]when > 0; per-model settings override per call (spec_models_015). - ENG-06 Changing the active agent, model, pins, instructions or agency rebuilds the runner; on failure the previous value is restored. Session, artifact and memory services are shared across rebuilds so history survives.
- ENG-07 The runner always has a compaction config, because it honours compaction events only then:
retain_events(default 20) and the threshold, orMaxInt32when auto-compaction is off.
- ENG-10
Execute(ctx, sessionID, prompt, handler, opts…): empty session ID isdefault. Run state in the context carries session ID, max turns, attachments, plan-only flag and mode. - ENG-11 The user content is the attachments (images first, as providers recommend) followed by the prompt text.
- ENG-12 Tool-call parallelism: at most
tools.max_parallel(default 8) tool calls of one model response run at once. The limit is per batch (sub-agents get their own), avoiding deadlock; every task runs exactly once, queued tasks after cancellation fail fast. - ENG-13 Streaming mode is SSE when the workspace was opened with streaming.
- ENG-14
WithMaxTurns(n): the before-model callback counts model calls in the run; call n+1 fails withErrMaxTurns(“maximum turns reached (n model calls)”), which is not wrapped. - ENG-17
WithAllowedTools(names)refuses every tool outside the list for the run (sub-agents included): “X isn’t among the tools this command allows (…)”. - ENG-16 Per-run overrides:
WithAgent(name)makesnamethe root agent andWithModel(llm)replaces the configured model, for that run only. Either builds a runner for the run (sharing the session, artifact and memory services) without changing the engine’s active agent, model or pins. The override model runs the root agent even when it is pinned, and every unpinned agent; pinned sub-agents keep their pins;invoke_agentsub-agents in the run follow the same rule. An unknown agent is an error. Usage is priced by the model each agent ran on in that run, and the turn span names the run’s agent and model. Used by workers (spec_workers_023 WK-45). - ENG-15 Other runner errors are wrapped as “agent execution error: …”; a failing handler aborts the drain. Failures not caused by cancellation are logged at error level.
- ENG-20 Before each tool: audit
tool_call(name, args); apply the plan/read-only refusal (§4); MCP approval (ApproveMCP);pre_toolhooks (a block returns{"error":"blocked by pre_tool hook: <reason>"}instead of running). Refusals are returned as tool results so the model sees them. - ENG-21 After each tool:
post_toolhooks (asynchronous, see spec_hooks_010); audittool_resultwith decisionok/error(tool error or anerrorfield); attach steer messages (§5). - ENG-22 After each non-partial model response: note fallback changes (§ fallback in spec_models_015) and record usage priced by the model that actually answered (
ModelVersionif it has a price, else the calling agent’s model), including cache-write tokens fromCustomMetadata["cache_creation_input_tokens"].
- ENG-30 Tools allowed in plan/read-only mode (
runtime.PlanAllows):read_file,list_files,glob,grep,search_workspace,lsp,view_image,view_document,view_media,web_fetch,web_search,list_agents,invoke_agent,list_or_search_skills,activate_skill,remember,list_tasks,task_output,list_mcp_resources,read_mcp_resource,ask_user_question, and the workflow toolstodo,exit_plan_mode,enter_plan_mode. Everything else, including every MCP tool (but its resources), is refused. (Brought up to date 2026-10-14.) Sub-agents run inside the same run and are equally restricted. - ENG-31 Refusal text: plan mode — “plan mode: X is disabled because it could change something. Describe this step in the plan instead.”; named mode — “
is read-only: X is disabled because it could change something.” - ENG-32
PlanPrompt(goal)asks for: objective summary, numbered implementation plan naming files/functions, risks and unknowns, verification, and questions only if blocking — presented withexit_plan_mode(or as the answer when no one can review it).CarryOutPrompt(planFile)is the go-ahead after approval. - ENG-34 A turn is also planning while its
tools.PlanGatesays so (the agent calledenter_plan_mode); plan refusals then apply from the next tool call. The primary agent always gets the workflow tools (todo,exit_plan_mode, andenter_plan_modewhenplan_review = agent-decides), whatever its tool list; sub-agents don’t. - ENG-33 Read-only modes are used by
/search(WithReadOnly) and/btw(modebtw).
- ENG-40
Steer(session, text)queues a message. The next tool result of that session’s own turn (not a sub-agent’s session) carries queued messages undermessage_from_user(a string for one, an array for several), so the model reads them before its next step without interrupting anything; the history keeps them where they arrived. A tool error is preserved in the result’serrorfield. A trace eventsteerrecords the count. - ENG-41
TakeSteers(session)removes and returns messages no tool result carried (the model finished without another tool call); the front end sends them as the next prompt (Turn.Accepted) or drops them if the turn was interrupted. - ENG-42 A session takes steer messages only while its turn does: the workspace opens it as the turn starts (
OpenSteers) and closes it as it collects the leftovers (CloseSteers, or when the turn ends otherwise). Outside that,Steerreturnsapi.ErrSteerTooLate(the service’sSTEER_TOO_LATE);Workspace.Steerhas recorded the message in the transcript first, so the caller sends it as the next prompt withTurn.Accepted, after the turn’s leftovers: the REPL collects them from its steer prompt, the desktop app fromSessionService.Steer’s error (running, it adds them to the leftovers; idle, it sends at once). A background process’s notice that is too late waits for the next prompt instead (BL-SVC-10). - ENG-43 Path-scoped rules ride on tool results the same way, under
project_rules(spec_memory_012 MEM-07); when both apply, rules come first and steer messages are added to the same result. - ENG-42 Rationale (decision): ADK model callbacks cannot add session events, so tool results are the channel that reaches both the model and the history.
- ENG-50
Aside(session, prompt)copies the session’s events (through JSON, sharing nothing) into a throwaway in-memory session, runs the root agent there in read-only modebtw, then discards it. Neither question nor answer reaches the history, the saved event log, the transcript or later prompts. - ENG-51 The copy honours existing compaction summaries but never compacts (threshold
MaxInt32). Tokens count toward the real session’s usage. Abtwspan is recorded. A session with no turns yet starts empty.
- ENG-60 Automatic: the ADK runner compacts once a prompt reaches
context.token_threshold, retainingretain_eventsraw events. - ENG-61 Manual
Compact(session, focus, keepTurns≥1): find the start of the keepTurns-th most recent user turn (a user-authored event that is not a function response and not a compaction), so a tool call is never separated from its result. Nothing before it, or everything already compacted, isErrNothingToCompact. - ENG-62 The window is summarised by the current model with a fixed prompt (goals and constraints, decisions and why, files read/changed, commands and outcomes, unresolved errors, next steps) plus “Pay particular attention to:
”, timeout 3 min. Earlier summaries inside the window are fed as “[Summary of earlier conversation]” events, and the events they cover are omitted, so a second compaction never loses the first summary’s content. - ENG-63 Only prose parts of the summary are kept; an empty summary is an error and history is unchanged (recording it would delete the covered turns).
- ENG-64 The compaction event spans the window’s true timestamp range and lists as excluded any in-range event outside the window not already covered, so nothing is dropped unsummarised. If the clock is not after the window’s end, it refuses to record. Summarisation tokens count toward session usage. A
compactspan is recorded.
- ENG-70
InvokeSubagent(agent, prompt)(behind theinvoke_agenttool) builds the agent fresh and runs it in an isolated in-memory sessionsubagent-<name>-<seq>; returns the concatenated final non-thought text authored by that agent. Nesting is capped at depth 3 (ErrSubagentDepth). The calling run’s usage accounting, per-model settings and turn limit context still apply.
- ENG-80 Per call: input = prompt + tool-use prompt tokens; cached; cache-write (clamped to uncached input); output = candidates + thoughts;
LastPrompt= input (the current context size). - ENG-81 Cost = (uncached × input + cached × cached_input + cache_write × write_rate + output × output_rate) / 1e6, where write_rate defaults to the input rate. Price lookup: exact model name, else the longest configured prefix (
gemini-3.8-flash-001→gemini-3.8-flash). An unpriced call marks the sessionPriced=false. - ENG-82 Usage is kept per session in memory for the life of the process; it is not persisted.
- ENG-90 Each turn is its own trace root (
turnspan) with attributes agent, model, prompt chars, attachments, max_turns, plan_only,gen_ai.conversation.id,turn.index, and at the end model_calls, tokens.input/output and cost_usd (when priced). - ENG-91 Turns of a session are chained: the previous turn’s traceparent and index are loaded from the turn store (session metadata
last_turn) and linked (link.type=previous_turn); after the turn the new traceparent is saved. With telemetry off nothing is written.
- ENG-95
WebSearchPromptlists the approved links (title, URL, snippet), tells the agent to read them withweb_fetch, skip failures, cite URLs used, say what is missing, and that other pages need approval; appends the engine’s own summary marked unverified. - ENG-96
SessionSearchPromptlists matching passages as[message N, role, YYYY-MM-DD HH:MM]with a note that earlier turns may be compacted; with no matches, asks the agent to answer from memory, say if it never came up, and not use tools.