1 // Package boot assembles a ready-to-drive control.Controller from configuration:
2 // it loads config, resolves the model(s), builds the tool registry (built-ins +
3 // plugins), wires the permission gate, and constructs the executor — optionally
4 // wrapping it in a two-model Coordinator. It is the one place that turns "what the
5 // user configured" into "a Controller a frontend can drive", so every frontend —
6 // the terminal TUI, the HTTP/SSE server, the desktop webview — shares the exact
7 // same assembly instead of each re-deriving it. Frontends pass only a sink and a
8 // couple of run knobs; everything else comes from config.
33 // maxToolOutputBytes caps a single tool result before it goes into the model's
34 // context. ~32KB is roughly 8K tokens — enough for a full file read or a busy
35 // grep, while preventing one accidental "read this 5 MB log" from blowing the
36 // window before the next compaction runs.
37 const maxToolOutputBytes = 32 * 1024
38
39 const maxFinalReadinessBlocks = 3
40
41 // maxFinalReadinessBlocksWithProgress is the hard cap on readiness retries when
42 // the model keeps producing new host-observable receipts between blocks. A
43 // converging turn (edit → verify → review still catching up to the latest
44 // mutation) deserves more nudges than a stuck one; a turn that stalls with no
45 // new receipts still fails at maxFinalReadinessBlocks.
46 const maxFinalReadinessBlocksWithProgress = 6
47 const maxEmptyFinalBlocks = 3
48 const maxStreamRecoveries = 3
49 const maxExecutorHandoffNudges = 1
50
51 // DeliveryRuntimeMarker is the delivery-mode contract block appended to user
52 // turns (withTurnPreferences). Exported as the single source of truth for the
53 // byte-exact suffix strip in preview derivation and for cross-package tests;
54 // its text is cache-frozen — changing it breaks steer replay matching and the
55 // prefix stability of every live delivery session.
56 const DeliveryRuntimeMarker = `<delivery-runtime>
57 This session is in delivery-first mode. Before any state-changing tool call,
58 establish concrete, verifiable acceptance criteria with todo_write. After the
59 change, inspect the result, run relevant verification, and sign off each step
60 with complete_step citing the successful verification command. The host enforces
61 these gates and will reject mutation or finalization when evidence is missing.
62 </delivery-runtime>`
60 // ErrTurnRunning reports that a caller tried to start a second foreground turn
61 // while one is already active in the same Controller.
62 var ErrTurnRunning = errors.New("turn already running")
63
64 // errTurnRunningRotation and errRotationInProgress are returned by the
65 // session-rotation gate (beginRotation) when a rotation cannot proceed: a turn
66 // is in flight, or another rotation already holds the gate.
67 var (
68 errTurnRunningRotation = errors.New("cannot start a new session while a turn is running")
69 errRotationInProgress = errors.New("cannot start a new session while another session change is in progress")
70 )
71
72 // errNoSessionPath is returned by snapshot when a session has content to persist
73 // but no resolved session path — a misconfiguration (e.g. an unresolvable data
74 // dir in a bot deployment) that previously dropped conversations silently
75 // (#4414). Callers log it and continue; it must never be swallowed quietly.
76 var errNoSessionPath = errors.New("session has content but no session path; conversation cannot be persisted")
40 // Message is a single conversation message.
41 type Message struct {
42 Role Role `json:"role"`
43 // Content is the provider-visible conversation content. Keeping this legacy
44 // field provider-visible preserves replay for older CLI/Desktop releases.
45 Content string `json:"content,omitempty"`
46 // RawContent is the user-authored form of a user turn, when it differs from
47 // Content because the host added transient context. Older releases ignore
48 // this field and still replay the provider-visible Content safely.
49 RawContent string `json:"raw_content,omitempty"`
50 // ProviderContent is a transitional field written by early Context Engine v2
51 // builds. Loaders migrate it into Content/RawContent before normal use.
52 ProviderContent string `json:"provider_content,omitempty"`
53 Images []string `json:"images,omitempty"` // data URLs (data:<mime>;base64,…) on user (attachments) and tool (MCP image results) messages; embedded only for vision-capable models
54 ReasoningContent string `json:"reasoning_content,omitempty"` // assistant: thinking-mode chain-of-thought, round-tripped on multi-turn
55 // ReasoningSignature is an opaque, provider-issued proof that ReasoningContent
56 // is genuine model output. Anthropic requires the signed thinking block be
57 // replayed on the next turn when a tool call followed thinking; providers
58 // without signed reasoning (e.g. the openai-compatible ones) leave it empty.
59 // Round-tripped alongside ReasoningContent.
60 ReasoningSignature string `json:"reasoning_signature,omitempty"`
61 ToolCalls []ToolCall `json:"tool_calls,omitempty"` // set by assistant
62 ToolCallID string `json:"tool_call_id,omitempty"` // links a tool result to its call
63 Name string `json:"name,omitempty"` // tool message: tool name
… 2 lines omitted; exact range 40–75 …
66 CreatedAt int64 `json:"createdAt,omitempty"` // local UI metadata; unix milliseconds; stripped before provider requests
67 Edited bool `json:"edited,omitempty"` // local UI metadata; provider requests ignore it
68 Original string `json:"original,omitempty"` // user prompt before inline edit
69 // LocalOnly marks durable transcript content that must never be sent to a
70 // model provider. Interrupted streaming output uses it so every frontend can
71 // replay what the user saw without feeding partial reasoning or tool-call
72 // arguments back into the next request.
73 LocalOnly bool `json:"local_only,omitempty"`
74 InterruptedTurn *InterruptedTurnRecovery `json:"interrupted_turn,omitempty"`
75 }
模型上次说“我要调用工具”但进程崩了,下一次请求前会补一张“没有结果”的占位回执,避免 API 因为票据对不上直接 400。
对自研 Harness 的含义
恢复协议应在 provider 边界做防御性修复,而不是污染用户保存的原始 session。
关键源码 · 契约
internal/provider/provider.go · L179–L192
179 // interruptedToolResult stands in for a tool result that never landed — an
180 // assistant tool_calls turn whose execution was cut short (interrupt, crash) and
181 // later resumed. Sending such a turn unanswered trips the OpenAI/DeepSeek 400
182 // "An assistant message with 'tool_calls' must be followed by tool messages
183 // responding to each 'tool_call_id'".
184 const interruptedToolResult = "[no result: the previous turn was interrupted before this tool call completed]"
185
186 // SanitizeToolPairing is the provider-side alias for NormalizeMessages. It repairs
187 // a history so it satisfies the tool-call contract the OpenAI-compatible and
188 // Anthropic APIs enforce (every assistant tool_calls answered, no orphan tool
189 // messages, truncated args closed) right before sending it to the wire — without
190 // touching the stored session. Kept as a distinct name so call sites read as
191 // "defensive wire prep" rather than "session mutation".
192 func SanitizeToolPairing(msgs []Message) []Message { return NormalizeMessages(msgs) }
1 // Package anthropic implements the Anthropic Messages API provider (POST
2 // /v1/messages, SSE streaming) with a hand-written net/http client — no SDK. It
3 // self-registers under the "anthropic" kind, so any Claude model is a config
4 // instance rather than code.
5 //
6 // Two notes, both rooted in the transport-agnostic provider.Message abstraction:
7 //
8 // - Extended thinking is opt-in (provider config thinking="adaptive"). Anthropic
9 // requires the *signed* thinking block be replayed on the next turn when a tool
10 // call followed thinking, so Message carries ReasoningSignature alongside
11 // ReasoningContent and this provider replays the signed block on the next
12 // request. DeepSeek's Anthropic endpoint instead uses unsigned thinking blocks,
13 // thinking.type enabled|disabled, and output_config.effort; requests carrying
14 // tools must replay all provider reasoning. Some other compatible gateways such
15 // as LongCat use the binary toggle without output_config. (redacted_thinking
16 // blocks are not yet captured/replayed.)
17 // - Native Anthropic requests omit temperature/top_p. Current Claude models
18 // (Opus 4.8/4.7) reject sampling parameters with a 400; Anthropic steers
19 // behavior via prompting instead. DeepSeek's compatible endpoint accepts the
20 // caller's temperature, so that field is preserved only for DeepSeek.
19 // Compaction is a low-frequency cache-reset point: the prompt grows append-only
20 // (high cache hits) until a turn nears compactRatio of the window, then it is
21 // compacted down to a tail budget. The budget is a fixed token count, not a
22 // fraction of the window, so a huge window still compacts rarely while a small
23 // one still lands below the trigger (which is what stops the re-compaction loop).
24 const (
25 defaultSoftCompactRatio = 0.5 // report growing context here, but keep the cache-stable prefix intact
26 defaultToolResultSnipRatio = 0.6 // rewrite stale tool results cheaply before summary compaction
27 defaultCompactRatio = 0.8 // trigger: prompt at this fraction of the window compacts
28 defaultCompactForceRatio = 0.9 // force compaction at this high-water mark even for low-value folds
29 defaultCompactTarget = 0.5 // safety cap: the kept tail never exceeds this fraction of the window
30 defaultTailTokens = 16384 // verbatim recent-tail budget, in tokens
31 minRecentKeep = 2 // never keep fewer recent messages than this
32 minCompactMessages = 2 // skip compaction below this many compactable messages
33 fallbackTokPerChar = 0.25 // ~4 chars/token, used before any usage is available to calibrate
34 maxPinnedFirstUserTokens = 1500 // ceiling on pinning the first user turn verbatim; larger first turns (pasted content) stay foldable
35 pinnedFirstUserWindowFrac = 0.15 // and never pin a first turn worth more than this fraction of the window
36 )
摘要 schema 本身是 Harness 的恢复协议,应该有 golden tests,不能完全交给模型自由发挥。
关键源码 · Prompt
internal/agent/compact.go · L49–L80
49 // summarySystemPrompt steers the executor to distill older history into a
50 // structured briefing it can keep relying on after the originals are dropped.
51 // The section layout mirrors what a coding agent actually needs to resume work
52 // mid-task: the goal verbatim, the concrete state of the code, and an explicit
53 // next step — so the post-compaction turn doesn't lose the thread or re-derive
54 // decisions already made.
55 const summarySystemPrompt = `You are compacting the earlier part of a coding agent's conversation to save context.
56 The agent keeps your summary alongside the user's own turns (kept verbatim) and the recent tail; your job is to fold the assistant/tool work into a briefing it can resume from.
57 Write under these exact headings, omitting a heading only if it has no content:
58
59 ## Standing facts & constraints
60 Everything the user stated that still governs the work — names, paths, IDs, versions, tokens, preferences, and hard "never do X" rules — in their own words. Be exhaustive; this is the durable contract, so prefer over- to under-including.
61
62 ## Goal
63 The user's request and intent.
64
65 ## Decisions & rationale
66 Key choices made so far and why — so they are not re-litigated or reversed.
67
68 ## Files & code
69 Files read or modified, with the specific facts that matter: signatures, line locations, data shapes, and exact edits applied. Be concrete; this is what lets the agent act without re-reading everything.
70
71 ## Commands & outcomes
72 Commands run (builds, tests, git) and their relevant results — what passed, what failed, and the error text that matters.
73
74 ## Errors & fixes
75 Problems hit and how they were resolved (or not), so the same dead ends are not repeated.
76
77 ## Pending & next step
78 What is still in progress or unstarted, and the single most concrete next action to take.
79
80 Rules: be terse — bullet points and fragments, not prose. Preserve identifiers, paths, and numbers exactly. Do NOT invent anything not present in the messages; if something is unknown, leave it out rather than guessing.`
12 // Set is everything memory loaded for one session: the hierarchical docs and a
13 // handle to the auto-memory store (whose index is captured at load time). It is
14 // assembled once at boot and folded into the system prompt by Compose. CWD and
15 // UserDir are retained so the controller can resolve quick-add targets without
16 // re-deriving discovery context.
17 type Set struct {
18 Docs []Source // REASONIX.md / AGENTS.md, ascending precedence
19 GlobalGuidance []Memory // stable snapshot of global user/feedback bodies
20 Store Store // auto-memory store (may be a zero/disabled Store)
21 Index string // MEMORY.md contents at load time
22 CWD string // project working dir used for discovery
23 UserDir string // user config root (may be "")
24 InstructionDiagnostics []instruction.Diagnostic
25 }
26
27 // Options configures discovery. CWD defaults to "." and UserDir is the user
28 // config root (config.MemoryUserDir()); a "" UserDir disables user-global docs
29 // and the auto-memory store.
30 type Options struct {
31 CWD string
32 UserDir string
33 }
34
35 // Load discovers all memory for a session: the hierarchical docs and the
… 8 lines omitted; exact range 12–53 …
44 resolved := instruction.Resolve(instruction.ResolveOptions{TargetDir: cwd, UserDir: opts.UserDir})
45 return &Set{
46 Docs: resolved.Documents,
47 GlobalGuidance: store.globalGuidanceForProject(),
48 Store: store,
49 Index: store.Index(),
50 CWD: cwd,
51 UserDir: opts.UserDir,
52 InstructionDiagnostics: resolved.Diagnostics,
53 }
272 // applyDeliveryPolicyGates enforces delivery-profile bash and criteria rules and
273 // classifies whether the call mutates workspace state.
274 func (a *Agent) applyDeliveryPolicyGates(plan *toolCallPlan) (toolOutcome, bool) {
275 if a.deliveryProfile && plan.evidenceName == "bash" && evidence.BashToolCallMasksVerificationExit(plan.evidenceArgs) {
276 return toolOutcome{
277 output: "blocked: the trailing echo/printf of $? masks the verifier's exit status, so this command would look successful even when the check failed. Run the verifier or read-only extraction pipeline by itself and let its exit status be the tool result; for example: tail ... | head ... | node --check -",
278 blocked: true,
279 errMsg: "blocked: verification exit status masked",
280 }, true
281 }
282 if a.deliveryProfile && plan.evidenceName == "bash" && evidence.BashToolCallMixesMutationAndVerification(plan.evidenceArgs) {
283 return toolOutcome{
284 output: "blocked: this command mixes a verification check with a segment that may write state. Run the state-changing preparation separately while a todo is in_progress, then run a read-only verification command. For generated input, prefer a host-recognized read-only pipeline into the verifier (for example: tail ... | head ... | node --check -) instead of writing a temporary file.",
285 blocked: true,
286 errMsg: "blocked: mixed mutation and verification command",
287 }, true
288 }
289 if a.deliveryProfile && plan.evidenceName == "bash" && evidence.BashToolCallUsesOpaqueInlineInterpreter(plan.evidenceArgs) {
290 return toolOutcome{
291 output: "blocked: delivery mode cannot audit inline interpreter source such as node -e or python -c, so executing it would become an opaque mutation and invalidate prior verification. For inspection, use read_file/grep or another host-proven read-only command. For validation, use a conventional verifier such as node --check, a project test/check/lint command, or a read-only extraction pipeline into the verifier. For an intentional state change, use a file tool or a script file under the current in_progress todo. " + evidence.VerificationCommandSummary(),
292 blocked: true,
293 errMsg: "blocked: opaque inline interpreter command",
294 }, true
295 }
… 7 lines omitted; exact range 272–312 …
303 }, true
304 }
305 if a.deliveryProfile && plan.mutates && !a.hasActiveCanonicalTodo() {
306 return toolOutcome{
307 output: "blocked: delivery-first mode requires every state change to belong to the current in_progress todo. Preserve the completed todo prefix, append a concrete new item if more work was discovered, mark that item in_progress with todo_write, then retry this mutation.",
308 blocked: true,
309 errMsg: "blocked: active delivery todo required",
310 }, true
311 }
312 return toolOutcome{}, false
查看全部 3 处证据
实现internal/agent/execute_one.go:272–312Delivery 的验证、混合命令、inline interpreter 与 todo gates。
工具成功不等于只把一段字符串塞回聊天:系统还保存“谁调用了谁、是否真的产出、哪个 todo 前进了、恢复守卫看到什么”。
对自研 Harness 的含义
审计与恢复需要结构化 receipt,而不是从自然语言最终答案里猜发生过什么。
关键源码 · 实现
internal/agent/execute_one.go · L552–L654
552 // finishToolExecution performs the concrete Execute, records evidence, runs
553 // post hooks and recovery observation, and truncates the model-facing result.
554 func (a *Agent) finishToolExecution(ctx context.Context, plan *toolCallPlan) toolOutcome {
555 cctx := plan.cctx
556 runTool := plan.runTool
557 runArgs := plan.runArgs
558 call := plan.call
559 t := plan.tool
560 readOnly := plan.readOnly
561 permName := plan.permName
562 permArgs := plan.permArgs
563 evidenceName := plan.evidenceName
564 evidenceArgs := plan.evidenceArgs
565 mutates := plan.mutates
566 recoveryGen := plan.recoveryGen
567
568 var result string
569 var images []string
570 var err error
571 // A call that was authorized under reader classification carries that
572 // basis into dispatch: the MCP execution layer re-verifies it linearizably
573 // against server authorization and live safety metadata, and refuses to
574 // promote it into a writer lane if reclassification landed after the gate.
575 if readOnly && isInstalledMCPTool(runTool) && mcpServerAuthorized(runTool) && !mcpDestructiveHint(runTool) {
… 69 lines omitted; exact range 552–654 …
645 a.recordRepeatSuccess(call, t)
646 // A foreground `task` sub-agent just finished — its result is the final answer.
647 // (A backgrounded one returns a "Started…" string and stops later in a job, so
648 // it doesn't fire here.) SubagentStop lets a hook react to delegated work.
649 if a.hooks != nil && call.Name == "task" && !isBackgroundTaskCall(call.Arguments) {
650 a.hooks.SubagentStop(ctx, result)
651 }
652 body, truncMsg := truncateToolOutput(result)
653 return toolOutcome{output: body, images: images, truncated: truncMsg != "", truncMsg: truncMsg, recoveryGeneration: recoveryGen}
654 }
1 // Package sandbox wraps a shell command in an OS-level jail so the model's
2 // `bash` calls are confined: it may read almost freely but write only inside
3 // the writable roots (workspace, configured extras, plus temp and toolchain
4 // caches), with optional forbid-read roots, and reach the network only when
5 // allowed. This is the *enforcement* layer beneath the permission rules
6 // (*policy*): a permitted command still cannot escape the box.
7 //
8 // macOS uses Seatbelt via sandbox-exec and Linux uses bubblewrap when available.
9 // Windows does not currently provide an OS-level bash sandbox and resolves the
10 // product setting to off. When enforce is requested but no OS sandbox backend
11 // is available, the bash tool fails closed instead of running the command
12 // unwrapped.
13 // Confining the in-process file-writer built-ins is handled separately, in
14 // package tool/builtin.
1 // Package permission decides, per tool call, whether to allow it, deny it, or
2 // ask the user first. The core is a pure Policy (rule evaluation, no I/O); a
3 // Gate wraps a Policy with an optional interactive Approver and is what the
4 // agent consults at execute time. Keeping rule evaluation pure makes it
5 // trivially testable and keeps the agent independent of how "ask" is resolved.
200 cfg, err := config.LoadForRoot(root)
201 if err != nil {
202 return nil, err
203 }
204 applyRuntimeAutoPricingCurrency(cfg, opts.AutoPricingCurrency)
205 // Arm the credential-protection layers from the user-global [secrets]
206 // section before any tool, hook, or plugin subprocess can spawn. Package
207 // globals are correct here because [secrets] is user-global (project
208 // reasonix.toml cannot override it), so concurrent workspaces agree.
209 secrets.SetFilterSubprocessEnv(cfg.Secrets.FilterSubprocessEnv)
210 secrets.SetProtectSensitiveFiles(cfg.Secrets.ProtectSensitiveFiles)
211 secrets.RegisterCredentialEnvKeys(cfg.CredentialEnvNames())
1 // Package plugin is Reasonix's MCP client. It connects to external MCP servers and
2 // adapts their tools to the tool.Tool interface, so the agent treats plugin
3 // tools and built-ins uniformly. The wire protocol is JSON-RPC 2.0 in every
4 // case; only the transport differs (stdio subprocess, Streamable HTTP, or the
5 // legacy HTTP+SSE). A transport interface hides that difference so the MCP-level
6 // logic — handshake, tools/list, tools/call — is written once.
7 package plugin
扩展生态的启动成本应被当成调度问题,采用 schema cache、lazy process 和 bounded startup,而不是启动时全量阻塞。
关键源码 · 契约
internal/plugin/plugin.go · L243–L279
243 // StartPolicy tunes batch plugin startup. The zero value disables every safeguard,
244 // so most call sites should use the StartAll / StartAvailable wrappers, which
245 // fill in production defaults.
246 type StartPolicy struct {
247 // PerPluginTimeout caps how long a single plugin's handshake (start +
248 // initialize + listTools + listPrompts/Resources) may take. Zero disables.
249 // Exceeded plugins are recorded as failures and, when AbortOnError is set,
250 // tear down the whole batch with the timeout as the cause.
251 PerPluginTimeout time.Duration
252
253 // Concurrency caps how many handshakes run at once. Zero or negative means
254 // no cap (every plugin gets a goroutine immediately). A small cap prevents
255 // process storms / FD exhaustion when many MCP servers are configured.
256 Concurrency int
257
258 // AbortOnError makes any single failure tear down the partial batch and
259 // return an error (StartAll semantics). When false, failures are recorded
260 // on the host and other plugins keep going (StartAvailable semantics).
261 AbortOnError bool
262
263 // SkipPersistence disables RecordStartup / SaveCachedSchema side effects.
264 // Use for read-only live probes (capability diagnostics) that must not
265 // write MCP stats or schema cache files under Reasonix home.
266 SkipPersistence bool
… 3 lines omitted; exact range 243–279 …
270 // Eight is the standard "process storm" guardrail (Bazel's --jobs=auto, most LSP
271 // managers) — large enough to mask single-plugin latency, small enough to spare
272 // a workstation with 20+ configured MCP servers from fork-bombing itself.
273 const defaultStartConcurrency = 8
274
275 // defaultStartTimeout is the per-plugin budget used by StartAvailable. Five
276 // seconds covers a healthy stdio MCP spawning under a slow npm/node loader; past
277 // that, an interactive user is better served by recording the failure and moving
278 // on than by stalling the whole session.
279 const defaultStartTimeout = 5 * time.Second
10 // IndexMaxChars caps the pinned skills-index block so it can't bloat the
11 // cache-stable system-prompt prefix; bodies never enter the prefix.
12 const IndexMaxChars = 4000
13
14 const missingDescPlaceholder = `(no description — frontmatter is missing a "description:" line; tell the user to add one)`
15
16 // indexHeader introduces the skills block in the system prompt: the invocation
17 // policy (mandatory for inline, judgment-based for subagent) and how to call one.
18 const indexHeader = "# Skills — playbooks you can invoke\n\n" +
19 "One-liner index. Before non-trivial work, scan it: if an untagged (inline) skill is even plausibly relevant to the task, invoke it before continuing instead of pre-judging — loading one imperfect inline skill is cheap. Skills tagged `[🧬 subagent]` are the heavy path; reach for them only when the task genuinely needs context-heavy work, not on weak relevance. Each entry is a built-in or a user-authored playbook. Call `run_skill({ name: \"<skill-name>\", arguments: \"<task>\" })` — `name` is JUST the identifier (e.g. `\"explore\"`), NOT the `[🧬 subagent]` tag that follows it. Prefer the dedicated top-level tool when one exists for a built-in subagent skill. Entries tagged `[🧬 subagent]` spawn an isolated subagent — its tool calls and reasoning never enter your context, only its final answer does; use them for context-heavy work (deep exploration, multi-step research) where you only need the conclusion. Untagged skills are inlined: the body becomes a tool result you read and act on directly. The user can also invoke a skill via `/<name>`."
20
21 const readOnlyIndexHeader = "# Skills — read-only playbooks you can invoke\n\n" +
22 "One-liner index for the narrow read-only skill surface. Call `read_only_skill({ name: \"<skill-name>\", arguments: \"<task>\" })` — `name` is JUST the identifier, NOT the `[🧬 subagent]` tag. Inline skills are loaded into context. Skills tagged `[🧬 subagent]` run in an isolated ephemeral read-only subagent with only read-only research tools and safe foreground bash; no writes, installers, memory mutation, continuation/fork, background jobs, or writer-capable delegation are available. Read-only nested delegation may be available until max_subagent_depth is reached."
23
24 // IndexBlock renders the system/tool-result skills listing without attaching it
25 // to a base prompt. Only names + descriptions (+ a subagent tag) are listed;
26 // bodies load on demand via run_skill.
27 func IndexBlock(skills []Skill) string {
28 return indexBlockWithHeader(indexHeader, skills)
12 // Set is everything memory loaded for one session: the hierarchical docs and a
13 // handle to the auto-memory store (whose index is captured at load time). It is
14 // assembled once at boot and folded into the system prompt by Compose. CWD and
15 // UserDir are retained so the controller can resolve quick-add targets without
16 // re-deriving discovery context.
17 type Set struct {
18 Docs []Source // REASONIX.md / AGENTS.md, ascending precedence
19 GlobalGuidance []Memory // stable snapshot of global user/feedback bodies
20 Store Store // auto-memory store (may be a zero/disabled Store)
21 Index string // MEMORY.md contents at load time
22 CWD string // project working dir used for discovery
23 UserDir string // user config root (may be "")
24 InstructionDiagnostics []instruction.Diagnostic
查看全部 3 处证据
契约internal/memory/memory.go:12–24Docs、GlobalGuidance、Store 与 Index 的分层数据结构。
实现internal/memory/memory.go:131–155背景事实的低权语义与 stale warning。
36 // DefaultPlannerPrompt steers the planner toward concise plans, not execution.
37 const DefaultPlannerPrompt = `You are the planner in a two-model coding agent.
38 Given a task, produce a concise, ordered plan for the executor model to carry out.
39 Use the read-only tools available to you when the task needs context from the
40 workspace, user rules, or docs; keep that research targeted and stop once you
41 have enough evidence. Do not write full implementations or attempt side effects.
42 Do not ask the user how to trigger the executor and do not say you are waiting
43 for the executor. Output executor-ready instructions: what to do, which files or
44 commands are relevant, expected blockers, and key decisions. Keep it short and
45 actionable.
46
47 A host-authored <planner-turn> block at the end of the user turn selects the
48 planning depth. For depth=light, return a compact objective, 1-4 ordered steps,
49 likely touchpoints, and the main verification; omit empty boilerplate sections.
50 For depth=full, inspect enough evidence to distinguish verified touchpoints from
51 candidate touchpoints, then include goal/non-goals when useful, ordered steps,
52 risks or blockers, concrete acceptance criteria, command-level verification, and
53 rollback only when the change is risky or difficult to reverse. Label assumptions
54 instead of presenting inferred paths or commands as verified facts.
55
56 If execution must stop for explicit user approval of the plan, end the plan with
57 a final line containing exactly [planner_requires_approval]. If execution needs
58 a user-owned decision or missing user-provided value before it can be safe, do
59 not ask in prose; include one structured block:
… 12 lines omitted; exact range 36–81 …
72 When you need external real data and the capability route does not name a
73 specific tool, call use_capability(action="list") first to see configured MCP
74 servers, then inspect or call a non-destructive capability. If a capability is
75 destructive, do not treat that as missing configuration or an unavailable MCP:
76 write the operation into the plan for the executor instead.
77
78 If your research shows the task needs no changes and no actions at all (already
79 implemented, already resolved), explain that briefly and end your reply with a
80 final line containing exactly [no_changes]. Never emit that marker when any
81 work, verification, or follow-up remains.`
756 // The `task` tool spawns sub-agents that reuse the parent's provider and
757 // tool registry. Wired here after the built-ins / plugins are loaded so
758 // sub-agents inherit the full tool set (minus `task` itself, to keep
759 // nesting out of the picture). It registers into the same reg the
760 // executor uses, so the model surfaces it like any other tool.
761 resolveSubagentProvider := func(modelRef, effort string) (provider.Provider, *provider.Pricing, int, error) {
762 me := *entry
763 selectedRef := modelRefFromEntry(entry)
764 if strings.TrimSpace(modelRef) != "" {
765 if resolved, ok := cfg.ResolveModel(modelRef); ok {
766 me = *resolved
767 selectedRef = modelRefFromEntry(resolved)
768 } else if opts.ProviderResolver != nil {
769 me = *syntheticEntryFromResolver(opts.ProviderResolver, modelRef)
770 selectedRef = modelRef
771 } else {
772 return nil, nil, 0, fmt.Errorf("unknown model %q", modelRef)
773 }
774 }
775 var effortOverride *string
776 if strings.TrimSpace(effort) != "" {
777 normalized, err := config.NormalizeEffort(&me, effort)
778 if err != nil {
779 if opts.ProviderResolver == nil {
26 const (
27 cleanupPendingExt = ".cleanup-pending.json"
28 maxRecoveryParentStemBytes = 80
29 sessionLockSidecarSuffix = ".jsonl.lock"
30 sessionLeaseLockSidecarSuffix = ".jsonl.lease.lock"
31 sessionLeaseInfoSidecarSuffix = ".jsonl.lease.json"
32 guardianSidecarSuffix = ".guardian.jsonl"
33 // nameMaxBytes is the single-component filename limit shared by the
34 // filesystems Reasonix targets (APFS, ext4, NTFS all cap at 255).
35 nameMaxBytes = 255
36 // maxSessionBasenameBytes bounds transcript basenames that reconciliation
37 // leaves in place. Sidecars append up to ~16 bytes to the transcript name
38 // or its stem (".lease.lock", ".cleanup-pending.json", ".guardian.jsonl"),
39 // so 224 keeps every sidecar comfortably under nameMaxBytes with headroom
40 // for future suffixes. Names past this bound come from the pre-bounded
41 // recovery cascade and get renamed by reconcileOverlongSessionFilenames.
42 maxSessionBasenameBytes = 224
43 )
44
45 var (
46 sessionSaveLocks sync.Map
47 // sessionFileLockWait bounds cross-process save-lock acquisition. Session
48 // leases normally prevent competing writers, but CLI/legacy writers and a
49 // stalled process can still hold the compatibility .lock file. Navigation
… 15 lines omitted; exact range 26–74 …
65 // forking further multiplies session files without converging (#5993).
66 ErrSessionRecoveryDepthExceeded = errors.New("session recovery chain depth exceeded")
67 sessionWriterID = newSessionWriterID()
68 )
69
70 // SessionRecoveryMaxDepth bounds nested recovery forks: a normal session may
71 // fork a recovery branch (depth 1), which may itself fork twice more under
72 // genuine repeated incidents; past that the caller should stop forking and
73 // write onto the branch it already owns.
74 const SessionRecoveryMaxDepth = 3
查看全部 3 处证据
契约internal/agent/save.go:26–74sidecar、锁、recovery depth cap 与冲突 sentinel。
348 // OutputBytes is the host-observed length of the tool's (redacted, trimmed)
349 // output. Content-evidence checks require it to be non-zero so a command
350 // that printed nothing (head -n 0, >/dev/null) can never count as reading.
351 OutputBytes int `json:"output_bytes,omitempty"`
352 }
353
354 // BackgroundLease identifies a background job whose evidence was provisionally
355 // merged into the current turn's ledger. The host commits these leases only
356 // after the turn passes its delivery gates, so a failed turn leaves the job's
357 // evidence collectable again.
358 type BackgroundLease struct {
359 Session string
360 JobID string
361 }
362
363 // DeliveryCheckpoint is the compact, persistence-safe state carried across
364 // runs of one host-owned Goal. It intentionally stores no raw tool arguments or
365 // output. PendingMutation means a previously observed change still needs fresh
366 // verification, review, and sign-off before the Goal can finalize.
367 type DeliveryCheckpoint struct {
368 ScopeID string `json:"scopeID,omitempty"`
369 CriteriaEstablished bool `json:"criteriaEstablished,omitempty"`
370 WorkObserved bool `json:"workObserved,omitempty"`
371 MutationObserved bool `json:"mutationObserved,omitempty"`
372 PendingMutation bool `json:"pendingMutation,omitempty"`
373 }
68 // TestRunMultiToolRoundEmptyIDsSurvivePairing drives the real loop through a turn
69 // that fans out two tool calls carrying no id (a gateway that streams by index),
70 // then asserts both results still pair back after SanitizeToolPairing — the repair
71 // that runs on every send. Keying on tool_call_id alone collapsed them into one,
72 // dropping a result from the model's context on the very next turn.
73 func TestRunMultiToolRoundEmptyIDsSurvivePairing(t *testing.T) {
74 mp := testutil.NewMock("m",
75 testutil.Turn{ToolCalls: []provider.ToolCall{
76 {ID: "", Name: "echo", Arguments: `{"text":"alpha"}`},
77 {ID: "", Name: "echo", Arguments: `{"text":"beta"}`},
78 }},
79 testutil.Turn{Text: "done"},
80 )
81 a := New(mp, echoRegistry(), NewSession(""), Options{}, event.Discard)
82 if err := a.Run(context.Background(), "go"); err != nil {
83 t.Fatalf("Run: %v", err)
84 }
85
86 repaired := provider.SanitizeToolPairing(a.Session().Messages)
87 var results []string
88 for _, m := range repaired {
89 if m.Role == provider.RoleTool {
90 results = append(results, m.Content)
91 }
… 1 lines omitted; exact range 68–102 …
93 if len(results) != 2 {
94 t.Fatalf("want 2 tool results after pairing, got %d: %v", len(results), results)
95 }
96 if results[0] == results[1] {
97 t.Fatalf("both results collapsed to %q — one was lost from the model's context", results[0])
98 }
99 if !strings.Contains(results[0], "alpha") || !strings.Contains(results[1], "beta") {
100 t.Errorf("results lost their identity: %v", results)
101 }
102 }
查看全部 3 处证据
测试internal/agent/loop_e2e_test.go:68–102空 ID 多工具调用的真实 loop pairing。