Skip to content

Commit 469845a

Browse files
author
SqlRush
committed
Gate deferred tools delta via feature setting
1 parent 828b234 commit 469845a

13 files changed

Lines changed: 144 additions & 15 deletions

docs/cc-100-roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ M8/M2 补充:conversation request 现在会在发送 beta `defer_loading` / `t
203203

204204
M8/M2 补充:MCP tools 现在按官方 `isMcp` 语义进入 deferred tool pool,除非显式 `always_load` 覆盖;Anthropic tool schema 序列化会为 MCP tools 输出 `defer_loading`,ToolSearch auto 的 token-count 请求也会计入 MCP tools 但以 loaded schema 计数;configured MCP toolset attach/close 会清空 ToolSearch token-count cache,避免 MCP server lifecycle 变化后复用旧计数。token-count VCR 仍未完成。
205205

206-
M8/M2 补充:`USER_TYPE=ant` ToolSearch deferred tool 公告改走官方 `deferred_tools_delta` attachment:runner 会扫描历史 attachment 计算新增/移除 deferred 工具,生成并持久化 attachment,API normalization 会把 attachment 渲染为官方 ToolSearch 可用/断开提示;delta 模式不再注入临时 `<available-deferred-tools>` prepend。更深的 analytics/pool-change telemetry 和非 ant feature gate 仍未完成。
206+
M8/M2 补充:`USER_TYPE=ant` 或 settings `advanced.tengu_glacier_2xr`/`advanced.tenguGlacier2xr` 启用时,ToolSearch deferred tool 公告改走官方 `deferred_tools_delta` attachment:runner 会扫描历史 attachment 计算新增/移除 deferred 工具,生成并持久化 attachment,API normalization 会把 attachment 渲染为官方 ToolSearch 可用/断开提示;delta 模式不再注入临时 `<available-deferred-tools>` prepend。更深的 analytics/pool-change telemetry 仍未完成。
207207

208208
M8/M2 补充:当本次 request 未启用 `ToolSearch` 时,conversation request 现在会从 API user `tool_result.content` 中剥离历史 `tool_reference` blocks;纯 reference 结果会替换为官方占位文本 `[Tool references removed - tool search not enabled]`,剥离发生在 discovered-tool 扫描之后,避免影响后续 loaded 工具恢复。
209209

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ test/parity/ # golden tests against TS/official behavior
296296
- 本轮补充:当 request registry 同时存在 `ToolSearch` 和 deferred 工具时,conversation request 会按官方 dynamic tool loading 过滤工具列表:未发现 deferred 工具不再发送 schema,已发现 deferred 工具作为 loaded tool 发送,`ToolSearch` 保持可调用,并在首条 API user message 前注入 `<available-deferred-tools>` 名称列表;没有 deferred 工具时会从请求中移除 `ToolSearch`
297297
- 本轮补充:conversation request 现在会在发送 beta `defer_loading` / `tool_reference` shape 前执行官方 ToolSearch enablement gate:Haiku 模型、`CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS`、falsy `ENABLE_TOOL_SEARCH`、`ENABLE_TOOL_SEARCH=auto:100`、未显式启用 ToolSearch 时的非一方 `ANTHROPIC_BASE_URL`、以及 `ENABLE_TOOL_SEARCH=auto` 下 deferred 工具描述体积低于阈值都会回落为标准 inline tool schema;显式 `ENABLE_TOOL_SEARCH=true` 仍可让支持 beta shape 的自定义网关 opt in。auto 模式已接入 Anthropic `/v1/messages/count_tokens` 精确计数路径,成功时减去官方固定 tool overhead 后按 token 阈值判断,失败时会用 Haiku message-create usage 作为官方第二层 fallback,再失败才按官方字符 fallback 计算阈值,覆盖 `auto:N` 百分比、`[1m]` context 和 ant-only context-window override;deferred tool token count 现在按官方 deferred tool name 列表做 in-process memoization,成功值和不可用结果都会缓存。token-count VCR 仍未宣称完成。
298298
- 本轮补充:MCP tools 现在按官方 `isMcp` 语义进入 deferred tool pool,除非显式 `always_load` 覆盖;Anthropic tool schema 序列化会为 MCP tools 输出 `defer_loading`,ToolSearch auto 的 token-count 请求也会计入 MCP tools 但以 loaded schema 计数;configured MCP toolset attach/close 会清空 ToolSearch token-count cache,避免 MCP server lifecycle 变化后复用旧计数。token-count VCR 仍未宣称完成。
299-
- 本轮补充:`USER_TYPE=ant` ToolSearch deferred tool 公告改走官方 `deferred_tools_delta` attachment:runner 会扫描历史 attachment 计算新增/移除 deferred 工具,生成并持久化 attachment,API normalization 会把 attachment 渲染为官方 ToolSearch 可用/断开提示;delta 模式不再注入临时 `<available-deferred-tools>` prepend。更深的 analytics/pool-change telemetry 和非 ant feature gate 仍未宣称完成。
299+
- 本轮补充:`USER_TYPE=ant` 或 settings `advanced.tengu_glacier_2xr`/`advanced.tenguGlacier2xr` 启用时,ToolSearch deferred tool 公告改走官方 `deferred_tools_delta` attachment:runner 会扫描历史 attachment 计算新增/移除 deferred 工具,生成并持久化 attachment,API normalization 会把 attachment 渲染为官方 ToolSearch 可用/断开提示;delta 模式不再注入临时 `<available-deferred-tools>` prepend。更深的 analytics/pool-change telemetry 仍未宣称完成。
300300
- 本轮补充:当本次 request 未启用 `ToolSearch` 时,API message normalization 会从历史 user `tool_result.content` 中剥离 `tool_reference` blocks;纯 reference 结果会替换成官方占位文本 `[Tool references removed - tool search not enabled]`,但剥离发生在 discovered-tool 扫描之后,不影响后续启用 ToolSearch 时恢复 loaded 工具。
301301
- 本轮补充: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 仍未宣称完成。
302302
- 本轮补充:tool executor 会在未通过 `ToolSearch` 发现的 deferred 工具发生输入 schema 校验失败时追加 schema-not-sent 恢复提示,引导模型先调用 `ToolSearch``select:<tool>` 再重试;runner 会把当前 turn messages 传入工具 metadata,提示判断兼容 `tool_reference` 结果和 compact boundary 的已发现工具快照。

docs/first-second-parity-audit.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Anthropic API 和 conversation:
9595
- Request construction now also applies the official ToolSearch enablement gates before emitting beta `defer_loading` / `tool_reference` shapes: Haiku models, `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS`, falsy `ENABLE_TOOL_SEARCH`, `ENABLE_TOOL_SEARCH=auto:100`, unset ToolSearch behind non-first-party `ANTHROPIC_BASE_URL`, and `ENABLE_TOOL_SEARCH=auto` requests below the deferred-tool size threshold all fall back to standard inline tool schemas; explicit `ENABLE_TOOL_SEARCH=true` can still opt a custom gateway back in. Auto mode now uses the Anthropic `/v1/messages/count_tokens` endpoint when the client supports it, subtracts the official fixed tool overhead, falls back to Haiku message-create usage when count-tokens fails, and then falls back to the official character threshold when token counting is unavailable, including `auto:N` percentages, 1M context suffixes, and ant-only context-window overrides.
9696
- Deferred tool token counts in ToolSearch auto mode are now memoized in-process by the official deferred tool name list key, including both successful counts and unavailable/null-equivalent results.
9797
- MCP tools now follow the official deferred-tool rule: they are deferred unless explicitly marked `always_load`, Anthropic tool schema serialization emits `defer_loading` for MCP tools, ToolSearch auto token counting includes MCP tools as loaded schemas, and configured MCP toolset attach/close invalidates the in-process deferred-tool token-count cache.
98-
- ToolSearch deferred tool announcements now support the official ant `deferred_tools_delta` attachment path: the runner diffs deferred tools against prior delta attachments, persists a new attachment, API normalization renders added/removed tool reminders, and ant delta mode skips the ephemeral `<available-deferred-tools>` prepend.
98+
- ToolSearch deferred tool announcements now support the official `deferred_tools_delta` attachment path when `USER_TYPE=ant` or settings `advanced.tengu_glacier_2xr`/`advanced.tenguGlacier2xr` is enabled: the runner diffs deferred tools against prior delta attachments, persists a new attachment, API normalization renders added/removed tool reminders, and delta mode skips the ephemeral `<available-deferred-tools>` prepend.
9999
- When ToolSearch is inactive for a request, API message normalization strips historical `tool_reference` blocks from user `tool_result` content and replaces pure-reference results with the official placeholder text, while preserving history scanning before stripping so discovered tools can still be loaded when ToolSearch is active.
100100
- 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`.
101101
- Tool validation now appends an official-style schema-not-sent recovery hint when an undiscovered deferred tool is invoked with invalid typed input and `ToolSearch` is available, including compact-boundary discovered-tool metadata so already loaded tools do not get a false hint.
@@ -117,7 +117,7 @@ The following items remain incomplete and must not be treated as done:
117117
- Auto mode / YOLO classifier: transcript construction, two-stage classifier, XML/tool-use parsing, prompt dump, denial circuit breaker, model gating, and fallback behavior.
118118
- Interactive permission prompt flow: REPL dialogs, bridge/channel/swarm permission relays, user feedback images, prompt race handling, cancellation.
119119
- Full filesystem permission parity gaps that remain: skill-scope allow suggestions, remaining local/local-jsx slash-command execution wiring beyond the currently covered no-query builtins, command permission UI/SDK surfacing, forked/remote/MCP/bundled SkillTool behavior and remaining plugin SkillTool UI/SDK polish beyond current metadata preservation, skill prompt shell injection, complete auto-memory override policy, and deeper platform-specific Windows/WSL bypass handling.
120-
- Full tool execution parity gaps that remain: complete hook runtime policy beyond the current settings command hook path, MCP elicitation, complete SDK control event surface beyond current progress NDJSON, full deferred/lazy tool discovery beyond current local registry BM25 search, remaining ToolSearch model/env/proxy edge gates, remaining `ENABLE_TOOL_SEARCH=auto` edge behavior such as official token-count VCR, remaining deferred-tool pool-change analytics/telemetry and non-ant delta feature-gate details, mid-call cancellation for concrete tools, background task behavior, telemetry, and concrete tool-specific semantics.
120+
- Full tool execution parity gaps that remain: complete hook runtime policy beyond the current settings command hook path, MCP elicitation, complete SDK control event surface beyond current progress NDJSON, full deferred/lazy tool discovery beyond current local registry BM25 search, remaining ToolSearch model/env/proxy edge gates, remaining `ENABLE_TOOL_SEARCH=auto` edge behavior such as official token-count VCR, remaining deferred-tool pool-change analytics/telemetry, mid-call cancellation for concrete tools, background task behavior, telemetry, and concrete tool-specific semantics.
121121
- Complete Anthropic API parity gaps that remain: dynamic beta-header latching for any remaining provider-specific feature gates beyond prompt-cache/cache-editing/strict-output/1M-context requests, ant-only dump gating and remote `/issue` submission integration beyond the current local context summary, full official cost tracker/session restore lifecycle beyond current transcript usage restore, post-handshake streaming failure semantics, deeper gateway/proxy auth and routing behavior beyond static custom headers, first-party/Bedrock/Vertex/Foundry client setup, deeper OAuth account/profile lifecycle beyond request-time refresh retry, fast-mode retry/cooldown semantics, persistent unattended retry heartbeats, full prompt-cache editing lifecycle, and provider-specific cache behavior.
122122
- Full conversation/query loop: stop hooks, compact/auto-compact, token budget escalation, resume, SDK JSON/NDJSON control events, status updates, rate-limit handling, model switch breadcrumbs, side questions.
123123
- Full settings parity gaps that remain: complete Zod-equivalent validation messages, full remote managed-settings watcher/non-daemon background refresh lifecycle beyond current turn-start and daemon-heartbeat refresh, remaining forked agent/frontmatter execution and UI edge cases beyond current policy metadata filtering, complete marketplace TUI/UI/background lifecycle beyond current local/settings/directory/file/URL catalog/git/github/npm cache manifest load, URL catalog cache fallback, git/github fetch/pull cache refresh, npm pack cache refresh, and background watcher/continuous app-state sync beyond current turn-start local settings reload.

