Skip to content

Commit cabd4fc

Browse files
author
SqlRush
committed
Return ToolSearch references
1 parent e477dbf commit cabd4fc

7 files changed

Lines changed: 275 additions & 16 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ M8 补充:command registry 现在保存本地 skill prompt template,并提
191191

192192
M8 补充:新增基础 `Skill` tool wrapper,已注册到默认内置工具集,可按官方 `skill`/`args` 输入调用项目目录发现到的本地 prompt skill,并兼容 `commandName`/`arguments` 别名;tool result 会返回 `Launching skill: ...`、structured command metadata 和 prompt expansion 生成的 meta user message,conversation runner 现在会把 `ToolResult.NewMessages` 追加进后续模型请求和 transcript。插件目录 skill 可通过工作目录发现和 display name 调用,structured content 会保留 source、loadedFrom、displayName、description、argument metadata、skillRoot、whenToUse、version、context、agent、effort、paths、contentLength 和 progressMessage,方便后续 UI/SDK/remote surface 使用。forked skill、remote/MCP/bundled skill 细节、skill prompt shell injection、slash/local command UI wiring 仍未完成。
193193

194-
M8 补充:新增基础 `ToolSearch` tool 并注册到默认内置工具集,executor 会把当前 tool registry 放入工具 metadata,`ToolSearch` 可按 name、alias、description、prompt、search hint 和 input/output schema 字段搜索当前可用工具定义,返回 BM25-ranked structured results、read-only/concurrency/destructive、input/output schema 以及 `should_defer`/`always_load`/`requires_interaction`/`strict`/cache/eager 等请求提示元数据,并兼容 `query`/`q`/`search``topn`/`limit` 等输入别名。完整 deferred/lazy tool discovery 仍未完成。
194+
M8 补充:新增基础 `ToolSearch` tool 并注册到默认内置工具集,executor 会把当前 tool registry 放入工具 metadata,`ToolSearch` 可按 name、alias、description、prompt、search hint 和 input/output schema 字段搜索当前可用工具定义,支持 `select:ToolA,ToolB` 直接选择并返回 `tool_reference` content,返回 BM25/select structured results、read-only/concurrency/destructive、input/output schema 以及 `should_defer`/`always_load`/`requires_interaction`/`strict`/cache/eager 等请求提示元数据,并兼容 `query`/`q`/`search``topn`/`limit` 等输入别名。完整 deferred/lazy tool discovery 仍未完成。
195195

