feat(boot): setting, present_files, compress and context_budget move behind use_capability and are indexed in its description, cutting the first-round tool definitions by another 4 KB

This commit is contained in:
yhh committed 2026-11-06 22:34:44 -08:00
1 parent c8afd3be60
commit 339f443ffb
10 files changed
+27 -271

No files matched your search

+1 -4
View File
@@ -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)
}
+2 -1
View File
@@ -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",
@@ -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]))
}
}
@@ -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)
}
}
@@ -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))
}
})
}
+3 -3
View File
@@ -1,7 +1,7 @@
{
"SystemHash": "bc08dbc97c837bba",
"ToolsHash": "32d919766a6d1e78",
"PrefixHash": "dfa7eda18c0ad7c0",
"ToolsHash": "b779ff9e4407548b",
"PrefixHash": "fc00c9d2f68d114f",
"LogRewriteVersion": 0,
"ToolSchemaTokens": 5032
"ToolSchemaTokens": 4305
}
+1 -115
View File
@@ -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": {
+1 -119
View File
@@ -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": {
@@ -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",
+1 -1
View File
@@ -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