internal/config/schema_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ func TestSettingsJSONSchemaIncludesNestedTypesAndEnums(t *testing.T) {
7676
advanced := properties["advanced"].(map[string]any)
7777
advancedProps := advanced["properties"].(map[string]any)
7878
if advancedProps["bridge"].(map[string]any)["type"] != "boolean" ||
79-
advancedProps["computerUse"].(map[string]any)["type"] != "boolean" {
79+
advancedProps["computerUse"].(map[string]any)["type"] != "boolean" ||
80+
advancedProps["tengu_glacier_2xr"].(map[string]any)["type"] != "boolean" {
8081
t.Fatalf("advanced schema = %#v", advancedProps)
8182
}
8283
}

internal/config/settings.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ func mergeAdvancedSetting(a, b *contracts.AdvancedSetting) *contracts.AdvancedSe
324324
if b.NativeIntegrations != nil {
325325
cp.NativeIntegrations = clonePtr(b.NativeIntegrations)
326326
}
327+
if b.TenguGlacier2XR != nil {
328+
cp.TenguGlacier2XR = clonePtr(b.TenguGlacier2XR)
329+
}
327330
return &cp
328331
}
329332
out := *a
@@ -348,6 +351,9 @@ func mergeAdvancedSetting(a, b *contracts.AdvancedSetting) *contracts.AdvancedSe
348351
if a.NativeIntegrations != nil {
349352
out.NativeIntegrations = clonePtr(a.NativeIntegrations)
350353
}
354+
if a.TenguGlacier2XR != nil {
355+
out.TenguGlacier2XR = clonePtr(a.TenguGlacier2XR)
356+
}
351357
if b.Bridge != nil {
352358
out.Bridge = clonePtr(b.Bridge)
353359
}
@@ -369,6 +375,9 @@ func mergeAdvancedSetting(a, b *contracts.AdvancedSetting) *contracts.AdvancedSe
369375
if b.NativeIntegrations != nil {
370376
out.NativeIntegrations = clonePtr(b.NativeIntegrations)
371377
}
378+
if b.TenguGlacier2XR != nil {
379+
out.TenguGlacier2XR = clonePtr(b.TenguGlacier2XR)
380+
}
372381
return &out
373382
}
374383