196196
M8/M2 补充:Anthropic request tool 转换现在会保留 contract 的 `strict``eager_input_streaming``cache_control``should_defer`,将 deferred 工具序列化为 API `defer_loading`,并用 `always_load` 覆盖 deferred hint;API tool description 会按 description、prompt、searchHint 顺序 fallback,conversation runner 构造请求时会把 `Task` 等 deferred tool 的 strict/defer_loading 元数据带到最终请求。完整 deferred/lazy tool discovery 仍未完成。
197197

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ test/parity/ # golden tests against TS/official behavior
290290
- 本轮补充:新增 `internal/commands` registry 基础层,按官方 command 来源顺序合并 bundled/builtin-plugin/project-skill/workflow/plugin/dynamic/builtin metadata,并提供 dynamic 去重、display-name/alias 查找、hidden 过滤、SkillTool/slash-skill 过滤和 bridge-safe 判定;实际 local/local-jsx 执行、`/help`/`/skills` UI、plugin/MCP/workflow 加载仍未宣称完成。
291291
- 本轮补充:command registry 现在保存本地 skill prompt template,并提供 prompt expansion 入口,覆盖 `$ARGUMENTS`、indexed/shorthand 参数、frontmatter named arguments、`${CLAUDE_SESSION_ID}` 替换、非 MCP skill 的 `${CLAUDE_SKILL_DIR}` 替换和 meta user message 输出;shell injection、SkillTool wrapper、local/local-jsx 执行和 REPL/UI wiring 仍未宣称完成。
292292
- 本轮补充:新增基础 `Skill` tool wrapper 并注册到默认内置工具集,可调用本地项目 prompt skill、兼容官方 `skill`/`args` 与相邻别名,并把 prompt expansion 产生的 meta user message 通过 `ToolResult.NewMessages` 交给 conversation runner;runner 现在会把这些新消息写入 transcript 并追加到下一轮模型请求。插件目录 skill 可通过工作目录发现和 display name 调用,Skill tool structured content 会保留 source、loadedFrom、displayName、description、argument metadata、skillRoot、whenToUse、version、context、agent、effort、paths、contentLength 和 progressMessage。forked/remote/MCP/bundled skills、plugin skill UI/SDK polish、shell injection、slash/local command UI wiring 仍未宣称完成。
293-
- 本轮补充:新增基础 `ToolSearch` tool 并注册到默认内置工具集,executor 会把当前 tool registry 注入工具 metadata;`ToolSearch` 可按 name、alias、description、prompt、search hint 搜索当前工具定义,返回 BM25-ranked structured results 和 read-only/concurrency/destructive 以及 `should_defer`/`always_load`/`requires_interaction`/`strict`/cache/eager 等请求提示元数据,并兼容 query/limit 相邻输入别名。deferred/lazy tool discovery 仍未宣称完成。
293+
- 本轮补充:新增基础 `ToolSearch` tool 并注册到默认内置工具集,executor 会把当前 tool registry 注入工具 metadata;`ToolSearch` 可按 name、alias、description、prompt、search hint 和 input/output schema 字段搜索当前工具定义,支持 `select:ToolA,ToolB` 直接选择并返回 `tool_reference` content,返回 BM25/select structured results 和 read-only/concurrency/destructive、input/output schema 以及 `should_defer`/`always_load`/`requires_interaction`/`strict`/cache/eager 等请求提示元数据,并兼容 query/limit 相邻输入别名。deferred/lazy tool discovery 仍未宣称完成。
294294
- 本轮补充:Anthropic request tool 转换会保留 contract 的 `strict``eager_input_streaming``cache_control``should_defer`,把 deferred 工具序列化为 API `defer_loading`,并让 `always_load` 覆盖 deferred hint;API tool description 会按 description、prompt、searchHint 顺序 fallback,conversation runner 构造请求时会保留 `Task` 等 deferred tool 的 strict/defer_loading 元数据。完整 deferred/lazy tool discovery 仍未宣称完成。
295295
- 本轮补充:新增基础 slash command parser/executor,支持官方 `/command args``/mcp:tool (MCP) args` 解析;conversation runner 现在会在请求模型前展开本地项目 prompt skill slash command,生成 command metadata user message 和 meta prompt message,保留 transcript parent chain,并支持 skill `model` 覆盖本轮请求。local/local-jsx 目前只返回未实现输出且不会误发模型,command permissions attachment、forked/MCP/plugin/bundled slash command 和 UI wiring 仍未宣称完成。
296296
- 本轮补充:本地 prompt skill slash command 和 `Skill` tool 现在都会生成 `command_permissions` attachment,解析 `allowed-tools` 的 comma/space 分隔形式并保留括号内 tool pattern;Runner 会在当前 turn 内把 attachment 转成 `PermissionSourceCommand` allow rules 合并进 engine permission decider,让 skill 授权的后续工具调用可在同一轮通过。完整权限 UI/SDK 展示、forked/MCP/plugin/bundled skill 权限继承仍未宣称完成。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ Anthropic API 和 conversation:
8888
- `/plugin <plugin-name>` now takes the no-query local plugin-detail path used by `/plugin show <plugin-name>` when the name matches a local plugin manifest, while unknown one-word subcommands still report unsupported.
8989
- `/model show`, `/model info`, and `/model current` now take the no-query current-model path instead of treating those words as custom model names and mutating the runner model.
9090
- A basic `Skill` tool wrapper is now registered with the default built-in tool set. It can invoke local project and plugin-directory prompt skills through the command registry, returns the official-style `Launching skill: ...` result plus structured command metadata, preserves command source/display/root/frontmatter metadata for plugin skills, and passes expanded meta user messages through `ToolResult.NewMessages`; the conversation runner now appends those new messages to transcripts and subsequent model requests.
91-
- A basic `ToolSearch` tool is now registered with the default built-in tool set. The executor exposes the current registry through tool metadata, and `ToolSearch` can search available definitions by name, alias, description, prompt, search hint, and input/output schema fields, returning BM25-ranked structured results with read-only/concurrency/destructive plus input/output schema, deferred/loading/request-hint metadata, and query/limit aliases.
91+
- A basic `ToolSearch` tool is now registered with the default built-in tool set. The executor exposes the current registry through tool metadata, and `ToolSearch` can search available definitions by name, alias, description, prompt, search hint, and input/output schema fields, supports `select:ToolA,ToolB` direct selection, and returns tool-reference content plus BM25/select structured results with read-only/concurrency/destructive, input/output schema, deferred/loading/request-hint metadata, and query/limit aliases.
9292
- Anthropic request tool conversion now preserves contract-level `strict`, `eager_input_streaming`, `cache_control`, and API-level `defer_loading` hints from deferred tool definitions, with `always_load` overriding `should_defer`; it also falls back from `description` to `prompt` to `searchHint` when building API tool descriptions. Runner request construction preserves these fields for tools such as `Task`.
9393
- Basic slash prompt command invocation is now wired for local project prompt skills: `/skill args` is parsed before the model request, converted into official-shaped command metadata plus expanded meta prompt messages, appended to transcript parent chains, and can override the turn model from skill frontmatter.
9494
- Local prompt-skill slash commands and the `Skill` tool now emit `command_permissions` attachments, parse `allowed-tools` into command-scoped permission rules, and merge those rules into the engine permission decider for the current turn so authorized follow-up tool calls can pass without leaking into later turns.

