one-code-extension 0.4.0 → 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 +219 -219
- 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 +66 -34
- 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 +19 -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
|
@@ -12,19 +12,23 @@
|
|
|
12
12
|
* `<sessionDir>/subagents/<taskId>/`, so a finished run can be resumed from disk.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { childWriteWatcher } from "../lib/child-writes.ts";
|
|
15
16
|
import { createHash } from "node:crypto";
|
|
16
|
-
import {
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
18
|
import { type AgentSession, type ExtensionError, getAgentDir, SessionManager, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
19
|
+
import type { Api, Message, Model } from "@earendil-works/pi-ai";
|
|
20
20
|
import { whenAborted } from "../lib/abort.ts";
|
|
21
|
-
import { type AgentLoaderOptions, buildAgentLoader, createSharedModelRuntime, openChildSession } from "../lib/agent-loader.ts";
|
|
21
|
+
import { type AgentLoaderOptions, buildAgentLoader, createSharedModelRuntime, disposeChildSession, openChildSession } from "../lib/agent-loader.ts";
|
|
22
22
|
import { agentPromptIdentity, PrefixWarmGate, prefixWarmKey, type Release } from "../lib/prefix-warm-gate.ts";
|
|
23
23
|
import type { PermissionBridge } from "../permissions/subagent-gate.ts";
|
|
24
24
|
import type { HookBridge } from "../hooks/subagent-bridge.ts";
|
|
25
25
|
import { findConfigured, modelSpec } from "../lib/model-policy.ts";
|
|
26
26
|
import { isModelUnavailableError } from "../auto-mode/model-select.ts";
|
|
27
|
-
import { type AgentDefinition,
|
|
27
|
+
import { type AgentDefinition, agentToolOptions, unusableAllowlistError } from "./agents.ts";
|
|
28
|
+
import { CHILD_EXTENSION_PATHS } from "../lib/child-extensions.ts";
|
|
29
|
+
import { type HandbackSlot, SUBAGENT_HANDBACK, subagentHandbackExtension } from "../lib/subagent-handback.ts";
|
|
30
|
+
import type { RequestCapture } from "../lib/request-replay.ts";
|
|
31
|
+
import { forkCacheExtension } from "./fork-cache.ts";
|
|
28
32
|
import type { ChildHandle, ChildOutcome, RpcChildHandle } from "./outcome.ts";
|
|
29
33
|
import { sendToMainTool } from "./send-to-main-tool.ts";
|
|
30
34
|
import { SessionTurnTracker } from "./session-turns.ts";
|
|
@@ -53,34 +57,12 @@ const WALL_CLOCK_CAP_MS = 30 * 60 * 1000;
|
|
|
53
57
|
|
|
54
58
|
/**
|
|
55
59
|
* Tools the runtime injects into a child; the permission gate must never gate
|
|
56
|
-
* them. `
|
|
60
|
+
* them. `SubagentHandback` only returns the child's report to its caller.
|
|
61
|
+
* `Agent` is the injected child-spawn tool (index.ts) — the spawn itself
|
|
57
62
|
* is free (matching the main conversation's auto-allowed Agent tool); every
|
|
58
63
|
* tool call the spawned grandchild makes still runs through the real gate.
|
|
59
64
|
*/
|
|
60
|
-
const NEVER_GATE = new Set(["structured_output", "SendMessage", "Agent"]);
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* The curated extensions loaded into a subagent session (via additionalExtensionPaths,
|
|
64
|
-
* which the loader loads even under noExtensions). Broadly Claude Code's model — a
|
|
65
|
-
* subagent gets project context + freshness + a working toolset, but NOT the frontier
|
|
66
|
-
* chrome (banner/spinner/recap) or the orchestration EXTENSIONS. Nested spawning
|
|
67
|
-
* (CC parity: subagents spawn subagents) is instead an injected `Agent` custom tool
|
|
68
|
-
* (index.ts childAgentTool, passed via extraTools) that delegates to the PARENT
|
|
69
|
-
* extension's runtime — one registry, one panel, one permission gate — and is
|
|
70
|
-
* depth-capped there.
|
|
71
|
-
*
|
|
72
|
-
* `lsp` is deliberately NOT here (matching CC, findings §17.3): a child session is torn
|
|
73
|
-
* down with the raw AgentSession.dispose(), which never fires session_shutdown, so lsp's
|
|
74
|
-
* cleanup would never run and any language server it started would leak for the life of
|
|
75
|
-
* the parent session. MCP is also not listed: its
|
|
76
|
-
* tools are shared in from the parent as customTools (getMcpTools), not reconnected.
|
|
77
|
-
*
|
|
78
|
-
* Order mirrors the package's load order: reminder/deferral sinks (system-reminder,
|
|
79
|
-
* tool-search) first, before anything emitting on their channels (the bus does not replay).
|
|
80
|
-
*/
|
|
81
|
-
const EXTENSIONS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
82
|
-
const CHILD_EXTENSIONS = ["system-reminder", "tool-search", "claude-context", "file-tracker", "search-tools", "skill", "web", "web-fetch", "notebook"];
|
|
83
|
-
const CHILD_EXTENSION_PATHS = CHILD_EXTENSIONS.map((name) => join(EXTENSIONS_DIR, name, "index.ts"));
|
|
65
|
+
const NEVER_GATE = new Set(["structured_output", "SendMessage", SUBAGENT_HANDBACK, "Agent"]);
|
|
84
66
|
|
|
85
67
|
type Session = AgentSession;
|
|
86
68
|
|
|
@@ -92,8 +74,15 @@ interface ChildSessionSpec {
|
|
|
92
74
|
agent?: AgentDefinition;
|
|
93
75
|
/** Parent session file — present for a fork run (inherit the parent transcript). */
|
|
94
76
|
forkFrom?: string;
|
|
77
|
+
/** Messages appended to a fork's transcript before its task (e.g. `/btw`'s side exchange). */
|
|
78
|
+
forkMessages?: Message[];
|
|
95
79
|
/** The parent's current system prompt, applied to a fork so it continues as the parent. */
|
|
96
80
|
parentSystemPrompt?: string;
|
|
81
|
+
/**
|
|
82
|
+
* The parent's last provider request, for a fork: every fork request is
|
|
83
|
+
* built from it so the fork reads the parent's prompt cache (fork-cache.ts).
|
|
84
|
+
*/
|
|
85
|
+
parentRequest?: RequestCapture;
|
|
97
86
|
/** Existing persisted session to resume (SendMessage to a finished agent). */
|
|
98
87
|
sessionFile?: string;
|
|
99
88
|
/** Where a new run's persisted session lands; falls back to an in-memory session. */
|
|
@@ -111,6 +100,12 @@ interface ChildSessionSpec {
|
|
|
111
100
|
onMessageToMain?: (message: string, summary?: string) => void;
|
|
112
101
|
/** Extra injected tools (e.g. the child-spawn Agent tool for depth-0 children). */
|
|
113
102
|
extraTools?: ToolDefinition[];
|
|
103
|
+
/**
|
|
104
|
+
* The main session's tool call that started the run's current turn, read
|
|
105
|
+
* per gated call (a SendMessage can start a later turn). The permission
|
|
106
|
+
* bridge passes it on as `ChildToolCall.parentToolCallId`.
|
|
107
|
+
*/
|
|
108
|
+
mainToolCallId?: () => string | undefined;
|
|
114
109
|
}
|
|
115
110
|
|
|
116
111
|
export interface SubagentRunOptions extends ChildSessionSpec {
|
|
@@ -125,11 +120,33 @@ export interface ResidentRunOptions extends Omit<ChildSessionSpec, "sessionFile"
|
|
|
125
120
|
onProgress: (toolCalls: number, lastText: string, usage: UsageTotals) => void;
|
|
126
121
|
/** Fires at the end of EVERY turn (initial task and later messages alike). */
|
|
127
122
|
onTurnEnd: (outcome: ChildOutcome) => void;
|
|
128
|
-
|
|
123
|
+
/** Fires once when the resident exits; a returned promise (worktree cleanup) is part of kill()'s settle. */
|
|
124
|
+
onExit?: () => void | Promise<unknown>;
|
|
129
125
|
/** Optional live sink for the subagent panel (activity + transcript blocks). */
|
|
130
126
|
sink?: LiveSink;
|
|
131
127
|
}
|
|
132
128
|
|
|
129
|
+
/**
|
|
130
|
+
* A child's session manager: a resume reopens its file; otherwise a fork
|
|
131
|
+
* clones the parent transcript (when the parent has written one) and anything
|
|
132
|
+
* else starts empty, and `forkMessages` are appended so the child sees them before
|
|
133
|
+
* its task and a later resume finds them in its file. Exported for tests.
|
|
134
|
+
*/
|
|
135
|
+
export function newChildSessionManager(spec: Pick<ChildSessionSpec, "cwd" | "forkFrom" | "forkMessages" | "sessionFile" | "sessionDir">): SessionManager {
|
|
136
|
+
// The resume cwd is passed explicitly: a worktree run's persisted cwd may
|
|
137
|
+
// be gone by the time it is messaged (index.ts substitutes the parent cwd).
|
|
138
|
+
if (spec.sessionFile) return SessionManager.open(spec.sessionFile, undefined, spec.cwd);
|
|
139
|
+
// pi writes a session file with its first assistant reply, so a parent with
|
|
140
|
+
// no transcript yet (or run with --no-session) has nothing to clone.
|
|
141
|
+
const manager = spec.forkFrom && existsSync(spec.forkFrom)
|
|
142
|
+
? SessionManager.forkFrom(spec.forkFrom, spec.cwd, spec.sessionDir)
|
|
143
|
+
: spec.sessionDir
|
|
144
|
+
? SessionManager.create(spec.cwd, spec.sessionDir)
|
|
145
|
+
: SessionManager.inMemory(spec.cwd);
|
|
146
|
+
for (const message of spec.forkMessages ?? []) manager.appendMessage(message);
|
|
147
|
+
return manager;
|
|
148
|
+
}
|
|
149
|
+
|
|
133
150
|
/** Prepend a one-time spawn note (e.g. a model fallback) to an outcome's output. */
|
|
134
151
|
function withNote(outcome: ChildOutcome, note: string | undefined): ChildOutcome {
|
|
135
152
|
return note ? { ...outcome, output: `${note}\n\n${outcome.output}` } : outcome;
|
|
@@ -158,10 +175,14 @@ export class SubagentRuntime {
|
|
|
158
175
|
/** A child extension handler threw (pi swallows it otherwise); surfaced to the user by index.ts. */
|
|
159
176
|
private readonly onExtensionError: (runName: string | undefined, error: ExtensionError) => void;
|
|
160
177
|
private readonly onModelUnusable: (model: string, reason: string) => void;
|
|
178
|
+
/** A child's edit or write changed a file (lib/child-writes.ts); index.ts tells the parent's file-tracker. */
|
|
179
|
+
private readonly onChildWrite: (path: string, runName: string | undefined) => void;
|
|
161
180
|
/** Child session id → run name, for the bridge wrapper above. */
|
|
162
181
|
private readonly runNames = new Map<string, string>();
|
|
163
182
|
/** Child session id → agent type (`explore`, …), for the hook payload's `agent_type`. */
|
|
164
183
|
private readonly agentTypes = new Map<string, string>();
|
|
184
|
+
/** Child session id → the main-session call that started its turn, for the bridge wrapper. */
|
|
185
|
+
private readonly mainToolCalls = new Map<string, () => string | undefined>();
|
|
165
186
|
/**
|
|
166
187
|
* Parallel children with the same request prefix (same agent prompt and
|
|
167
188
|
* model; forks share the parent's) let the first one start streaming before
|
|
@@ -178,8 +199,10 @@ export class SubagentRuntime {
|
|
|
178
199
|
getHookBridge: () => HookBridge | undefined,
|
|
179
200
|
onExtensionError: (runName: string | undefined, error: ExtensionError) => void,
|
|
180
201
|
onModelUnusable: (model: string, reason: string) => void,
|
|
202
|
+
onChildWrite: (path: string, runName: string | undefined) => void,
|
|
181
203
|
) {
|
|
182
204
|
this.modelRuntime = modelRuntime;
|
|
205
|
+
this.onChildWrite = onChildWrite;
|
|
183
206
|
this.baseCwd = baseCwd;
|
|
184
207
|
this.getMcpTools = getMcpTools;
|
|
185
208
|
this.getHookBridge = getHookBridge;
|
|
@@ -191,7 +214,11 @@ export class SubagentRuntime {
|
|
|
191
214
|
const namedBridge: PermissionBridge = (call) => {
|
|
192
215
|
const bridge = getPermissionBridge();
|
|
193
216
|
if (!bridge) throw new Error("the parent's permission bridge is no longer available");
|
|
194
|
-
return bridge({
|
|
217
|
+
return bridge({
|
|
218
|
+
...call,
|
|
219
|
+
agent: call.agent ?? (call.sessionId ? this.runNames.get(call.sessionId) : undefined),
|
|
220
|
+
parentToolCallId: call.parentToolCallId ?? (call.sessionId ? this.mainToolCalls.get(call.sessionId)?.() : undefined),
|
|
221
|
+
});
|
|
195
222
|
};
|
|
196
223
|
this.getPermissionBridge = () => (getPermissionBridge() ? namedBridge : undefined);
|
|
197
224
|
}
|
|
@@ -203,11 +230,12 @@ export class SubagentRuntime {
|
|
|
203
230
|
getHookBridge: () => HookBridge | undefined = () => undefined,
|
|
204
231
|
onExtensionError: (runName: string | undefined, error: ExtensionError) => void = () => {},
|
|
205
232
|
onModelUnusable: (model: string, reason: string) => void = () => {},
|
|
233
|
+
onChildWrite: (path: string, runName: string | undefined) => void = () => {},
|
|
206
234
|
): Promise<SubagentRuntime> {
|
|
207
235
|
const modelRuntime = await createSharedModelRuntime(getAgentDir());
|
|
208
236
|
// Prime the catalog once; spawns read the live snapshot (see resolveModel).
|
|
209
237
|
await modelRuntime.getAvailable();
|
|
210
|
-
return new SubagentRuntime(modelRuntime, cwd, getMcpTools, getPermissionBridge, getHookBridge, onExtensionError, onModelUnusable);
|
|
238
|
+
return new SubagentRuntime(modelRuntime, cwd, getMcpTools, getPermissionBridge, getHookBridge, onExtensionError, onModelUnusable, onChildWrite);
|
|
211
239
|
}
|
|
212
240
|
|
|
213
241
|
/**
|
|
@@ -234,15 +262,23 @@ export class SubagentRuntime {
|
|
|
234
262
|
* prompt and default tools, review S6); a named agent carries its own; a
|
|
235
263
|
* plain run gets pi's base prompt. Built fresh per session (agent-loader.ts).
|
|
236
264
|
*/
|
|
237
|
-
private childLoaderOptions(spec: ChildSessionSpec): AgentLoaderOptions {
|
|
238
|
-
const
|
|
265
|
+
private childLoaderOptions(spec: ChildSessionSpec, handback: HandbackSlot): AgentLoaderOptions {
|
|
266
|
+
const fork = SubagentRuntime.isFork(spec);
|
|
267
|
+
const systemPrompt = fork ? spec.parentSystemPrompt : spec.agent?.systemPrompt;
|
|
239
268
|
return {
|
|
240
269
|
cwd: this.baseCwd,
|
|
241
270
|
agentDir: getAgentDir(),
|
|
242
271
|
systemPrompt,
|
|
243
272
|
neverGate: NEVER_GATE,
|
|
244
273
|
extraExtensionPaths: CHILD_EXTENSION_PATHS,
|
|
245
|
-
//
|
|
274
|
+
// Every child hands back through SubagentHandback (deferred in a fork,
|
|
275
|
+
// lib/subagent-handback.ts). Inline extensions load after the paths
|
|
276
|
+
// above, so the fork-cache one, last, sees the final body.
|
|
277
|
+
extraFactories: [
|
|
278
|
+
subagentHandbackExtension(handback, { deferred: fork }),
|
|
279
|
+
...(fork && spec.parentRequest ? [forkCacheExtension(spec.parentRequest)] : []),
|
|
280
|
+
],
|
|
281
|
+
// claude-context (above) injects the CLAUDE.md instructions on the child's session_start;
|
|
246
282
|
// pi must not append the same files to the system prompt as well.
|
|
247
283
|
noContextFiles: true,
|
|
248
284
|
getPermissionBridge: this.getPermissionBridge,
|
|
@@ -271,7 +307,7 @@ export class SubagentRuntime {
|
|
|
271
307
|
/**
|
|
272
308
|
* Hold a run until a sibling with the same request prefix has started
|
|
273
309
|
* streaming (see `warmGate`): same prompt identity, same cwd (the child's
|
|
274
|
-
*
|
|
310
|
+
* instructions block names its paths) and same model. Resolves to the release
|
|
275
311
|
* for the caller's `finally`.
|
|
276
312
|
*/
|
|
277
313
|
private admitPrefix(spec: Pick<ChildSessionSpec, "cwd" | "forkFrom" | "parentSystemPrompt" | "agent">, session: Session): Promise<Release> {
|
|
@@ -288,9 +324,12 @@ export class SubagentRuntime {
|
|
|
288
324
|
onSettled?: () => void,
|
|
289
325
|
sink?: LiveSink,
|
|
290
326
|
): () => void {
|
|
327
|
+
const childWrote = childWriteWatcher(session.sessionManager?.getCwd?.() ?? this.baseCwd);
|
|
291
328
|
return session.subscribe((event) => {
|
|
292
329
|
try {
|
|
293
330
|
const settled = tracker.process(event as never);
|
|
331
|
+
const wrote = childWrote(event as never);
|
|
332
|
+
if (wrote) this.onChildWrite(wrote, this.runNames.get(session.sessionId));
|
|
294
333
|
const e = event as { type?: string; toolName?: string; args?: unknown; isError?: boolean; result?: unknown; message?: { role?: string; content?: unknown } };
|
|
295
334
|
if (e.type === "tool_execution_start") {
|
|
296
335
|
sink?.onBlock?.({ kind: "call", tool: e.toolName ?? "tool", text: summarizeArgs(e.args) });
|
|
@@ -350,20 +389,11 @@ export class SubagentRuntime {
|
|
|
350
389
|
* inherits the parent transcript + system prompt; a fresh named run gets the
|
|
351
390
|
* agent's own prompt and toolset.
|
|
352
391
|
*/
|
|
353
|
-
private async buildChildSession(spec: ChildSessionSpec): Promise<{ session: Session; note?: string }> {
|
|
354
|
-
const [loader, mcpTools] = await Promise.all([buildAgentLoader(this.childLoaderOptions(spec)), this.getMcpTools()]);
|
|
355
|
-
const newSessionManager = () =>
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
spec.sessionFile
|
|
359
|
-
? SessionManager.open(spec.sessionFile, undefined, spec.cwd)
|
|
360
|
-
: spec.forkFrom
|
|
361
|
-
? SessionManager.forkFrom(spec.forkFrom, spec.cwd, spec.sessionDir)
|
|
362
|
-
: spec.sessionDir
|
|
363
|
-
? SessionManager.create(spec.cwd, spec.sessionDir)
|
|
364
|
-
: SessionManager.inMemory(spec.cwd);
|
|
365
|
-
// A fork keeps the parent's toolset; only a named agent carries an allowlist.
|
|
366
|
-
const allowlist = spec.forkFrom ? undefined : spec.agent?.tools;
|
|
392
|
+
private async buildChildSession(spec: ChildSessionSpec, handback: HandbackSlot): Promise<{ session: Session; note?: string }> {
|
|
393
|
+
const [loader, mcpTools] = await Promise.all([buildAgentLoader(this.childLoaderOptions(spec, handback)), this.getMcpTools()]);
|
|
394
|
+
const newSessionManager = () => newChildSessionManager(spec);
|
|
395
|
+
// A fork keeps the parent's toolset; only a named agent carries tool lists.
|
|
396
|
+
const agent = spec.forkFrom ? undefined : spec.agent;
|
|
367
397
|
const make = async (model: string | undefined): Promise<Session> => {
|
|
368
398
|
const resolvedModel = model ? await this.resolveModel(model) : undefined;
|
|
369
399
|
const session = await openChildSession({
|
|
@@ -374,10 +404,9 @@ export class SubagentRuntime {
|
|
|
374
404
|
modelRuntime: this.modelRuntime,
|
|
375
405
|
model: resolvedModel as never,
|
|
376
406
|
thinkingLevel: spec.thinking as never,
|
|
377
|
-
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
excludeTools: spec.forkFrom ? undefined : spec.agent?.excludeTools,
|
|
407
|
+
// The allowlist with the plumbing re-added, and the denylist (CC's "All
|
|
408
|
+
// tools except …" shape), which filters injected customTools too.
|
|
409
|
+
...agentToolOptions(agent),
|
|
381
410
|
customTools: [sendToMainTool((m, s) => spec.onMessageToMain?.(m, s)), ...(spec.extraTools ?? []), ...mcpTools],
|
|
382
411
|
sessionManager: newSessionManager(),
|
|
383
412
|
},
|
|
@@ -386,21 +415,17 @@ export class SubagentRuntime {
|
|
|
386
415
|
});
|
|
387
416
|
// An allowlist that matched nothing real would run a tool-less agent
|
|
388
417
|
// (pi drops unknown names silently). Fail loud with the fix named.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
).length === 0
|
|
395
|
-
) {
|
|
418
|
+
const unusable = unusableAllowlistError(
|
|
419
|
+
agent,
|
|
420
|
+
session.getAllTools().map((t) => t.name),
|
|
421
|
+
);
|
|
422
|
+
if (unusable) {
|
|
396
423
|
this.discard(session);
|
|
397
|
-
throw new Error(
|
|
398
|
-
`Agent "${spec.agent?.name}" lists tools (${allowlist.join(", ")}) that match no available tool. ` +
|
|
399
|
-
"Use Claude Code names (Read, Edit, Write, Bash, Grep, Glob, WebFetch, WebSearch, NotebookEdit, Skill, Agent, SendMessage) or pi names (read, edit, write, bash, grep, find, ls, …) in the agent file's `tools` list.",
|
|
400
|
-
);
|
|
424
|
+
throw new Error(unusable);
|
|
401
425
|
}
|
|
402
426
|
if (spec.name) this.runNames.set(session.sessionManager.getSessionId(), spec.name);
|
|
403
427
|
if (spec.agent) this.agentTypes.set(session.sessionManager.getSessionId(), spec.agent.name);
|
|
428
|
+
if (spec.mainToolCallId) this.mainToolCalls.set(session.sessionManager.getSessionId(), spec.mainToolCallId);
|
|
404
429
|
return session;
|
|
405
430
|
};
|
|
406
431
|
try {
|
|
@@ -421,7 +446,8 @@ export class SubagentRuntime {
|
|
|
421
446
|
private discard(session: Session): void {
|
|
422
447
|
this.runNames.delete(session.sessionManager.getSessionId());
|
|
423
448
|
this.agentTypes.delete(session.sessionManager.getSessionId());
|
|
424
|
-
session.
|
|
449
|
+
this.mainToolCalls.delete(session.sessionManager.getSessionId());
|
|
450
|
+
disposeChildSession(session);
|
|
425
451
|
}
|
|
426
452
|
|
|
427
453
|
/**
|
|
@@ -452,17 +478,17 @@ export class SubagentRuntime {
|
|
|
452
478
|
// corrupt session file, loader error) must resolve to a failed
|
|
453
479
|
// outcome, never reject handle.result — call sites like the
|
|
454
480
|
// SendMessage resume path consume it with a bare .then().
|
|
455
|
-
const built = await this.buildChildSession(options);
|
|
481
|
+
const built = await this.buildChildSession(options, tracker);
|
|
456
482
|
session = built.session;
|
|
457
483
|
spawnNote = built.note;
|
|
458
484
|
unsubscribe = this.wireTracker(session, tracker, options.onProgress, undefined, options.sink);
|
|
459
485
|
// A resume continues a unique history; only a fresh run shares a prefix.
|
|
460
486
|
const releasePrefix = options.sessionFile ? undefined : await this.admitPrefix(options, session);
|
|
461
487
|
try {
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
if (
|
|
488
|
+
// abort() is a no-op before the prompt starts. Every cancellation
|
|
489
|
+
// source (signal, kill, wall-clock cap) marks the tracker first,
|
|
490
|
+
// including one that landed during construction or the prefix wait.
|
|
491
|
+
if (tracker.aborted) throw new Error("cancelled before the first request was sent");
|
|
466
492
|
// No template/command expansion: a task starting with "/" is text, not
|
|
467
493
|
// a command lookup (pi's own sendUserMessage does the same — L2).
|
|
468
494
|
await session.prompt(options.task, { expandPromptTemplates: false });
|
|
@@ -504,11 +530,12 @@ export class SubagentRuntime {
|
|
|
504
530
|
* while an idle resident lives until task_stop / session shutdown.
|
|
505
531
|
*/
|
|
506
532
|
async runResident(options: ResidentRunOptions): Promise<RpcChildHandle> {
|
|
507
|
-
const
|
|
533
|
+
const tracker = new SessionTurnTracker();
|
|
534
|
+
const built = await this.buildChildSession(options, tracker);
|
|
508
535
|
const session = built.session;
|
|
509
536
|
let spawnNote = built.note;
|
|
510
|
-
const tracker = new SessionTurnTracker();
|
|
511
537
|
let exited = false;
|
|
538
|
+
let exitPromise: Promise<void> | undefined;
|
|
512
539
|
let turnActive = false;
|
|
513
540
|
let firstTurn = true;
|
|
514
541
|
let turnTimer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -635,7 +662,7 @@ export class SubagentRuntime {
|
|
|
635
662
|
busy: () => turnActive || !session.isIdle,
|
|
636
663
|
exited: () => exited,
|
|
637
664
|
kill: () => {
|
|
638
|
-
if (exited) return Promise.resolve();
|
|
665
|
+
if (exited) return exitPromise ?? Promise.resolve();
|
|
639
666
|
exited = true;
|
|
640
667
|
pendingSend = undefined;
|
|
641
668
|
// Report an in-flight turn as terminated, not as a normal completion:
|
|
@@ -645,14 +672,15 @@ export class SubagentRuntime {
|
|
|
645
672
|
finishTurn(tracker.turnOutcome());
|
|
646
673
|
// onExit only after the abort settles: it triggers worktree cleanup,
|
|
647
674
|
// which must not race an aborting session still writing files. The
|
|
648
|
-
// returned promise
|
|
649
|
-
|
|
675
|
+
// returned promise settles after that cleanup too, so session_shutdown
|
|
676
|
+
// waits for exactly that.
|
|
677
|
+
return (exitPromise = session
|
|
650
678
|
.abort()
|
|
651
679
|
.catch(() => undefined)
|
|
652
|
-
.
|
|
680
|
+
.then(async () => {
|
|
653
681
|
discard();
|
|
654
|
-
options.onExit?.();
|
|
655
|
-
});
|
|
682
|
+
await Promise.resolve(options.onExit?.()).catch(() => undefined);
|
|
683
|
+
}));
|
|
656
684
|
},
|
|
657
685
|
release: () => {
|
|
658
686
|
if (exited || turnActive || !session.isIdle) return;
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
import { readdirSync, statSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
|
|
13
|
+
/** The synthetic agent type of a run that clones the calling conversation. */
|
|
14
|
+
export const FORK_AGENT = "fork";
|
|
15
|
+
/** The agent a run that names none starts, as in Claude Code. */
|
|
16
|
+
export const DEFAULT_AGENT = "general-purpose";
|
|
17
|
+
|
|
13
18
|
export interface AgentRunRecord {
|
|
14
19
|
name: string;
|
|
15
20
|
agent: string;
|
|
@@ -23,6 +28,12 @@ export interface AgentRunRecord {
|
|
|
23
28
|
worktree?: boolean;
|
|
24
29
|
model?: string;
|
|
25
30
|
thinking?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The run's short title, Claude Code's `description`: the Agent call's
|
|
33
|
+
* `description`, or a `/btw` fork's question. The strip, `/tasks` and the
|
|
34
|
+
* notification summary show it (findings §40); absent, they show the name.
|
|
35
|
+
*/
|
|
36
|
+
description?: string;
|
|
26
37
|
/**
|
|
27
38
|
* Nesting level: 0 = spawned by main, 1 = spawned by a child's own Agent
|
|
28
39
|
* tool. Set once at creation and read by every spawn-tool handout (the
|
|
@@ -38,13 +49,23 @@ export interface AgentRunRecord {
|
|
|
38
49
|
inline?: boolean;
|
|
39
50
|
}
|
|
40
51
|
|
|
52
|
+
/** `<base>-<n>` with the lowest n ≥ `from` not in `taken`. */
|
|
53
|
+
function firstFreeSuffixed(taken: ReadonlySet<string>, base: string, from: number): string {
|
|
54
|
+
for (let n = from; ; n++) {
|
|
55
|
+
const candidate = `${base}-${n}`;
|
|
56
|
+
if (!taken.has(candidate)) return candidate;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
/** `<agent>-<n>` with the lowest n not already taken. */
|
|
42
61
|
export function nextRunName(existing: Iterable<string>, agent: string): string {
|
|
62
|
+
return firstFreeSuffixed(new Set(existing), agent, 1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `base` when free, else `<base>-<n>` with the lowest n ≥ 2 not taken (Claude Code's fork naming). */
|
|
66
|
+
export function freeRunName(existing: Iterable<string>, base: string): string {
|
|
43
67
|
const taken = new Set(existing);
|
|
44
|
-
|
|
45
|
-
const candidate = `${agent}-${n}`;
|
|
46
|
-
if (!taken.has(candidate)) return candidate;
|
|
47
|
-
}
|
|
68
|
+
return taken.has(base) ? firstFreeSuffixed(taken, base, 2) : base;
|
|
48
69
|
}
|
|
49
70
|
|
|
50
71
|
/**
|
|
@@ -119,11 +140,16 @@ export class RunRegistry {
|
|
|
119
140
|
this.reserved.clear();
|
|
120
141
|
}
|
|
121
142
|
|
|
122
|
-
/** Hold a name until its record is added
|
|
143
|
+
/** Hold a name until its record is added. */
|
|
123
144
|
reserve(name: string): void {
|
|
124
145
|
this.reserved.add(name);
|
|
125
146
|
}
|
|
126
147
|
|
|
148
|
+
/** Free a held name whose spawn failed validation, so a corrected retry gets it. */
|
|
149
|
+
release(name: string): void {
|
|
150
|
+
this.reserved.delete(name);
|
|
151
|
+
}
|
|
152
|
+
|
|
127
153
|
/** Latest wins per name, matching Claude Code's semantics. */
|
|
128
154
|
add(record: AgentRunRecord): void {
|
|
129
155
|
this.reserved.delete(record.name);
|
|
@@ -131,6 +157,14 @@ export class RunRegistry {
|
|
|
131
157
|
this.byId.set(record.taskId, record);
|
|
132
158
|
}
|
|
133
159
|
|
|
160
|
+
/** Forget a run that never started, so neither list_agents nor a resume offers it. */
|
|
161
|
+
remove(taskId: string): void {
|
|
162
|
+
const record = this.byId.get(taskId);
|
|
163
|
+
if (!record) return;
|
|
164
|
+
this.byId.delete(taskId);
|
|
165
|
+
if (this.byName.get(record.name) === record) this.byName.delete(record.name);
|
|
166
|
+
}
|
|
167
|
+
|
|
134
168
|
/** Every name in use: recorded runs plus names reserved for runs being spawned. */
|
|
135
169
|
names(): string[] {
|
|
136
170
|
return [...new Set([...this.byName.keys(), ...this.reserved])];
|
|
@@ -27,7 +27,7 @@ export function sendToMainTool(onMessage: (message: string, summary?: string) =>
|
|
|
27
27
|
}) as never,
|
|
28
28
|
async execute(_toolCallId: string, params: unknown) {
|
|
29
29
|
const p = (params ?? {}) as { to?: unknown; message?: unknown; summary?: unknown };
|
|
30
|
-
// Fail loud (docs/decisions/tools.md): a wrong recipient or a missing
|
|
30
|
+
// Fail loud (working-docs/decisions/tools.md): a wrong recipient or a missing
|
|
31
31
|
// message returns isError with the fix named, never a silent success.
|
|
32
32
|
if (p.to !== undefined && p.to !== "main") {
|
|
33
33
|
return {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { recordAction, type ChildAction } from "../auto-mode/actions.ts";
|
|
15
|
-
import
|
|
15
|
+
import type { HandbackSlot } from "../lib/subagent-handback.ts";
|
|
16
|
+
import type { ChildOutcome } from "./outcome.ts";
|
|
16
17
|
import { addUsage, emptyUsage, type UsageTotals } from "./usage.ts";
|
|
17
18
|
|
|
18
19
|
/** The subset of AgentSession events this tracker reads. */
|
|
@@ -23,7 +24,12 @@ interface TrackedEvent {
|
|
|
23
24
|
message?: { role?: string; content?: unknown; usage?: unknown; stopReason?: string; errorMessage?: string };
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The tracker is also the run's `HandbackSlot`: a `SubagentHandback` call
|
|
29
|
+
* records the turn's report here, and the turn's outcome is that report, or
|
|
30
|
+
* the turn's final assistant text when the child never called the tool.
|
|
31
|
+
*/
|
|
32
|
+
export class SessionTurnTracker implements HandbackSlot {
|
|
27
33
|
toolCalls = 0;
|
|
28
34
|
/** What the child did this turn, for auto mode's return review. */
|
|
29
35
|
actions: ChildAction[] = [];
|
|
@@ -43,12 +49,15 @@ export class SessionTurnTracker {
|
|
|
43
49
|
aborted: string | undefined;
|
|
44
50
|
/** Every turn's final text joined, for task_output on a resident agent. */
|
|
45
51
|
transcript = "";
|
|
52
|
+
/** The report this turn handed back through SubagentHandback, if it did. */
|
|
53
|
+
handback: string | undefined;
|
|
46
54
|
|
|
47
55
|
/** Call when a new turn is started (a prompt sent while idle). */
|
|
48
56
|
beginTurn(): void {
|
|
49
57
|
this.turnText = "";
|
|
50
58
|
this.providerError = undefined;
|
|
51
59
|
this.aborted = undefined;
|
|
60
|
+
this.handback = undefined;
|
|
52
61
|
// Per turn, not per session: the review that reads this judges the turn just finished.
|
|
53
62
|
this.actions = [];
|
|
54
63
|
}
|
|
@@ -60,6 +69,17 @@ export class SessionTurnTracker {
|
|
|
60
69
|
this.aborted ??= reason;
|
|
61
70
|
}
|
|
62
71
|
|
|
72
|
+
recordHandback(message: string): boolean {
|
|
73
|
+
if (this.handback !== undefined) return false;
|
|
74
|
+
this.handback = message;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** What the turn returns: the handed-back report, else the final assistant text. */
|
|
79
|
+
private get result(): string {
|
|
80
|
+
return this.handback ?? this.turnText;
|
|
81
|
+
}
|
|
82
|
+
|
|
63
83
|
/** Feed one subscribed event. Returns true when a turn just settled. */
|
|
64
84
|
process(event: TrackedEvent): boolean {
|
|
65
85
|
switch (event.type) {
|
|
@@ -83,8 +103,8 @@ export class SessionTurnTracker {
|
|
|
83
103
|
return false;
|
|
84
104
|
}
|
|
85
105
|
case "agent_settled": {
|
|
86
|
-
if (this.
|
|
87
|
-
this.transcript = this.transcript ? `${this.transcript}\n\n---\n\n${this.
|
|
106
|
+
if (this.result.trim()) {
|
|
107
|
+
this.transcript = this.transcript ? `${this.transcript}\n\n---\n\n${this.result}` : this.result;
|
|
88
108
|
}
|
|
89
109
|
return true;
|
|
90
110
|
}
|
|
@@ -95,7 +115,7 @@ export class SessionTurnTracker {
|
|
|
95
115
|
|
|
96
116
|
/** The outcome of the turn that just settled. */
|
|
97
117
|
turnOutcome(): ChildOutcome {
|
|
98
|
-
return finishOutcome(this.
|
|
118
|
+
return finishOutcome(this.result, this.providerError, this.toolCalls, this.usage, this.actions, this.aborted);
|
|
99
119
|
}
|
|
100
120
|
}
|
|
101
121
|
|
|
@@ -103,16 +123,20 @@ export class SessionTurnTracker {
|
|
|
103
123
|
* Shape a run/turn's collected state into a ChildOutcome, matching the spawned
|
|
104
124
|
* child's messages. `aborted` (a reason) wins over everything: an aborted turn's
|
|
105
125
|
* partial text is reported as terminated and failed, never as the answer.
|
|
126
|
+
*
|
|
127
|
+
* The text is kept whole. Every consumer bounds it with `persistIfLarge`, which
|
|
128
|
+
* saves an oversized report to a file and points at it; a cut here left the
|
|
129
|
+
* persist step nothing over its limit, so a long report lost its end with no
|
|
130
|
+
* marker (SUBAGENTS-WORKFLOWS-REVIEW-2026-09-26 M1).
|
|
106
131
|
*/
|
|
107
132
|
export function finishOutcome(
|
|
108
|
-
|
|
133
|
+
output: string,
|
|
109
134
|
providerError: string | undefined,
|
|
110
135
|
toolCalls: number,
|
|
111
136
|
usage: UsageTotals,
|
|
112
137
|
actions: ChildAction[],
|
|
113
138
|
aborted?: string,
|
|
114
139
|
): ChildOutcome {
|
|
115
|
-
const output = rawOutput.slice(0, OUTPUT_CAP);
|
|
116
140
|
if (aborted) {
|
|
117
141
|
return {
|
|
118
142
|
output: output.trim() ? `${output}\n\n[${aborted}]` : `Subagent ${aborted}.`,
|
|
@@ -134,5 +158,5 @@ export function finishOutcome(
|
|
|
134
158
|
};
|
|
135
159
|
}
|
|
136
160
|
if (output.trim()) return { output, toolCalls, usage, actions };
|
|
137
|
-
return { output: "Subagent
|
|
161
|
+
return { output: "(Subagent completed but returned no output.)", toolCalls, usage, actions };
|
|
138
162
|
}
|