internal/config/settings_test.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,8 @@ func TestMergeSettings(t *testing.T) {
351351
bridgeEnabled := true
352352
telemetryDisabled := false
353353
telemetryEnabled := true
354+
tenguDisabled := false
355+
tenguEnabled := true
354356
a := contracts.Settings{
355357
Env: map[string]string{"A": "1"},
356358
Permissions: &contracts.PermissionsSetting{
@@ -363,8 +365,9 @@ func TestMergeSettings(t *testing.T) {
363365
SymlinkDirectories: []string{"cache"},
364366
},
365367
Advanced: &contracts.AdvancedSetting{
366-
Bridge: &bridgeEnabled,
367-
Telemetry: &telemetryDisabled,
368+
Bridge: &bridgeEnabled,
369+
Telemetry: &telemetryDisabled,
370+
TenguGlacier2XR: &tenguDisabled,
368371
},
369372
TelemetryExport: &contracts.TelemetryExportSetting{
370373
Path: "/tmp/old.jsonl",
@@ -390,7 +393,8 @@ func TestMergeSettings(t *testing.T) {
390393
SymlinkDirectories: []string{"node_modules"},
391394
},
392395
Advanced: &contracts.AdvancedSetting{
393-
Telemetry: &telemetryEnabled,
396+
Telemetry: &telemetryEnabled,
397+
TenguGlacier2XR: &tenguEnabled,
394398
},
395399
TelemetryExport: &contracts.TelemetryExportSetting{
396400
Path: "/tmp/new.jsonl",
@@ -422,7 +426,10 @@ func TestMergeSettings(t *testing.T) {
422426
if len(merged.Worktree.SymlinkDirectories) != 2 || merged.Worktree.SymlinkDirectories[0] != "cache" || merged.Worktree.SymlinkDirectories[1] != "node_modules" {
423427
t.Fatalf("worktree symlink dirs = %#v", merged.Worktree.SymlinkDirectories)
424428
}
425-
if merged.Advanced == nil || merged.Advanced.Bridge == nil || !*merged.Advanced.Bridge || merged.Advanced.Telemetry == nil || !*merged.Advanced.Telemetry {
429+
if merged.Advanced == nil ||
430+
merged.Advanced.Bridge == nil || !*merged.Advanced.Bridge ||
431+
merged.Advanced.Telemetry == nil || !*merged.Advanced.Telemetry ||
432+
merged.Advanced.TenguGlacier2XR == nil || !*merged.Advanced.TenguGlacier2XR {
426433
t.Fatalf("advanced = %#v", merged.Advanced)
427434
}
428435
if merged.TelemetryExport == nil ||
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package contracts
2+
3+
import "encoding/json"
4+
5+
func (a *AdvancedSetting) UnmarshalJSON(data []byte) error {
6+
type alias AdvancedSetting
7+
var raw map[string]json.RawMessage
8+
if err := json.Unmarshal(data, &raw); err != nil {
9+
return err
10+
}
11+
if _, ok := raw["tengu_glacier_2xr"]; !ok {
12+
if value, aliasOK := raw["tenguGlacier2xr"]; aliasOK {
13+
raw["tengu_glacier_2xr"] = value
14+
}
15+
}
16+
normalized, err := json.Marshal(raw)
17+
if err != nil {
18+
return err
19+
}
20+
var out alias
21+
if err := json.Unmarshal(normalized, &out); err != nil {
22+
return err
23+
}
24+
*a = AdvancedSetting(out)
25+
return nil
26+
}

internal/contracts/settings.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ type AdvancedSetting struct {
132132
Voice *bool `json:"voice,omitempty"`
133133
ComputerUse *bool `json:"computerUse,omitempty"`
134134
NativeIntegrations *bool `json:"nativeIntegrations,omitempty"`
135+
TenguGlacier2XR *bool `json:"tengu_glacier_2xr,omitempty"`
135136
}
136137

137138
type PermissionsSetting struct {

internal/contracts/settings_json_test.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func TestSettingsPreservesUnknownFields(t *testing.T) {
3232
}
3333

3434
func TestSettingsParsesAdvancedFeatureGates(t *testing.T) {
35-
input := []byte(`{"advanced":{"bridge":true,"lsp":false,"telemetry":true,"computerUse":true},"telemetryExport":{"path":"/tmp/events.jsonl","url":"https://example.com/telemetry","headers":{"Authorization":"Bearer token"}},"remote":{"defaultEnvironmentId":"env-prod","registrationUrl":"https://example.com/register","authToken":"remote-token"}}`)
35+
input := []byte(`{"advanced":{"bridge":true,"lsp":false,"telemetry":true,"computerUse":true,"tengu_glacier_2xr":true},"telemetryExport":{"path":"/tmp/events.jsonl","url":"https://example.com/telemetry","headers":{"Authorization":"Bearer token"}},"remote":{"defaultEnvironmentId":"env-prod","registrationUrl":"https://example.com/register","authToken":"remote-token"}}`)
3636
var settings Settings
3737
if err := json.Unmarshal(input, &settings); err != nil {
3838
t.Fatal(err)
@@ -43,7 +43,9 @@ func TestSettingsParsesAdvancedFeatureGates(t *testing.T) {
4343
if settings.Advanced.LSP == nil || *settings.Advanced.LSP {
4444
t.Fatalf("advanced lsp = %#v", settings.Advanced)
4545
}
46-
if settings.Advanced.Telemetry == nil || !*settings.Advanced.Telemetry || settings.Advanced.ComputerUse == nil || !*settings.Advanced.ComputerUse {
46+
if settings.Advanced.Telemetry == nil || !*settings.Advanced.Telemetry ||
47+
settings.Advanced.ComputerUse == nil || !*settings.Advanced.ComputerUse ||
48+
settings.Advanced.TenguGlacier2XR == nil || !*settings.Advanced.TenguGlacier2XR {
4749
t.Fatalf("advanced gates = %#v", settings.Advanced)
4850
}
4951
if settings.Extra["advanced"] != nil {
@@ -69,6 +71,17 @@ func TestSettingsParsesAdvancedFeatureGates(t *testing.T) {
6971
}
7072
}
7173

74+
func TestSettingsParsesAdvancedTenguGlacierCamelAlias(t *testing.T) {
75+
input := []byte(`{"advanced":{"tenguGlacier2xr":true}}`)
76+
var settings Settings
77+
if err := json.Unmarshal(input, &settings); err != nil {
78+
t.Fatal(err)
79+
}
80+
if settings.Advanced == nil || settings.Advanced.TenguGlacier2XR == nil || !*settings.Advanced.TenguGlacier2XR {
81+
t.Fatalf("advanced = %#v", settings.Advanced)
82+
}
83+
}
84+
7285
func TestSettingsParsesOfficialMCPPolicyEntries(t *testing.T) {
7386
input := []byte(`{
7487
"allowedMcpServers": [

internal/conversation/deferred_tools_delta.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type deferredToolsDelta struct {
1515
}
1616

1717
func (r Runner) maybeAppendDeferredToolsDeltaAttachment(ctx context.Context, history []contracts.Message) ([]contracts.Message, *contracts.Message, error) {
18-
if !deferredToolsDeltaEnabled() || r.Tools.Registry == nil {
18+
if !r.deferredToolsDeltaEnabled() || r.Tools.Registry == nil {
1919
return history, nil, nil
2020
}
2121
definitions, err := r.Tools.Registry.Definitions(toolPromptContext(r))

0 commit comments

Comments
 (0)