one-code-extension 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +58 -31
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +123 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +52 -0
- package/extensions/background/wakeup.ts +159 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +88 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +72 -19
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +198 -31
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +13 -5
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +42 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/local-command.ts +47 -13
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +308 -64
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +13 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +113 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +149 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +143 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +70 -4
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +87 -5
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +209 -71
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +151 -67
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +61 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +12 -7
- package/extensions/recap/index.ts +65 -60
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +175 -13
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +105 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +18 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/** Reject damaged OpenRouter streams before tool execution, without changing the request prefix. */
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { notifyOrPrint } from "../lib/headless-output.ts";
|
|
4
|
+
import { TURN_FAILED_CHANNEL, type TurnFailedEvent } from "../lib/interrupt.ts";
|
|
5
|
+
import { sessionOutlivesTurn } from "../lib/notifications.ts";
|
|
6
|
+
import { isOpenRouter, openRouterProviderName, toolCallCorruptionNotice, type UpstreamFailure } from "../lib/openrouter-generation.ts";
|
|
7
|
+
import { sessionAlive } from "../lib/session-lifecycle.ts";
|
|
8
|
+
import {
|
|
9
|
+
CONTENT_MARKUP_LIMIT,
|
|
10
|
+
RUNAWAY_PADDING_LIMIT,
|
|
11
|
+
scanToolCallMarkup,
|
|
12
|
+
TOOL_CALL_MARKUP_LIMIT,
|
|
13
|
+
type ToolCallDamage,
|
|
14
|
+
type TrailingPadding,
|
|
15
|
+
toolCallCorruptionReason,
|
|
16
|
+
trailingPadding,
|
|
17
|
+
} from "../lib/tool-call-corruption.ts";
|
|
18
|
+
import { withKeepAlive } from "../lsp/keep-alive.ts";
|
|
19
|
+
|
|
20
|
+
// Silence allowed inside a stream. Every raw chunk restarts the clock, but
|
|
21
|
+
// OpenRouter's keepalive comments never reach us, so a model thinking
|
|
22
|
+
// server-side before its first token looks silent: that wait is long. After
|
|
23
|
+
// the first content it is Claude Code's 90 s watchdog, and 60 s while a tool
|
|
24
|
+
// call's arguments stream (arguments never pause for thinking).
|
|
25
|
+
const FIRST_CONTENT_IDLE_MS = 300_000;
|
|
26
|
+
const STREAM_IDLE_MS = 90_000;
|
|
27
|
+
const TOOL_CALL_IDLE_MS = 60_000;
|
|
28
|
+
|
|
29
|
+
export default function toolCallCorruptionExtension(pi: ExtensionAPI) {
|
|
30
|
+
toolCallCorruptionGuard(pi, { announce: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The guard itself. `announce: false` is the in-process child's shape
|
|
35
|
+
* (`child.ts`): it blocks and stops the same way, but a child has no terminal
|
|
36
|
+
* of its own (stderr is the parent's TUI) and is disposed without
|
|
37
|
+
* `session_shutdown`, so it prints nothing and starts no provider lookup that
|
|
38
|
+
* could outlive it. Its parent sees the child's turn end terminated.
|
|
39
|
+
*/
|
|
40
|
+
export function toolCallCorruptionGuard(pi: ExtensionAPI, { announce }: { announce: boolean }) {
|
|
41
|
+
const alive = sessionAlive(pi);
|
|
42
|
+
const rawCalls = new Map<number, { raw: string; markup: number; contentMarkup: number; next: number }>();
|
|
43
|
+
const flagged = new Map<string, ToolCallDamage>();
|
|
44
|
+
// Any provider: a call streaming blank padding without end never reaches
|
|
45
|
+
// toolcall_end, and nothing else stops it (gpt-6.1-sol, battery round 7:
|
|
46
|
+
// 26 MB of tabs and newlines over 25 minutes in a one-shot run).
|
|
47
|
+
const padding = new Map<number, TrailingPadding>();
|
|
48
|
+
const pending = new Map<AbortController, Promise<void>>();
|
|
49
|
+
let responseId: string | undefined;
|
|
50
|
+
// OpenRouter names the serving provider on every chunk; a cancelled
|
|
51
|
+
// generation is never recorded, so the lookup cannot name it after a stop.
|
|
52
|
+
let streamProvider: string | undefined;
|
|
53
|
+
let stopped = false;
|
|
54
|
+
// A suspect call goes back to the model once per run; the next one is corrupt.
|
|
55
|
+
let suspectReturned = false;
|
|
56
|
+
let contentStarted = false;
|
|
57
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
58
|
+
let idleMs = 0;
|
|
59
|
+
|
|
60
|
+
const clearIdle = () => {
|
|
61
|
+
clearTimeout(idleTimer);
|
|
62
|
+
idleTimer = undefined;
|
|
63
|
+
};
|
|
64
|
+
const resetMessage = () => {
|
|
65
|
+
rawCalls.clear();
|
|
66
|
+
flagged.clear();
|
|
67
|
+
padding.clear();
|
|
68
|
+
responseId = undefined;
|
|
69
|
+
streamProvider = undefined;
|
|
70
|
+
contentStarted = false;
|
|
71
|
+
clearIdle();
|
|
72
|
+
};
|
|
73
|
+
const resetRun = () => {
|
|
74
|
+
resetMessage();
|
|
75
|
+
stopped = false;
|
|
76
|
+
suspectReturned = false;
|
|
77
|
+
};
|
|
78
|
+
const cancelLookups = () => {
|
|
79
|
+
for (const controller of pending.keys()) controller.abort();
|
|
80
|
+
pending.clear();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Abort the turn once and tell the user which upstream provider failed. */
|
|
84
|
+
const stopTurn = (ctx: ExtensionContext, failure: UpstreamFailure) => {
|
|
85
|
+
if (stopped || !alive()) return;
|
|
86
|
+
stopped = true;
|
|
87
|
+
clearIdle();
|
|
88
|
+
const what = typeof failure === "object" ? `stalled ${failure.stalledSeconds} s` : failure;
|
|
89
|
+
pi.events.emit(TURN_FAILED_CHANNEL, { reason: `OpenRouter upstream failure: ${what}` } satisfies TurnFailedEvent);
|
|
90
|
+
ctx.abort();
|
|
91
|
+
if (!announce) return;
|
|
92
|
+
const model = ctx.model!;
|
|
93
|
+
const registry = ctx.modelRegistry;
|
|
94
|
+
const output = { hasUI: ctx.hasUI, ui: ctx.ui };
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
const named = streamProvider;
|
|
97
|
+
const notice = withKeepAlive(async () => {
|
|
98
|
+
const provider = named ?? await openRouterProviderName(responseId, async () => {
|
|
99
|
+
const auth = await registry.getApiKeyAndHeaders(model);
|
|
100
|
+
return auth.ok ? auth.apiKey : undefined;
|
|
101
|
+
}, controller.signal);
|
|
102
|
+
if (alive() && !controller.signal.aborted) notifyOrPrint(output, toolCallCorruptionNotice(model.id, provider, failure), "error");
|
|
103
|
+
}).finally(() => pending.delete(controller));
|
|
104
|
+
pending.set(controller, notice);
|
|
105
|
+
};
|
|
106
|
+
/** (Re)start the silence clock; a call still streaming arguments keeps the short window. */
|
|
107
|
+
const armIdle = (ctx: ExtensionContext) => {
|
|
108
|
+
if (stopped) return clearIdle();
|
|
109
|
+
const ms = rawCalls.size > 0 ? TOOL_CALL_IDLE_MS : contentStarted ? STREAM_IDLE_MS : FIRST_CONTENT_IDLE_MS;
|
|
110
|
+
if (idleTimer && ms === idleMs) {
|
|
111
|
+
idleTimer.refresh();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
clearIdle();
|
|
115
|
+
idleMs = ms;
|
|
116
|
+
idleTimer = setTimeout(() => {
|
|
117
|
+
idleTimer = undefined;
|
|
118
|
+
try {
|
|
119
|
+
stopTurn(ctx, { stalledSeconds: ms / 1000 });
|
|
120
|
+
} catch {
|
|
121
|
+
// A child session disposed mid-stream (no session_shutdown): its ctx throws, and there is nothing left to stop.
|
|
122
|
+
}
|
|
123
|
+
}, ms);
|
|
124
|
+
idleTimer.unref?.();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
pi.on("session_start", () => {
|
|
128
|
+
cancelLookups();
|
|
129
|
+
resetRun();
|
|
130
|
+
});
|
|
131
|
+
pi.on("session_shutdown", () => {
|
|
132
|
+
cancelLookups();
|
|
133
|
+
resetRun();
|
|
134
|
+
});
|
|
135
|
+
pi.on("agent_start", resetRun);
|
|
136
|
+
pi.on("message_start", (event, ctx) => {
|
|
137
|
+
if (event.message.role !== "assistant") return;
|
|
138
|
+
resetMessage();
|
|
139
|
+
if (alive() && isOpenRouter(ctx.model)) armIdle(ctx);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
pi.on("provider_stream_event", (event, ctx) => {
|
|
143
|
+
if (!alive() || !isOpenRouter(ctx.model)) return;
|
|
144
|
+
const provider = (event.data as { provider?: unknown } | null)?.provider;
|
|
145
|
+
if (typeof provider === "string" && provider.trim()) streamProvider = provider.trim();
|
|
146
|
+
// Any chunk is a sign of life, content or not (reasoning details, usage, an empty delta).
|
|
147
|
+
if (idleTimer) armIdle(ctx);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
pi.on("message_update", (event, ctx) => {
|
|
151
|
+
const update = event.assistantMessageEvent;
|
|
152
|
+
if (!alive() || stopped || update.type !== "toolcall_delta") return;
|
|
153
|
+
const state = trailingPadding(padding.get(update.contentIndex), update.delta);
|
|
154
|
+
padding.set(update.contentIndex, state);
|
|
155
|
+
if (!state.structural || state.run < RUNAWAY_PADDING_LIMIT) return;
|
|
156
|
+
stopped = true;
|
|
157
|
+
clearIdle();
|
|
158
|
+
const reason = `The model streamed over ${RUNAWAY_PADDING_LIMIT.toLocaleString("en-US")} characters of blank space into a tool call's arguments without finishing it, so the turn was stopped.`;
|
|
159
|
+
pi.events.emit(TURN_FAILED_CHANNEL, { reason } satisfies TurnFailedEvent);
|
|
160
|
+
ctx.abort();
|
|
161
|
+
if (announce) notifyOrPrint({ hasUI: ctx.hasUI, ui: ctx.ui }, `${reason} Send the request again, or switch models if it repeats.`, "error");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
pi.on("message_update", (event, ctx) => {
|
|
165
|
+
if (!alive() || !isOpenRouter(ctx.model)) return;
|
|
166
|
+
const update = event.assistantMessageEvent;
|
|
167
|
+
if (!("partial" in update)) return;
|
|
168
|
+
responseId = update.partial.responseId || responseId;
|
|
169
|
+
contentStarted = true;
|
|
170
|
+
switch (update.type) {
|
|
171
|
+
case "toolcall_start":
|
|
172
|
+
rawCalls.set(update.contentIndex, { raw: "", markup: 0, contentMarkup: 0, next: 0 });
|
|
173
|
+
break;
|
|
174
|
+
case "toolcall_delta": {
|
|
175
|
+
const call = rawCalls.get(update.contentIndex) ?? { raw: "", markup: 0, contentMarkup: 0, next: 0 };
|
|
176
|
+
call.raw += update.delta;
|
|
177
|
+
const scan = scanToolCallMarkup(call.raw, call.next);
|
|
178
|
+
call.markup += scan.count;
|
|
179
|
+
call.contentMarkup += scan.contentCount;
|
|
180
|
+
call.next = scan.next;
|
|
181
|
+
rawCalls.set(update.contentIndex, call);
|
|
182
|
+
// A leaked call can stream markup without end and never reach toolcall_end.
|
|
183
|
+
if (call.markup >= TOOL_CALL_MARKUP_LIMIT || call.contentMarkup >= CONTENT_MARKUP_LIMIT) return stopTurn(ctx, "corrupt-stream");
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case "toolcall_end": {
|
|
187
|
+
const raw = rawCalls.get(update.contentIndex)?.raw;
|
|
188
|
+
rawCalls.delete(update.contentIndex);
|
|
189
|
+
if (raw === undefined) break;
|
|
190
|
+
const damage = toolCallCorruptionReason(update.toolCall.name, raw);
|
|
191
|
+
if (damage) flagged.set(update.toolCall.id, damage);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
armIdle(ctx);
|
|
196
|
+
});
|
|
197
|
+
pi.on("message_end", (event, ctx) => {
|
|
198
|
+
if (event.message.role === "assistant") clearIdle();
|
|
199
|
+
if (alive() && isOpenRouter(ctx.model) && event.message.role === "assistant") {
|
|
200
|
+
responseId = event.message.responseId || responseId;
|
|
201
|
+
// A reply cut off at the token limit ends mid-call: the damage to its
|
|
202
|
+
// last block is the limit's, not the provider's. A call completed
|
|
203
|
+
// before it was judged whole and stays blocked.
|
|
204
|
+
const last = event.message.stopReason === "length" ? event.message.content.at(-1) : undefined;
|
|
205
|
+
if (last?.type === "toolCall") flagged.delete(last.id);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
pi.on("tool_call", (event, ctx) => {
|
|
209
|
+
if (!alive() || !isOpenRouter(ctx.model)) return;
|
|
210
|
+
const damage = flagged.get(event.toolCallId);
|
|
211
|
+
if (!damage) return;
|
|
212
|
+
if (damage.kind === "suspect" && !suspectReturned) {
|
|
213
|
+
suspectReturned = true;
|
|
214
|
+
flagged.delete(event.toolCallId);
|
|
215
|
+
return { block: true, reason: `${damage.reason} It was not run. Send the complete command again; if it really ends there, drop the trailing blank or the empty option.` };
|
|
216
|
+
}
|
|
217
|
+
const repeat = damage.kind === "suspect" ? " An earlier command in this run came through cut the same way." : "";
|
|
218
|
+
flagged.set(event.toolCallId, { kind: "corrupt", reason: damage.reason });
|
|
219
|
+
return {
|
|
220
|
+
block: true,
|
|
221
|
+
reason: `OpenRouter tool-call corruption: ${damage.reason}${repeat} Do not retry this call; switch models or exclude the upstream provider.`,
|
|
222
|
+
terminate: true,
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
226
|
+
if (!alive() || stopped || !isOpenRouter(ctx.model) || flagged.get(event.toolCallId)?.kind !== "corrupt") return;
|
|
227
|
+
// Aborting inside tool_call would replace our reason with "Operation aborted".
|
|
228
|
+
// This also catches schema-validation failures, which bypass tool_call entirely.
|
|
229
|
+
stopTurn(ctx, "blocked-call");
|
|
230
|
+
});
|
|
231
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
232
|
+
clearIdle();
|
|
233
|
+
// Print/JSON dispose the session immediately after agent_end. The turn is
|
|
234
|
+
// already aborted; flush its bounded diagnostic before one-shot teardown.
|
|
235
|
+
if (!sessionOutlivesTurn(ctx.mode)) await Promise.all(pending.values());
|
|
236
|
+
});
|
|
237
|
+
}
|
|
@@ -31,6 +31,22 @@ export function planAnnouncement(input: {
|
|
|
31
31
|
return added.length === 0 ? { kind: "none" } : { kind: "addendum", added };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** JSON-safe persisted baseline for the names already described to the model. */
|
|
35
|
+
export function deferredToolsBaseline(value: unknown): string[] | undefined {
|
|
36
|
+
if (!Array.isArray(value) || !value.every((name) => typeof name === "string")) return undefined;
|
|
37
|
+
return [...new Set(value)];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Compatibility fallback for a v1 stack written before structured baselines. */
|
|
41
|
+
export function deferredToolsFromReminder(text: string | undefined): string[] | undefined {
|
|
42
|
+
if (!text) return undefined;
|
|
43
|
+
const marker = "\n";
|
|
44
|
+
const lines = text.split(marker);
|
|
45
|
+
return lines.length > 1 && lines[0].startsWith("The following deferred tools are available via tool_search")
|
|
46
|
+
? lines.slice(1).filter(Boolean)
|
|
47
|
+
: undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
/** Bring the announced set in line with what the plan told the model. */
|
|
35
51
|
export function applyAnnouncement(announced: Set<string>, plan: AnnouncementPlan): void {
|
|
36
52
|
if (plan.kind === "rewrite") {
|
|
@@ -33,23 +33,32 @@ import {
|
|
|
33
33
|
deferredAddendumText,
|
|
34
34
|
deferredMissReminderText,
|
|
35
35
|
DeferredRegistry,
|
|
36
|
+
deferralEnabled,
|
|
36
37
|
deferredReminderText,
|
|
38
|
+
loaderUnavailableReminderText,
|
|
37
39
|
type DeferRequest,
|
|
38
40
|
resultText,
|
|
39
41
|
searchTools,
|
|
40
|
-
|
|
42
|
+
selectedRequests,
|
|
43
|
+
type ResponsesToolLoadModel,
|
|
41
44
|
stabilizeDeferredTools,
|
|
45
|
+
stabilizeResponsesToolLoads,
|
|
46
|
+
responsesToolLoadCompat,
|
|
42
47
|
supportsToolReferences,
|
|
43
48
|
toolNotFoundName,
|
|
44
49
|
toolSearchLoads,
|
|
50
|
+
WITHHOLD_CHANNEL,
|
|
51
|
+
withheldMissReminderText,
|
|
45
52
|
} from "../lib/deferred.ts";
|
|
46
|
-
import { looksLikeAnthropicRequest } from "../lib/anthropic-payload.ts";
|
|
53
|
+
import { looksLikeAnthropicRequest, MID_CONVERSATION_SYSTEM_BETA, withBetas } from "../lib/anthropic-payload.ts";
|
|
54
|
+
import { addendumNamesOnBranch, liftAddenda, supportsToolAdditions, TOOL_ADDITION_BETAS, withToolAdditions } from "../lib/tool-additions.ts";
|
|
47
55
|
import { MCP_TOOLS_CHANNEL, type McpToolsPayload } from "../lib/mcp-share.ts";
|
|
48
56
|
import { CONTEXT_ORDER, REMINDER_CHANNEL } from "../lib/reminders.ts";
|
|
49
57
|
import { sessionAlive } from "../lib/session-lifecycle.ts";
|
|
50
58
|
import { ccToolRenderers } from "../lib/tui-render.ts";
|
|
51
59
|
import { normalizeToolName } from "../permissions/matcher.ts";
|
|
52
|
-
import { applyAnnouncement, planAnnouncement } from "./announce.ts";
|
|
60
|
+
import { applyAnnouncement, deferredToolsBaseline, deferredToolsFromReminder, planAnnouncement } from "./announce.ts";
|
|
61
|
+
import { CONTEXT_BASELINE_CHANNEL, restoredContext } from "../lib/context-stack.ts";
|
|
53
62
|
import { registerLocalCommand } from "../lib/local-command.ts";
|
|
54
63
|
|
|
55
64
|
/** Coalesces the burst of per-tool defers one server emits into one listing update. */
|
|
@@ -58,6 +67,12 @@ const ANNOUNCE_DEBOUNCE_MS = 100;
|
|
|
58
67
|
export default function toolSearchExtension(pi: ExtensionAPI) {
|
|
59
68
|
// Owned here (see lib/deferred.ts): the only instance, fed over DEFER_CHANNEL.
|
|
60
69
|
const deferredRegistry = new DeferredRegistry();
|
|
70
|
+
// Names WITHHOLD_CHANNEL removed from the registry, kept only so a direct
|
|
71
|
+
// call to one still gets an explanation (tool_execution_end below) instead
|
|
72
|
+
// of a bare "not found": the registry itself can't answer that, since
|
|
73
|
+
// removal is what makes the name stop being deferred (searchable/loadable).
|
|
74
|
+
// A re-defer (a model change bringing it back) clears the name from here.
|
|
75
|
+
const withheldNames = new Set<string>();
|
|
61
76
|
let sessionStarted = false;
|
|
62
77
|
// The deferred-tools listing is a first-prepend reminder on message 1, part of
|
|
63
78
|
// the cached prefix of every later request. Before the first request nothing
|
|
@@ -77,21 +92,36 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
|
|
|
77
92
|
const alive = sessionAlive(pi);
|
|
78
93
|
/** Names the model has been told about: the frozen listing plus every addendum. */
|
|
79
94
|
const announced = new Set<string>();
|
|
95
|
+
/** The session's model as last seen, for an announcement made outside any hook. */
|
|
96
|
+
let latestModel: { provider?: string; api?: string; compat?: unknown } | undefined;
|
|
80
97
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
98
|
+
/** Whether tool_search is registered; a `--tools` list without it turns deferral off (lib/deferred.ts). */
|
|
99
|
+
const deferring = () => deferralEnabled(pi.getAllTools());
|
|
100
|
+
const searchableTools = () => {
|
|
101
|
+
const all = pi.getAllTools();
|
|
102
|
+
if (!deferralEnabled(all)) return [];
|
|
103
|
+
return all
|
|
84
104
|
.filter((tool) => deferredRegistry.has(tool.name))
|
|
85
105
|
.map((tool) => ({
|
|
86
106
|
name: tool.name,
|
|
87
107
|
description: tool.description ?? "",
|
|
88
108
|
keywords: deferredRegistry.keywordsFor(tool.name),
|
|
89
109
|
}));
|
|
110
|
+
};
|
|
90
111
|
|
|
112
|
+
const publishBaseline = (names: ReadonlySet<string>) => {
|
|
113
|
+
pi.events.emit(CONTEXT_BASELINE_CHANNEL, { key: "tool-search", value: [...names] });
|
|
114
|
+
};
|
|
91
115
|
const announce = () => {
|
|
92
116
|
if (!alive()) return;
|
|
93
117
|
const available = searchableTools();
|
|
94
118
|
const plan = planAnnouncement({ requestSent, announced, available: available.map((t) => t.name) });
|
|
119
|
+
// Store the post-announcement state before the next context pass can place it.
|
|
120
|
+
if (plan.kind !== "none") {
|
|
121
|
+
const next = new Set(announced);
|
|
122
|
+
applyAnnouncement(next, plan);
|
|
123
|
+
publishBaseline(next);
|
|
124
|
+
}
|
|
95
125
|
if (plan.kind === "rewrite") {
|
|
96
126
|
pi.events.emit(REMINDER_CHANNEL, {
|
|
97
127
|
scope: "every-turn",
|
|
@@ -104,6 +134,12 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
|
|
|
104
134
|
// Unkeyed on purpose: a keyed next-turn reminder replaces its
|
|
105
135
|
// predecessor, and an addendum replaced before delivery would lose names.
|
|
106
136
|
pi.events.emit(REMINDER_CHANNEL, { text: deferredAddendumText(plan.added) });
|
|
137
|
+
// Where the request carries it as Claude Code's tool_addition, the model
|
|
138
|
+
// calls the tool directly, so pi's dispatcher must already accept it.
|
|
139
|
+
if (supportsToolAdditions(latestModel)) {
|
|
140
|
+
for (const name of plan.added) loadedBefore.add(name);
|
|
141
|
+
pi.setActiveTools([...new Set([...pi.getActiveTools(), ...plan.added])]);
|
|
142
|
+
}
|
|
107
143
|
}
|
|
108
144
|
applyAnnouncement(announced, plan);
|
|
109
145
|
};
|
|
@@ -137,8 +173,15 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
|
|
|
137
173
|
}
|
|
138
174
|
});
|
|
139
175
|
|
|
140
|
-
|
|
176
|
+
/** The addenda the last `context` pass lifted off tool results, by call id (lib/tool-additions.ts). */
|
|
177
|
+
let addendaByCall = new Map<string, string[]>();
|
|
178
|
+
pi.on("context", (event, ctx) => {
|
|
141
179
|
requestSent = true;
|
|
180
|
+
addendaByCall = new Map();
|
|
181
|
+
if (!supportsToolAdditions(ctx?.model) || !Array.isArray(event?.messages)) return undefined;
|
|
182
|
+
const lifted = liftAddenda(event.messages);
|
|
183
|
+
addendaByCall = lifted.byCall;
|
|
184
|
+
return lifted.messages === event.messages ? undefined : { messages: lifted.messages };
|
|
142
185
|
});
|
|
143
186
|
|
|
144
187
|
// Every tool_search load of this session (call id → names), seeded from the
|
|
@@ -162,18 +205,44 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
|
|
|
162
205
|
// the eager list, re-caching everything from the tools block down — is
|
|
163
206
|
// demoted back to deferred and loaded through `tool_reference` blocks in the
|
|
164
207
|
// tool_search result that activated it (lib/deferred.ts stabilizeDeferredTools
|
|
165
|
-
// has the rules; docs/decisions/caching.md the measurements).
|
|
208
|
+
// has the rules; working-docs/decisions/caching.md the measurements).
|
|
209
|
+
// On OpenAI Responses models with native tool loading (GPT-5.4 and newer on
|
|
210
|
+
// openai and openai-codex) a loaded tool leaves `tools` for pi-ai's own
|
|
211
|
+
// load item at the tool_search result instead (stabilizeResponsesToolLoads).
|
|
166
212
|
pi.on("before_provider_request", (event, ctx) => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
213
|
+
latestModel = ctx.model;
|
|
214
|
+
const allTools = pi.getAllTools();
|
|
215
|
+
const enabled = deferralEnabled(allTools);
|
|
216
|
+
const toolLoadCompat = responsesToolLoadCompat(ctx.model as ResponsesToolLoadModel | undefined);
|
|
217
|
+
if (toolLoadCompat) {
|
|
218
|
+
if (!enabled) return undefined;
|
|
219
|
+
return stabilizeResponsesToolLoads(event.payload as Record<string, unknown>, (name) => deferredRegistry.has(name), loads, toolLoadCompat);
|
|
220
|
+
}
|
|
221
|
+
const original = event.payload as Record<string, unknown>;
|
|
222
|
+
const additions = supportsToolAdditions(ctx.model) && looksLikeAnthropicRequest(original);
|
|
223
|
+
// The context step lifted this request's addenda off their tool results;
|
|
224
|
+
// whatever happens below, they go out (lib/tool-additions.ts).
|
|
225
|
+
const announceAsText = () => {
|
|
226
|
+
const text = additions ? withToolAdditions(original, addendaByCall, false) : undefined;
|
|
227
|
+
return text ? withBetas(text.payload, [MID_CONVERSATION_SYSTEM_BETA]) : undefined;
|
|
228
|
+
};
|
|
229
|
+
if (!enabled || !supportsToolReferences(ctx.model as { provider?: string; id?: string } | undefined) || !looksLikeAnthropicRequest(original)) return announceAsText();
|
|
230
|
+
const isDeferred = (name: string) => deferredRegistry.has(name);
|
|
231
|
+
// A tool that arrived mid-session goes out as Claude Code's tool_addition
|
|
232
|
+
// where the model takes one, declared deferred.
|
|
233
|
+
const lifted = additions ? withToolAdditions(original, addendaByCall) : undefined;
|
|
234
|
+
const stable = stabilizeDeferredTools(lifted?.payload ?? original, allTools, isDeferred, loads, new Set(lifted?.names ?? []));
|
|
235
|
+
// Renamed (OAuth) tools cannot be referenced: the active tools stay as pi
|
|
236
|
+
// rendered them and the addition goes as text.
|
|
237
|
+
if (!stable) return announceAsText();
|
|
238
|
+
return lifted ? withBetas(stable, TOOL_ADDITION_BETAS) : stable;
|
|
170
239
|
});
|
|
171
240
|
|
|
172
241
|
|
|
173
242
|
/** Deactivate every deferred-registry tool the transcript has not loaded, and announce the loadable set. */
|
|
174
243
|
const deferAll = () => {
|
|
175
244
|
const deferred = new Set(deferredRegistry.names);
|
|
176
|
-
if (deferred.size === 0) return;
|
|
245
|
+
if (deferred.size === 0 || !deferring()) return;
|
|
177
246
|
const active = pi.getActiveTools();
|
|
178
247
|
const next = active.filter((name) => !deferred.has(name) || loadedBefore.has(name));
|
|
179
248
|
if (next.length !== active.length) pi.setActiveTools(next);
|
|
@@ -183,11 +252,12 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
|
|
|
183
252
|
pi.events.on(DEFER_CHANNEL, (data) => {
|
|
184
253
|
const request = data as DeferRequest;
|
|
185
254
|
deferredRegistry.add(request);
|
|
255
|
+
if (request?.name) withheldNames.delete(request.name);
|
|
186
256
|
// A defer arriving after the session_start pass (MCP servers connect
|
|
187
257
|
// asynchronously and register their tools then) would otherwise leave the
|
|
188
258
|
// tool eager AND unlisted in the reminder. Deactivate it (unless the
|
|
189
259
|
// transcript loaded it) and refresh the keyed reminder.
|
|
190
|
-
if (sessionStarted && request?.name) {
|
|
260
|
+
if (sessionStarted && request?.name && deferring()) {
|
|
191
261
|
const active = pi.getActiveTools();
|
|
192
262
|
if (active.includes(request.name) && !loadedBefore.has(request.name)) {
|
|
193
263
|
pi.setActiveTools(active.filter((name) => name !== request.name));
|
|
@@ -198,19 +268,51 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
|
|
|
198
268
|
}
|
|
199
269
|
});
|
|
200
270
|
|
|
271
|
+
pi.events.on(WITHHOLD_CHANNEL, (data) => {
|
|
272
|
+
const name = (data as { name?: string } | undefined)?.name;
|
|
273
|
+
if (!name || !deferredRegistry.remove(name)) return;
|
|
274
|
+
withheldNames.add(name);
|
|
275
|
+
if (!alive()) return;
|
|
276
|
+
const active = pi.getActiveTools();
|
|
277
|
+
if (active.includes(name)) pi.setActiveTools(active.filter((n) => n !== name));
|
|
278
|
+
// Before the first request the listing is still free to change: rewrite it
|
|
279
|
+
// now, synchronously, so a request going out right after session_start
|
|
280
|
+
// never names the withheld tool. After it the frozen listing stands.
|
|
281
|
+
if (sessionStarted && !requestSent) announce();
|
|
282
|
+
});
|
|
283
|
+
|
|
201
284
|
pi.on("session_start", (_event, ctx) => {
|
|
202
285
|
sessionStarted = true;
|
|
203
286
|
// A pending debounce belongs to the previous session's listing (RPC's
|
|
204
287
|
// new_session emits session_start twice, findings §3); deferAll below
|
|
205
288
|
// announces the fresh one.
|
|
206
289
|
clearAnnounce();
|
|
207
|
-
|
|
290
|
+
latestModel = ctx?.model;
|
|
291
|
+
const branch = ctx?.sessionManager?.getBranch?.() ?? [];
|
|
292
|
+
loads = toolSearchLoads(branch);
|
|
208
293
|
loadedBefore = new Set([...loads.values()].flat());
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
294
|
+
// Tools added mid-session by tool_addition stay callable after a resume.
|
|
295
|
+
if (supportsToolAdditions(latestModel)) for (const name of addendumNamesOnBranch(branch)) loadedBefore.add(name);
|
|
296
|
+
const restored = restoredContext(pi.events);
|
|
297
|
+
// A restored stack is already message 1. Seed its announced names so a live
|
|
298
|
+
// capability change becomes one addendum, not a rewrite or duplicate notice.
|
|
299
|
+
requestSent = restored !== undefined;
|
|
212
300
|
announced.clear();
|
|
301
|
+
const baseline = deferredToolsBaseline(restored?.baselines["tool-search"]);
|
|
302
|
+
const legacy = deferredToolsFromReminder(restored?.stack.find((entry) => (entry as { key?: string }).key === "deferred-tools")?.text);
|
|
303
|
+
for (const name of baseline ?? legacy ?? []) announced.add(name);
|
|
304
|
+
// Native tool_addition entries can outlive an older baseline snapshot; they
|
|
305
|
+
// already told the model about these names, so merge them before planning.
|
|
306
|
+
for (const name of addendumNamesOnBranch(branch)) announced.add(name);
|
|
213
307
|
deferAll();
|
|
308
|
+
// A restored message 1 keeps its listing (its bytes are locked), so when
|
|
309
|
+
// this run's tool list leaves tool_search out, say once that it is gone.
|
|
310
|
+
if (restored && announced.size > 0 && !deferring()) {
|
|
311
|
+
pi.events.emit(REMINDER_CHANNEL, { scope: "next-turn", key: "deferred-unavailable", text: loaderUnavailableReminderText() });
|
|
312
|
+
}
|
|
313
|
+
// Even an absent listing is meaningful on resume: preserve that baseline so
|
|
314
|
+
// the first later deferred tool is a tail notice, not a message-1 rewrite.
|
|
315
|
+
if (!restored) publishBaseline(announced);
|
|
214
316
|
});
|
|
215
317
|
|
|
216
318
|
pi.on("session_shutdown", clearAnnounce);
|
|
@@ -228,15 +330,28 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
|
|
|
228
330
|
// Map a Claude Code spelling (a direct `NotebookEdit` call) to our name so
|
|
229
331
|
// the miss is recognised and the steer names the loadable tool (M1).
|
|
230
332
|
const name = raw ? normalizeToolName(raw) : undefined;
|
|
231
|
-
if (!name
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
333
|
+
if (!name) return;
|
|
334
|
+
// Without tool_search there is nothing to steer a deferred-tool miss to.
|
|
335
|
+
if (deferredRegistry.has(name) && deferring()) {
|
|
336
|
+
pi.events.emit(REMINDER_CHANNEL, {
|
|
337
|
+
scope: "next-turn",
|
|
338
|
+
key: `deferred-miss-${name}`,
|
|
339
|
+
text: deferredMissReminderText(name),
|
|
340
|
+
});
|
|
341
|
+
} else if (withheldNames.has(name)) {
|
|
342
|
+
// A withheld tool is gone from the registry too (WITHHOLD_CHANNEL), so
|
|
343
|
+
// the ordinary steer above would send the model to tool_search for a
|
|
344
|
+
// name that will not be found there either.
|
|
345
|
+
pi.events.emit(REMINDER_CHANNEL, {
|
|
346
|
+
scope: "next-turn",
|
|
347
|
+
key: `deferred-miss-${name}`,
|
|
348
|
+
text: withheldMissReminderText(name),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
237
351
|
});
|
|
238
352
|
|
|
239
353
|
pi.registerTool({
|
|
354
|
+
// A literal, like every registration: agent-view-tool-labels.test.ts reads them from source.
|
|
240
355
|
name: "tool_search",
|
|
241
356
|
label: "Tool Search",
|
|
242
357
|
...ccToolRenderers("Tool Search"),
|
|
@@ -257,14 +372,34 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
|
|
|
257
372
|
// silently dropped before — the model was told the rest "Loaded" and only
|
|
258
373
|
// discovered the miss later as an opaque InputValidationError. Always
|
|
259
374
|
// surface the unmatched names.
|
|
260
|
-
const requested =
|
|
261
|
-
|
|
262
|
-
|
|
375
|
+
const requested = selectedRequests(params.query);
|
|
376
|
+
// A requested tool that is already callable (an eager tool, or one loaded
|
|
377
|
+
// earlier) is a harmless no-op, as in Claude Code: the model proceeds
|
|
378
|
+
// instead of retrying a name it was told does not exist.
|
|
379
|
+
const active = pi.getActiveTools();
|
|
380
|
+
const alreadyActive = (name: string) => active.find((tool) => tool.toLowerCase() === name);
|
|
381
|
+
const unmatched = requested
|
|
382
|
+
? requested.filter((request) => !matches.some((m) => m.name.toLowerCase() === request.name))
|
|
263
383
|
: [];
|
|
384
|
+
const eager = [...new Set(unmatched.flatMap((request) => alreadyActive(request.name) ?? []))];
|
|
385
|
+
const notFound = unmatched.filter((request) => !alreadyActive(request.name));
|
|
386
|
+
// A withheld name can still sit in the frozen listing (withdrawn by a model
|
|
387
|
+
// change after the first request), so it gets "do not retry", not a
|
|
388
|
+
// spelling hint. Both notes quote the model's own spelling.
|
|
389
|
+
const withdrawn = [...new Set(notFound.filter((n) => withheldNames.has(n.name)).map((n) => n.asWritten))];
|
|
390
|
+
const missing = [...new Set(notFound.filter((n) => !withheldNames.has(n.name)).map((n) => n.asWritten))];
|
|
264
391
|
const notFoundNote =
|
|
265
|
-
|
|
266
|
-
? ` Not found (not deferred tool names — check spelling, or search by keyword instead of \`select:\`): ${
|
|
267
|
-
: ""
|
|
392
|
+
(missing.length > 0
|
|
393
|
+
? ` Not found (not deferred tool names — check spelling, or search by keyword instead of \`select:\`): ${missing.join(", ")}.`
|
|
394
|
+
: "") +
|
|
395
|
+
(withdrawn.length > 0 ? ` Withdrawn for the current model (do not retry): ${withdrawn.join(", ")}.` : "");
|
|
396
|
+
|
|
397
|
+
if (matches.length === 0 && eager.length > 0) {
|
|
398
|
+
return {
|
|
399
|
+
content: [{ type: "text", text: `Already loaded: ${eager.join(", ")}.${notFoundNote}` }],
|
|
400
|
+
details: { matches: eager, added: [] as string[], notFound, refused: [] as string[] },
|
|
401
|
+
};
|
|
402
|
+
}
|
|
268
403
|
|
|
269
404
|
if (matches.length === 0) {
|
|
270
405
|
const names = available.map((t) => t.name).join(", ") || "(none)";
|
|
@@ -272,30 +407,39 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
|
|
|
272
407
|
content: [
|
|
273
408
|
{ type: "text", text: `No tools matched "${params.query}".${notFoundNote} Tools that can be loaded: ${names}` },
|
|
274
409
|
],
|
|
275
|
-
details: { matches: [] as string[], added: [] as string[], notFound },
|
|
410
|
+
details: { matches: [] as string[], added: [] as string[], notFound, refused: [] as string[] },
|
|
276
411
|
isError: true,
|
|
277
412
|
};
|
|
278
413
|
}
|
|
279
414
|
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
415
|
+
const matchNames = matches.map((m) => m.name);
|
|
416
|
+
const toActivate = matchNames.filter((name) => !active.includes(name));
|
|
417
|
+
if (toActivate.length > 0) pi.setActiveTools([...new Set([...active, ...toActivate])]);
|
|
418
|
+
// pi can decline to activate a tool: since 1.0.4 a `--tools` list that
|
|
419
|
+
// names no `mcp__*` entry keeps the MCP tools registered but not
|
|
420
|
+
// activatable. Report what pi actually activated, never a plausible load.
|
|
421
|
+
const now = new Set(pi.getActiveTools());
|
|
422
|
+
const added = toActivate.filter((name) => now.has(name));
|
|
423
|
+
const refused = toActivate.filter((name) => !now.has(name));
|
|
424
|
+
if (added.length > 0) loads.set(toolCallId, added);
|
|
286
425
|
|
|
287
|
-
const loaded =
|
|
426
|
+
const loaded = [...new Set([...matchNames, ...eager])].filter((name) => now.has(name));
|
|
427
|
+
const already = loaded.filter((name) => !added.includes(name));
|
|
428
|
+
const refusedNote =
|
|
429
|
+
refused.length > 0 ? ` Could not load ${refused.join(", ")}: this session's tool list (pi's --tools) leaves them out, so they cannot be called.` : "";
|
|
288
430
|
return {
|
|
289
431
|
content: [
|
|
290
432
|
{
|
|
291
433
|
type: "text",
|
|
292
434
|
text:
|
|
293
|
-
(
|
|
294
|
-
? `Loaded ${added.join(", ")}. These tools are now callable.`
|
|
295
|
-
|
|
435
|
+
([
|
|
436
|
+
added.length > 0 ? `Loaded ${added.join(", ")}. These tools are now callable.` : "",
|
|
437
|
+
already.length > 0 ? `Already loaded: ${already.join(", ")}.` : "",
|
|
438
|
+
].filter(Boolean).join(" ") + refusedNote + notFoundNote).trim(),
|
|
296
439
|
},
|
|
297
440
|
],
|
|
298
|
-
details: { matches: loaded, added, notFound },
|
|
441
|
+
details: { matches: loaded, added, notFound, refused },
|
|
442
|
+
isError: loaded.length === 0,
|
|
299
443
|
};
|
|
300
444
|
},
|
|
301
445
|
});
|
|
@@ -303,6 +447,10 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
|
|
|
303
447
|
registerLocalCommand(pi, "tools-deferred", {
|
|
304
448
|
description: "Show which tools are deferred (loadable via tool_search)",
|
|
305
449
|
handler: async (args, ctx) => {
|
|
450
|
+
if (!deferring()) {
|
|
451
|
+
ctx.ui.notify("tool_search is not registered (a --tools list leaves it out), so no tool is deferred.", "info");
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
306
454
|
const available = searchableTools();
|
|
307
455
|
const activeSet = new Set(pi.getActiveTools());
|
|
308
456
|
const lines = available.map((t) => `${activeSet.has(t.name) ? "loaded " : "deferred"} ${t.name}`);
|