diff --git a/internal/assembly/boot/agent_preset.go b/internal/assembly/boot/agent_preset.go index 4ec14267ff..a9874e58a5 100644 --- a/internal/assembly/boot/agent_preset.go +++ b/internal/assembly/boot/agent_preset.go @@ -54,9 +54,7 @@ func CoreProviderToolNames() []string { "read_file", "edit_file", "write_file", - "compress", "recall", - "context_budget", "use_capability", } } @@ -72,7 +70,6 @@ func HostControlToolNames() []string { "update_goal", "todo_write", "complete_step", - "setting", } } @@ -158,7 +155,7 @@ func applyUnifiedProviderToolSurface(reg *tool.Registry, goalTurnsUnreachable bo } // advise, best_of_n, run_script, the peer tools and read_mcp_resource are // registered only when config asks, at boot; present_files follows the same rule. - for _, name := range []string{advisor.Name, script.Name, "list_peers", "send_peer_message", "dispatch_to_project", "open_projects", "view_image", "present_files", plugin.ResourceReadToolName} { + for _, name := range []string{advisor.Name, script.Name, "list_peers", "send_peer_message", "dispatch_to_project", "open_projects", "view_image", plugin.ResourceReadToolName} { if _, ok := reg.Get(name); ok { allow = append(allow, name) } diff --git a/internal/assembly/boot/boot_test.go b/internal/assembly/boot/boot_test.go index ccfa9ef408..73b20d6045 100644 --- a/internal/assembly/boot/boot_test.go +++ b/internal/assembly/boot/boot_test.go @@ -2500,12 +2500,13 @@ command = "reasonix-missing-mockmcp" } req := reqs[0] assertSegmentedSurface(t, "light first request", toolSchemaNames(req.Tools)) - for _, want := range []string{"compress", "use_capability", "read_file", "edit_file", "write_file", "bash", "ask"} { + for _, want := range []string{"use_capability", "read_file", "edit_file", "write_file", "bash", "ask"} { if !requestHasTool(req, want) { t.Fatalf("light first request missing tool %q; tools=%v", want, toolSchemaNames(req.Tools)) } } for _, forbidden := range []string{ + "compress", "context_budget", "setting", "present_files", "browser_open", "connect_tool_source", "web_fetch", "task", "read_only_task", "read_only_skill", "run_skill", "read_skill", "install_skill", "install_source", "explore", "research", "review", "security_review", "lsp_definition", "lsp_references", "lsp_hover", "lsp_diagnostics", diff --git a/internal/assembly/boot/context_budget_effect_test.go b/internal/assembly/boot/context_budget_effect_test.go index 27b6ac68c6..758f17202b 100644 --- a/internal/assembly/boot/context_budget_effect_test.go +++ b/internal/assembly/boot/context_budget_effect_test.go @@ -146,9 +146,12 @@ func TestEffectContextBudgetToolAnswersUnderPressure(t *testing.T) { } } -func TestEffectContextBudgetToolIsOffered(t *testing.T) { +func TestEffectContextBudgetToolIsIndexedNotOffered(t *testing.T) { reqs := effectRun(t, "boot-budget-tool-surface", "", ablation.Set{}) - if !toolNames(reqs[0])["context_budget"] { - t.Fatalf("context_budget absent from the provider tool surface: %v", toolNames(reqs[0])) + if toolNames(reqs[0])["context_budget"] { + t.Fatalf("context_budget is in the first-round schema: %v", toolNames(reqs[0])) + } + if !strings.Contains(capabilityDescription(reqs[0]), "tool:context_budget") { + t.Fatalf("use_capability does not index context_budget: %s", capabilityDescription(reqs[0])) } } diff --git a/internal/assembly/boot/effect_present_files_test.go b/internal/assembly/boot/effect_present_files_test.go index a1c408b730..b1457afd77 100644 --- a/internal/assembly/boot/effect_present_files_test.go +++ b/internal/assembly/boot/effect_present_files_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "path/filepath" "reflect" + "strings" "testing" "time" @@ -135,24 +136,14 @@ func presentFilesEffect(t *testing.T, proxy bool, mode string) { t.Fatal("disabled presenter leaked into the first provider request") } } - count = 0 + // The presenter is deferred: never a schema of its own, indexed by + // use_capability's description exactly when it is enabled. for _, schema := range requests[0].Tools { - if schema.Name != want.Name() { - continue - } - count++ - var gotSchema, wantSchema any - if err := json.Unmarshal(schema.Parameters, &gotSchema); err != nil { - t.Fatal(err) - } - if err := json.Unmarshal(provider.CanonicalizeSchema(want.Schema()), &wantSchema); err != nil { - t.Fatal(err) - } - if schema.Description != want.Description() || !reflect.DeepEqual(gotSchema, wantSchema) { - t.Fatalf("unexpected provider contribution: %+v", schema) + if schema.Name == want.Name() { + t.Fatalf("present_files is in the first-round schema: %+v", schema) } } - if (count == 1) != enabled || count > 1 { - t.Fatalf("provider contribution = %d, enabled = %v", count, enabled) + if indexed := strings.Contains(capabilityDescription(requests[0]), "tool:"+want.Name()); indexed != enabled { + t.Fatalf("use_capability indexes present_files = %v, enabled = %v", indexed, enabled) } } diff --git a/internal/assembly/boot/effect_setting_tool_test.go b/internal/assembly/boot/effect_setting_tool_test.go index 4597b214f0..6fdaa29c66 100644 --- a/internal/assembly/boot/effect_setting_tool_test.go +++ b/internal/assembly/boot/effect_setting_tool_test.go @@ -36,8 +36,8 @@ func (p *settingProvider) Stream(_ context.Context, req provider.Request) (<-cha p.mu.Unlock() ch := make(chan provider.Chunk, 3) if first { - args, _ := json.Marshal(map[string]any{"op": "propose", "id": p.id, "value": json.RawMessage(p.val), "reason": "the task needs it"}) - ch <- provider.Chunk{Type: provider.ChunkToolCall, ToolCall: &provider.ToolCall{ID: "call-set", Name: "setting", Arguments: string(args)}} + args, _ := json.Marshal(map[string]any{"action": "call", "capability_id": "tool:setting", "arguments": map[string]any{"op": "propose", "id": p.id, "value": json.RawMessage(p.val), "reason": "the task needs it"}}) + ch <- provider.Chunk{Type: provider.ChunkToolCall, ToolCall: &provider.ToolCall{ID: "call-set", Name: "use_capability", Arguments: string(args)}} } else { ch <- provider.Chunk{Type: provider.ChunkText, Text: "done"} } @@ -53,7 +53,7 @@ func (p *settingProvider) toolResult() string { return "" } for _, m := range p.reqs[1].Messages { - if m.Role == provider.RoleTool && m.Name == "setting" { + if m.Role == provider.RoleTool && m.Name == "use_capability" { return m.Content } } @@ -152,8 +152,8 @@ func TestEffectTheModelProposesASettingAndTheUserClickDecidesIt(t *testing.T) { if !reflect.DeepEqual(rec.toolNames(0), rec.toolNames(1)) { t.Fatalf("the tool block moved between requests:\n%v\n%v", rec.toolNames(0), rec.toolNames(1)) } - if !slices.Contains(rec.toolNames(0), "setting") { - t.Fatalf("setting is not offered: %v", rec.toolNames(0)) + if slices.Contains(rec.toolNames(0), "setting") { + t.Fatalf("setting is in the first-round schema: %v", rec.toolNames(0)) } }) } diff --git a/internal/assembly/boot/testdata/golden/prefix_shape.json b/internal/assembly/boot/testdata/golden/prefix_shape.json index 2d306983de..90650ce7ff 100644 --- a/internal/assembly/boot/testdata/golden/prefix_shape.json +++ b/internal/assembly/boot/testdata/golden/prefix_shape.json @@ -1,7 +1,7 @@ { "SystemHash": "bc08dbc97c837bba", - "ToolsHash": "32d919766a6d1e78", - "PrefixHash": "dfa7eda18c0ad7c0", + "ToolsHash": "b779ff9e4407548b", + "PrefixHash": "fc00c9d2f68d114f", "LogRewriteVersion": 0, - "ToolSchemaTokens": 5032 + "ToolSchemaTokens": 4305 } diff --git a/internal/assembly/boot/testdata/golden/provider_request.json b/internal/assembly/boot/testdata/golden/provider_request.json index 6753ee0099..23e210e1f3 100644 --- a/internal/assembly/boot/testdata/golden/provider_request.json +++ b/internal/assembly/boot/testdata/golden/provider_request.json @@ -207,49 +207,6 @@ "type": "object" } }, - { - "name": "compress", - "description": "Compress a selected part of the current model-visible conversation without deleting visible history. Use only when the user explicitly asks for context compression. Choose `before` to summarize everything before the uniquely matched user turn while keeping that turn and later context, or `after` to summarize from that turn through the last completed turn while keeping the active turn. The anchor must be an exact, unique excerpt from a real user message; use a longer excerpt if the tool reports multiple matches.", - "parameters": { - "additionalProperties": false, - "properties": { - "anchor": { - "description": "An exact, unique excerpt from one real user message in the current model-visible conversation.", - "maxLength": 512, - "minLength": 1, - "type": "string" - }, - "direction": { - "description": "Which side of the anchor user turn to compress.", - "enum": [ - "before", - "after" - ], - "type": "string" - }, - "focus": { - "description": "Optional guidance about facts or decisions the summary must preserve.", - "maxLength": 2000, - "type": "string" - } - }, - "required": [ - "anchor", - "direction" - ], - "type": "object" - } - }, - { - "name": "context_budget", - "description": "Report how much context room is left before this conversation is automatically compacted. Nothing reports it unprompted: check it at the start of a long task, before a large change, and before output you may not finish reading (a broad search, a large file, a long build log), so you can narrow the command. `tokens_remaining` counts down to the compaction trigger, not the physical window.", - "parameters": { - "additionalProperties": false, - "properties": {}, - "required": [], - "type": "object" - } - }, { "name": "edit_file", "description": "Replace an exact string in a file with another. old_string must occur exactly once; add surrounding context to disambiguate. Use for targeted edits instead of rewriting the whole file.", @@ -292,41 +249,6 @@ "type": "object" } }, - { - "name": "present_files", - "description": "Present existing final files as clickable cards, including shell-created documents. Paths must be in the workspace or authorized folders. Opens current files, not snapshots; does not certify correctness.", - "parameters": { - "additionalProperties": false, - "properties": { - "files": { - "items": { - "additionalProperties": false, - "properties": { - "description": { - "maxLength": 256, - "type": "string" - }, - "path": { - "maxLength": 4096, - "type": "string" - } - }, - "required": [ - "path" - ], - "type": "object" - }, - "maxItems": 4, - "minItems": 1, - "type": "array" - } - }, - "required": [ - "files" - ], - "type": "object" - } - }, { "name": "read_file", "description": "Read a text file with optional line offset/limit. Output prefixes each line with its 1-based number (e.g. ` 42→...`) so subsequent edit_file calls can target exact lines. Use `offset` and `limit` to page through large files; the tool reports total length and pagination hints in a trailer.", @@ -384,42 +306,6 @@ "type": "object" } }, - { - "name": "setting", - "description": "Find and change the user's own settings. op=list finds settings by words of their id (for example \"peers dispatch\"). op=propose asks the user to confirm one change: give the id, the new value and a short reason; nothing changes until the user answers, and the result says what changed or that they declined. op=open points the user at a setting that only they may change. Use it when a refusal ends with (fix: setting ...), or when a tool you need is not offered because a switch is off. Do not change a setting the task did not call for.", - "parameters": { - "additionalProperties": false, - "properties": { - "id": { - "description": "The setting's id, for propose and open", - "type": "string" - }, - "op": { - "enum": [ - "list", - "propose", - "open" - ], - "type": "string" - }, - "query": { - "description": "Words of the setting's id, for op=list", - "type": "string" - }, - "reason": { - "description": "Why the change helps the user, shown on the card, for op=propose", - "type": "string" - }, - "value": { - "description": "The new value as JSON, for op=propose" - } - }, - "required": [ - "op" - ], - "type": "object" - } - }, { "name": "todo_write", "description": "Record and update a structured task list for the current work. Send the COMPLETE list every call — it replaces the previous one. Use it to plan multi-step work and show progress: keep exactly one item in_progress at a time, and flip an item to completed the moment it's done (don't batch completions). Skip it for trivial single-step tasks. The list is two-level: a `level` 0 item is a PHASE (a milestone) and the `level` 1 items after it are its concrete sub-steps. COPY `step_id` VERBATIM for every item that already has one: it is how a completion stays attached to its step when you retitle it, insert a step above it, or reorder the list. Give a new item a fresh unique id (e.g. \"plan_step_07\"); never reuse or renumber an existing one.", @@ -530,7 +416,7 @@ }, { "name": "use_capability", - "description": "Search and call deferred tools, skills, and MCP through stable IDs such as tool:grep, skill:review, task:subagent, and mcp-tool:server/tool. Before saying a tool, network, or integration is unavailable, search by natural-language goal; results include ranked IDs and available schemas. list enumerates the catalog, inspect loads one definition, call invokes it, and decline records why a preferred capability is skipped. task:subagent delegates 1-8 tasks that run in parallel. Calls retain all permission, sandbox, and execution checks. Deferred tools, called by id: tool:web_fetch (Read HTTP(S) text.); tool:browser_open (Load a page and return its snapshot, one element per line with [eN]…); tool:browser_read (Read a browser page: snapshot); tool:browser_act (Operate a page with real input.); tool:grep (Search for a regular expression in a file, or recursively under a…); tool:glob (Find files matching a glob pattern); tool:ls (List the entries of a directory.). Read JSON or static text with tool:web_fetch; open a browser only for pages that need rendering, clicks or a login.", + "description": "Search and call deferred tools, skills, and MCP through stable IDs such as tool:grep, skill:review, task:subagent, and mcp-tool:server/tool. Before saying a tool, network, or integration is unavailable, search by natural-language goal; results include ranked IDs and available schemas. list enumerates the catalog, inspect loads one definition, call invokes it, and decline records why a preferred capability is skipped. task:subagent delegates 1-8 tasks that run in parallel. Calls retain all permission, sandbox, and execution checks. Deferred tools, called by id: tool:web_fetch (Read HTTP(S) text.); tool:browser_open (Load a page and return its snapshot, one element per line with [eN]…); tool:browser_read (Read a browser page: snapshot); tool:browser_act (Operate a page with real input.); tool:grep (Search for a regular expression in a file, or recursively under a…); tool:glob (Find files matching a glob pattern); tool:ls (List the entries of a directory.); tool:present_files (Present existing final files as clickable cards, including…); tool:setting (Find and change the user's own settings.); tool:compress (Compress a selected part of the current model-visible conversation…); tool:context_budget (Report how much context room is left before this conversation is…). Read JSON or static text with tool:web_fetch; open a browser only for pages that need rendering, clicks or a login.", "parameters": { "properties": { "action": { diff --git a/internal/assembly/boot/testdata/golden/tool_schemas.json b/internal/assembly/boot/testdata/golden/tool_schemas.json index 000b00721c..9e1d5b636f 100644 --- a/internal/assembly/boot/testdata/golden/tool_schemas.json +++ b/internal/assembly/boot/testdata/golden/tool_schemas.json @@ -182,51 +182,6 @@ "type": "object" } }, - { - "Name": "compress", - "Description": "Compress a selected part of the current model-visible conversation without deleting visible history. Use only when the user explicitly asks for context compression. Choose `before` to summarize everything before the uniquely matched user turn while keeping that turn and later context, or `after` to summarize from that turn through the last completed turn while keeping the active turn. The anchor must be an exact, unique excerpt from a real user message; use a longer excerpt if the tool reports multiple matches.", - "ReadOnly": true, - "Schema": { - "additionalProperties": false, - "properties": { - "anchor": { - "description": "An exact, unique excerpt from one real user message in the current model-visible conversation.", - "maxLength": 512, - "minLength": 1, - "type": "string" - }, - "direction": { - "description": "Which side of the anchor user turn to compress.", - "enum": [ - "before", - "after" - ], - "type": "string" - }, - "focus": { - "description": "Optional guidance about facts or decisions the summary must preserve.", - "maxLength": 2000, - "type": "string" - } - }, - "required": [ - "anchor", - "direction" - ], - "type": "object" - } - }, - { - "Name": "context_budget", - "Description": "Report how much context room is left before this conversation is automatically compacted. Nothing reports it unprompted: check it at the start of a long task, before a large change, and before output you may not finish reading (a broad search, a large file, a long build log), so you can narrow the command. `tokens_remaining` counts down to the compaction trigger, not the physical window.", - "ReadOnly": true, - "Schema": { - "additionalProperties": false, - "properties": {}, - "required": [], - "type": "object" - } - }, { "Name": "edit_file", "Description": "Replace an exact string in a file with another. old_string must occur exactly once; add surrounding context to disambiguate. Use for targeted edits instead of rewriting the whole file.", @@ -271,42 +226,6 @@ "type": "object" } }, - { - "Name": "present_files", - "Description": "Present existing final files as clickable cards, including shell-created documents. Paths must be in the workspace or authorized folders. Opens current files, not snapshots; does not certify correctness.", - "ReadOnly": true, - "Schema": { - "additionalProperties": false, - "properties": { - "files": { - "items": { - "additionalProperties": false, - "properties": { - "description": { - "maxLength": 256, - "type": "string" - }, - "path": { - "maxLength": 4096, - "type": "string" - } - }, - "required": [ - "path" - ], - "type": "object" - }, - "maxItems": 4, - "minItems": 1, - "type": "array" - } - }, - "required": [ - "files" - ], - "type": "object" - } - }, { "Name": "read_file", "Description": "Read a text file with optional line offset/limit. Output prefixes each line with its 1-based number (e.g. ` 42→...`) so subsequent edit_file calls can target exact lines. Use `offset` and `limit` to page through large files; the tool reports total length and pagination hints in a trailer.", @@ -366,43 +285,6 @@ "type": "object" } }, - { - "Name": "setting", - "Description": "Find and change the user's own settings. op=list finds settings by words of their id (for example \"peers dispatch\"). op=propose asks the user to confirm one change: give the id, the new value and a short reason; nothing changes until the user answers, and the result says what changed or that they declined. op=open points the user at a setting that only they may change. Use it when a refusal ends with (fix: setting ...), or when a tool you need is not offered because a switch is off. Do not change a setting the task did not call for.", - "ReadOnly": true, - "Schema": { - "additionalProperties": false, - "properties": { - "id": { - "description": "The setting's id, for propose and open", - "type": "string" - }, - "op": { - "enum": [ - "list", - "propose", - "open" - ], - "type": "string" - }, - "query": { - "description": "Words of the setting's id, for op=list", - "type": "string" - }, - "reason": { - "description": "Why the change helps the user, shown on the card, for op=propose", - "type": "string" - }, - "value": { - "description": "The new value as JSON, for op=propose" - } - }, - "required": [ - "op" - ], - "type": "object" - } - }, { "Name": "todo_write", "Description": "Record and update a structured task list for the current work. Send the COMPLETE list every call — it replaces the previous one. Use it to plan multi-step work and show progress: keep exactly one item in_progress at a time, and flip an item to completed the moment it's done (don't batch completions). Skip it for trivial single-step tasks. The list is two-level: a `level` 0 item is a PHASE (a milestone) and the `level` 1 items after it are its concrete sub-steps. COPY `step_id` VERBATIM for every item that already has one: it is how a completion stays attached to its step when you retitle it, insert a step above it, or reorder the list. Give a new item a fresh unique id (e.g. \"plan_step_07\"); never reuse or renumber an existing one.", @@ -515,7 +397,7 @@ }, { "Name": "use_capability", - "Description": "Search and call deferred tools, skills, and MCP through stable IDs such as tool:grep, skill:review, task:subagent, and mcp-tool:server/tool. Before saying a tool, network, or integration is unavailable, search by natural-language goal; results include ranked IDs and available schemas. list enumerates the catalog, inspect loads one definition, call invokes it, and decline records why a preferred capability is skipped. task:subagent delegates 1-8 tasks that run in parallel. Calls retain all permission, sandbox, and execution checks. Deferred tools, called by id: tool:web_fetch (Read HTTP(S) text.); tool:browser_open (Load a page and return its snapshot, one element per line with [eN]…); tool:browser_read (Read a browser page: snapshot); tool:browser_act (Operate a page with real input.); tool:grep (Search for a regular expression in a file, or recursively under a…); tool:glob (Find files matching a glob pattern); tool:ls (List the entries of a directory.). Read JSON or static text with tool:web_fetch; open a browser only for pages that need rendering, clicks or a login.", + "Description": "Search and call deferred tools, skills, and MCP through stable IDs such as tool:grep, skill:review, task:subagent, and mcp-tool:server/tool. Before saying a tool, network, or integration is unavailable, search by natural-language goal; results include ranked IDs and available schemas. list enumerates the catalog, inspect loads one definition, call invokes it, and decline records why a preferred capability is skipped. task:subagent delegates 1-8 tasks that run in parallel. Calls retain all permission, sandbox, and execution checks. Deferred tools, called by id: tool:web_fetch (Read HTTP(S) text.); tool:browser_open (Load a page and return its snapshot, one element per line with [eN]…); tool:browser_read (Read a browser page: snapshot); tool:browser_act (Operate a page with real input.); tool:grep (Search for a regular expression in a file, or recursively under a…); tool:glob (Find files matching a glob pattern); tool:ls (List the entries of a directory.); tool:present_files (Present existing final files as clickable cards, including…); tool:setting (Find and change the user's own settings.); tool:compress (Compress a selected part of the current model-visible conversation…); tool:context_budget (Report how much context room is left before this conversation is…). Read JSON or static text with tool:web_fetch; open a browser only for pages that need rendering, clicks or a login.", "ReadOnly": true, "Schema": { "properties": { diff --git a/internal/assembly/boot/tool_contract_surface_test.go b/internal/assembly/boot/tool_contract_surface_test.go index 8f907662c2..79c6c580b8 100644 --- a/internal/assembly/boot/tool_contract_surface_test.go +++ b/internal/assembly/boot/tool_contract_surface_test.go @@ -18,13 +18,9 @@ func stableBootToolNames() []string { "ask", "await_user", "bash", - "compress", - "context_budget", "edit_file", - "present_files", "read_file", "recall", - "setting", "todo_write", "use_capability", "write_file", diff --git a/internal/runtime/usecap/deferred_index.go b/internal/runtime/usecap/deferred_index.go index 9824350a98..31cc2adbb3 100644 --- a/internal/runtime/usecap/deferred_index.go +++ b/internal/runtime/usecap/deferred_index.go @@ -9,7 +9,7 @@ import ( // deferredIndexTools are the built-ins a model reaches for by name before it // searches: hidden from the provider schema, so without a line here a // directly visible browser is the only way it can see to read a URL. -var deferredIndexTools = []string{"web_fetch", "web_search", "browser_open", "browser_read", "browser_act", "grep", "glob", "ls"} +var deferredIndexTools = []string{"web_fetch", "web_search", "browser_open", "browser_read", "browser_act", "grep", "glob", "ls", "present_files", "setting", "compress", "context_budget"} const deferredLeadBytes = 72