internal/contracts/tool.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ type ToolResult struct {
4646
Meta map[string]any `json:"_meta,omitempty"`
4747
}
4848

49+
type ToolReference struct {
50+
Type string `json:"type"`
51+
ToolName string `json:"tool_name"`
52+
}
53+
54+
func NewToolReference(name string) ToolReference {
55+
return ToolReference{Type: "tool_reference", ToolName: name}
56+
}
57+
4958
type ToolProgress struct {
5059
ToolUseID ID `json:"tool_use_id"`
5160
Type string `json:"type"`

internal/conversation/run_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,75 @@ func TestRunnerExecutesToolUseAndContinuesConversation(t *testing.T) {
234234
}
235235
}
236236

237+
func TestRunnerPreservesToolReferenceResultContent(t *testing.T) {
238+
registry, err := tool.NewRegistry(tool.FuncTool{
239+
DefinitionValue: contracts.ToolDefinition{
240+
Name: "ToolSearch",
241+
Description: "selects tool references",
242+
ReadOnly: true,
243+
InputSchema: contracts.JSONSchema{"type": "object"},
244+
},
245+
CallFunc: func(ctx tool.Context, raw json.RawMessage, sink tool.ProgressSink) (contracts.ToolResult, error) {
246+
return contracts.ToolResult{
247+
Content: []contracts.ToolReference{
248+
contracts.NewToolReference("Read"),
249+
contracts.NewToolReference("Edit"),
250+
},
251+
StructuredContent: map[string]any{
252+
"matches": 2,
253+
},
254+
}, nil
255+
},
256+
})
257+
if err != nil {
258+
t.Fatal(err)
259+
}
260+
261+
client := &fakeClient{calls: []fakeCall{
262+
{response: &anthropic.Response{
263+
ID: "msg_tool",
264+
Type: "message",
265+
Role: "assistant",
266+
Model: "sonnet",
267+
StopReason: "tool_use",
268+
Content: []contracts.ContentBlock{{
269+
Type: contracts.ContentToolUse,
270+
ID: "toolu_search",
271+
Name: "ToolSearch",
272+
Input: json.RawMessage(`{"query":"select:Read,Edit"}`),
273+
}},
274+
}},
275+
{response: &anthropic.Response{
276+
ID: "msg_done",
277+
Type: "message",
278+
Role: "assistant",
279+
Model: "sonnet",
280+
StopReason: "end_turn",
281+
Content: []contracts.ContentBlock{contracts.NewTextBlock("done")},
282+
}},
283+
}}
284+
runner := Runner{
285+
Client: client,
286+
Tools: tool.NewExecutor(registry),
287+
Model: "sonnet",
288+
MaxTokens: 128,
289+
}
290+
if _, err := runner.RunTurn(context.Background(), nil, messages.UserText("find tools")); err != nil {
291+
t.Fatal(err)
292+
}
293+
if len(client.requests) != 2 {
294+
t.Fatalf("requests = %d, want 2", len(client.requests))
295+
}
296+
last := client.requests[1].Messages[len(client.requests[1].Messages)-1]
297+
if last.Role != "user" || len(last.Content) != 1 || last.Content[0].Type != contracts.ContentToolResult {
298+
t.Fatalf("last api message = %#v", last)
299+
}
300+
references, ok := last.Content[0].Content.([]contracts.ToolReference)
301+
if !ok || len(references) != 2 || references[0].ToolName != "Read" || references[1].ToolName != "Edit" {
302+
t.Fatalf("tool result content = %#v", last.Content[0].Content)
303+
}
304+
}
305+
237306
func TestRunnerExecutesSettingsCommandHookForToolUse(t *testing.T) {
238307
registry, err := tool.NewRegistry(tool.FuncTool{
239308
DefinitionValue: contracts.ToolDefinition{

internal/tools/searchtools/tools.go

Lines changed: 114 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,39 @@ func NewToolSearchTool() tool.Tool {
3535
"properties": map[string]any{
3636
"query": map[string]any{
3737
"type": "string",
38-
"description": "Search query for available tools.",
38+
"description": "Search query for available tools. Use select:ToolA,ToolB to fetch exact tool references.",
3939
},
4040
"topn": map[string]any{
4141
"type": "integer",
4242
"description": "Maximum number of matching tools to return.",
4343
},
4444
},
4545
},
46+
OutputSchema: contracts.JSONSchema{
47+
"type": "object",
48+
"properties": map[string]any{
49+
"query": map[string]any{"type": "string"},
50+
"limit": map[string]any{"type": "integer"},
51+
"matches": map[string]any{"type": "integer"},
52+
"ranking": map[string]any{"type": "string"},
53+
"query_type": map[string]any{"type": "string", "enum": []any{"keyword", "select"}},
54+
"text": map[string]any{"type": "string"},
55+
"requested": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
56+
"missing": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
57+
"tool_references": map[string]any{
58+
"type": "array",
59+
"items": map[string]any{
60+
"type": "object",
61+
"required": []any{"type", "tool_name"},
62+
"properties": map[string]any{
63+
"type": map[string]any{"const": "tool_reference"},
64+
"tool_name": map[string]any{"type": "string"},
65+
},
66+
},
67+
},
68+
"results": map[string]any{"type": "array"},
69+
},
70+
},
4671
},
4772
NormalizeFunc: normalizeSearchInput,
4873
ValidateFunc: validateSearchInput,
@@ -114,13 +139,27 @@ func callToolSearch(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink)
114139
if err != nil {
115140
return contracts.ToolResult{}, err
116141
}
117-
results := matchToolDefinitions(definitions, input.Query, limit)
142+
queryType := "keyword"
143+
ranking := "bm25"
144+
var requested []string
145+
var missing []string
146+
var results []scoredDefinition
147+
if selectNames, ok := parseSelectQuery(input.Query); ok {
148+
queryType = "select"
149+
ranking = "select"
150+
requested = selectNames
151+
results, missing = selectToolDefinitions(definitions, selectNames)
152+
} else {
153+
results = matchToolDefinitions(definitions, input.Query, limit)
154+
}
118155
structuredResults := make([]map[string]any, 0, len(results))
119156
lines := []string{
120157
"Tool search: " + strings.TrimSpace(input.Query),
121158
fmt.Sprintf("Matches: %d", len(results)),
122159
}
160+
toolReferences := make([]contracts.ToolReference, 0, len(results))
123161
for _, result := range results {
162+
toolReferences = append(toolReferences, contracts.NewToolReference(result.Definition.Name))
124163
entry := map[string]any{
125164
"name": result.Definition.Name,
126165
"score": result.Score,
@@ -162,15 +201,31 @@ func callToolSearch(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink)
162201
if len(results) == 0 {
163202
lines = append(lines, "No tools matched.")
164203
}
204+
if len(missing) > 0 {
205+
lines = append(lines, "Missing: "+strings.Join(missing, ", "))
206+
}
207+
text := strings.Join(lines, "\n")
208+
content := any(text)
209+
if len(toolReferences) > 0 {
210+
content = toolReferences
211+
}
212+
structured := map[string]any{
213+
"query": strings.TrimSpace(input.Query),
214+
"limit": limit,
215+
"matches": len(results),
216+
"ranking": ranking,
217+
"query_type": queryType,
218+
"text": text,
219+
"tool_references": toolReferences,
220+
"results": structuredResults,
221+
}
222+
if queryType == "select" {
223+
structured["requested"] = requested
224+
structured["missing"] = missing
225+
}
165226
return contracts.ToolResult{
166-
Content: strings.Join(lines, "\n"),
167-
StructuredContent: map[string]any{
168-
"query": strings.TrimSpace(input.Query),
169-
"limit": limit,
170-
"matches": len(results),
171-
"ranking": "bm25",
172-
"results": structuredResults,
173-
},
227+
Content: content,
228+
StructuredContent: structured,
174229
}, nil
175230
}
176231

@@ -218,6 +273,55 @@ func matchToolDefinitions(definitions []contracts.ToolDefinition, query string,
218273
return results
219274
}
220275

276+
func parseSelectQuery(query string) ([]string, bool) {
277+
trimmed := strings.TrimSpace(query)
278+
if !strings.HasPrefix(strings.ToLower(trimmed), "select:") {
279+
return nil, false
280+
}
281+
rawNames := strings.Split(trimmed[len("select:"):], ",")
282+
names := make([]string, 0, len(rawNames))
283+
for _, rawName := range rawNames {
284+
name := strings.TrimSpace(rawName)
285+
if name != "" {
286+
names = append(names, name)
287+
}
288+
}
289+
return names, true
290+
}
291+
292+
func selectToolDefinitions(definitions []contracts.ToolDefinition, names []string) ([]scoredDefinition, []string) {
293+
selected := make([]scoredDefinition, 0, len(names))
294+
missing := make([]string, 0)
295+
seen := map[string]struct{}{}
296+
for _, name := range names {
297+
definition, ok := findToolDefinition(definitions, name)
298+
if !ok {
299+
missing = append(missing, name)
300+
continue
301+
}
302+
if _, ok := seen[definition.Name]; ok {
303+
continue
304+
}
305+
seen[definition.Name] = struct{}{}
306+
selected = append(selected, scoredDefinition{Definition: definition, Score: 1})
307+
}
308+
return selected, missing
309+
}
310+
311+
func findToolDefinition(definitions []contracts.ToolDefinition, name string) (contracts.ToolDefinition, bool) {
312+
for _, definition := range definitions {
313+
if strings.EqualFold(definition.Name, name) {
314+
return definition, true
315+
}
316+
for _, alias := range definition.Aliases {
317+
if strings.EqualFold(alias, name) {
318+
return definition, true
319+
}
320+
}
321+
}
322+
return contracts.ToolDefinition{}, false
323+
}
324+
221325
func buildBM25Corpus(definitions []contracts.ToolDefinition) bm25Corpus {
222326
documents := make([]bm25Document, 0, len(definitions))
223327
documentCounts := map[string]int{}

0 commit comments

Comments
 (0)