@esso0428/pi-subagents 0.17.16 → 0.17.18
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/CHANGELOG.md +11 -0
- package/ROADMAP.md +87 -0
- package/docs/post-0.17.6-agents-roster-viewer-spec.md +42 -0
- package/docs/post-0.17.6-durable-history-spec.md +41 -0
- package/docs/post-0.17.6-feature-specs.md +50 -0
- package/docs/post-0.17.6-nested-agents-spec.md +38 -0
- package/docs/post-0.17.6-recovery-shutdown-spec.md +46 -0
- package/docs/post-0.17.6-ui-latency-baseline-spec.md +46 -0
- package/docs/post-0.17.6-workflow-rpc-lifecycle-spec.md +54 -0
- package/package.json +1 -1
- package/src/agent-history.ts +2 -54
- package/src/agent-manager.ts +406 -1288
- package/src/agent-runner.ts +27 -251
- package/src/agent-types.ts +32 -188
- package/src/cross-extension-rpc.ts +20 -96
- package/src/custom-agents.ts +13 -170
- package/src/index.ts +480 -1920
- package/src/invocation-config.ts +3 -118
- package/src/model-resolver.ts +0 -18
- package/src/output-file.ts +6 -61
- package/src/prompts.ts +2 -45
- package/src/schedule.ts +14 -35
- package/src/settings.ts +2 -301
- package/src/status-note.ts +1 -66
- package/src/types.ts +10 -177
- package/src/ui/agent-widget.ts +57 -278
- package/src/ui/conversation-blocks.ts +0 -6
- package/src/ui/conversation-timeline.ts +25 -139
- package/src/ui/conversation-viewer.ts +48 -212
- package/src/ui/schedule-menu.ts +8 -9
- package/src/usage.ts +2 -109
- package/src/worktree.ts +55 -69
- package/dist/abortable.d.ts +0 -13
- package/dist/abortable.d.ts.map +0 -1
- package/dist/abortable.js +0 -43
- package/dist/abortable.js.map +0 -1
- package/dist/agent-color.d.ts +0 -36
- package/dist/agent-color.d.ts.map +0 -1
- package/dist/agent-color.js +0 -124
- package/dist/agent-color.js.map +0 -1
- package/dist/agent-file-toggle.d.ts +0 -126
- package/dist/agent-file-toggle.d.ts.map +0 -1
- package/dist/agent-file-toggle.js +0 -259
- package/dist/agent-file-toggle.js.map +0 -1
- package/dist/agent-history-list.d.ts +0 -19
- package/dist/agent-history-list.d.ts.map +0 -1
- package/dist/agent-history-list.js +0 -69
- package/dist/agent-history-list.js.map +0 -1
- package/dist/agent-history.d.ts +0 -35
- package/dist/agent-history.d.ts.map +0 -1
- package/dist/agent-history.js +0 -188
- package/dist/agent-history.js.map +0 -1
- package/dist/agent-manager.d.ts +0 -503
- package/dist/agent-manager.d.ts.map +0 -1
- package/dist/agent-manager.js +0 -1572
- package/dist/agent-manager.js.map +0 -1
- package/dist/agent-recovery.d.ts +0 -37
- package/dist/agent-recovery.d.ts.map +0 -1
- package/dist/agent-recovery.js +0 -171
- package/dist/agent-recovery.js.map +0 -1
- package/dist/agent-runner.d.ts +0 -303
- package/dist/agent-runner.d.ts.map +0 -1
- package/dist/agent-runner.js +0 -1003
- package/dist/agent-runner.js.map +0 -1
- package/dist/agent-types.d.ts +0 -120
- package/dist/agent-types.d.ts.map +0 -1
- package/dist/agent-types.js +0 -301
- package/dist/agent-types.js.map +0 -1
- package/dist/child-context.d.ts +0 -3
- package/dist/child-context.d.ts.map +0 -1
- package/dist/child-context.js +0 -13
- package/dist/child-context.js.map +0 -1
- package/dist/context.d.ts +0 -13
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -57
- package/dist/context.js.map +0 -1
- package/dist/cross-extension-rpc.d.ts +0 -67
- package/dist/cross-extension-rpc.d.ts.map +0 -1
- package/dist/cross-extension-rpc.js +0 -139
- package/dist/cross-extension-rpc.js.map +0 -1
- package/dist/custom-agents.d.ts +0 -55
- package/dist/custom-agents.d.ts.map +0 -1
- package/dist/custom-agents.js +0 -309
- package/dist/custom-agents.js.map +0 -1
- package/dist/default-agents.d.ts +0 -8
- package/dist/default-agents.d.ts.map +0 -1
- package/dist/default-agents.js +0 -123
- package/dist/default-agents.js.map +0 -1
- package/dist/enabled-models.d.ts +0 -50
- package/dist/enabled-models.d.ts.map +0 -1
- package/dist/enabled-models.js +0 -146
- package/dist/enabled-models.js.map +0 -1
- package/dist/env.d.ts +0 -7
- package/dist/env.d.ts.map +0 -1
- package/dist/env.js +0 -29
- package/dist/env.js.map +0 -1
- package/dist/group-join.d.ts +0 -33
- package/dist/group-join.d.ts.map +0 -1
- package/dist/group-join.js +0 -117
- package/dist/group-join.js.map +0 -1
- package/dist/index.d.ts +0 -51
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3687
- package/dist/index.js.map +0 -1
- package/dist/invocation-config.d.ts +0 -108
- package/dist/invocation-config.d.ts.map +0 -1
- package/dist/invocation-config.js +0 -84
- package/dist/invocation-config.js.map +0 -1
- package/dist/memory.d.ts +0 -54
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js +0 -166
- package/dist/memory.js.map +0 -1
- package/dist/mention-clone.d.ts +0 -88
- package/dist/mention-clone.d.ts.map +0 -1
- package/dist/mention-clone.js +0 -154
- package/dist/mention-clone.js.map +0 -1
- package/dist/mention.d.ts +0 -82
- package/dist/mention.d.ts.map +0 -1
- package/dist/mention.js +0 -132
- package/dist/mention.js.map +0 -1
- package/dist/model-resolver.d.ts +0 -37
- package/dist/model-resolver.d.ts.map +0 -1
- package/dist/model-resolver.js +0 -96
- package/dist/model-resolver.js.map +0 -1
- package/dist/model-scope.d.ts +0 -50
- package/dist/model-scope.d.ts.map +0 -1
- package/dist/model-scope.js +0 -49
- package/dist/model-scope.js.map +0 -1
- package/dist/nested-tools.d.ts +0 -57
- package/dist/nested-tools.d.ts.map +0 -1
- package/dist/nested-tools.js +0 -301
- package/dist/nested-tools.js.map +0 -1
- package/dist/nico-overrides.d.ts +0 -54
- package/dist/nico-overrides.d.ts.map +0 -1
- package/dist/nico-overrides.js +0 -170
- package/dist/nico-overrides.js.map +0 -1
- package/dist/output-file.d.ts +0 -44
- package/dist/output-file.d.ts.map +0 -1
- package/dist/output-file.js +0 -156
- package/dist/output-file.js.map +0 -1
- package/dist/prompts.d.ts +0 -56
- package/dist/prompts.d.ts.map +0 -1
- package/dist/prompts.js +0 -92
- package/dist/prompts.js.map +0 -1
- package/dist/schedule-store.d.ts +0 -39
- package/dist/schedule-store.d.ts.map +0 -1
- package/dist/schedule-store.js +0 -156
- package/dist/schedule-store.js.map +0 -1
- package/dist/schedule.d.ts +0 -110
- package/dist/schedule.d.ts.map +0 -1
- package/dist/schedule.js +0 -360
- package/dist/schedule.js.map +0 -1
- package/dist/settings.d.ts +0 -354
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js +0 -247
- package/dist/settings.js.map +0 -1
- package/dist/skill-loader.d.ts +0 -25
- package/dist/skill-loader.d.ts.map +0 -1
- package/dist/skill-loader.js +0 -94
- package/dist/skill-loader.js.map +0 -1
- package/dist/status-note.d.ts +0 -62
- package/dist/status-note.d.ts.map +0 -1
- package/dist/status-note.js +0 -86
- package/dist/status-note.js.map +0 -1
- package/dist/structured-output.d.ts +0 -62
- package/dist/structured-output.d.ts.map +0 -1
- package/dist/structured-output.js +0 -113
- package/dist/structured-output.js.map +0 -1
- package/dist/types.d.ts +0 -372
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -6
- package/dist/types.js.map +0 -1
- package/dist/ui/agent-mention.d.ts +0 -83
- package/dist/ui/agent-mention.d.ts.map +0 -1
- package/dist/ui/agent-mention.js +0 -188
- package/dist/ui/agent-mention.js.map +0 -1
- package/dist/ui/agent-widget.d.ts +0 -241
- package/dist/ui/agent-widget.d.ts.map +0 -1
- package/dist/ui/agent-widget.js +0 -992
- package/dist/ui/agent-widget.js.map +0 -1
- package/dist/ui/ccstyle/diff/ansi-utils.d.ts +0 -11
- package/dist/ui/ccstyle/diff/ansi-utils.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/ansi-utils.js +0 -145
- package/dist/ui/ccstyle/diff/ansi-utils.js.map +0 -1
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts +0 -12
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/diff-presentation.js +0 -48
- package/dist/ui/ccstyle/diff/diff-presentation.js.map +0 -1
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts +0 -46
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/diff-renderer.js +0 -2049
- package/dist/ui/ccstyle/diff/diff-renderer.js.map +0 -1
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts +0 -12
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/line-width-safety.js +0 -58
- package/dist/ui/ccstyle/diff/line-width-safety.js.map +0 -1
- package/dist/ui/ccstyle/diff/render-utils.d.ts +0 -6
- package/dist/ui/ccstyle/diff/render-utils.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/render-utils.js +0 -25
- package/dist/ui/ccstyle/diff/render-utils.js.map +0 -1
- package/dist/ui/ccstyle/diff/shiki-highlight.d.ts +0 -19
- package/dist/ui/ccstyle/diff/shiki-highlight.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/shiki-highlight.js +0 -85
- package/dist/ui/ccstyle/diff/shiki-highlight.js.map +0 -1
- package/dist/ui/ccstyle/diff/types.d.ts +0 -22
- package/dist/ui/ccstyle/diff/types.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/types.js +0 -10
- package/dist/ui/ccstyle/diff/types.js.map +0 -1
- package/dist/ui/ccstyle/diff/write-display-utils.d.ts +0 -2
- package/dist/ui/ccstyle/diff/write-display-utils.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/write-display-utils.js +0 -12
- package/dist/ui/ccstyle/diff/write-display-utils.js.map +0 -1
- package/dist/ui/ccstyle/tool-renderer.d.ts +0 -17
- package/dist/ui/ccstyle/tool-renderer.d.ts.map +0 -1
- package/dist/ui/ccstyle/tool-renderer.js +0 -74
- package/dist/ui/ccstyle/tool-renderer.js.map +0 -1
- package/dist/ui/ccstyle/tool-result.d.ts +0 -44
- package/dist/ui/ccstyle/tool-result.d.ts.map +0 -1
- package/dist/ui/ccstyle/tool-result.js +0 -423
- package/dist/ui/ccstyle/tool-result.js.map +0 -1
- package/dist/ui/conversation-blocks.d.ts +0 -46
- package/dist/ui/conversation-blocks.d.ts.map +0 -1
- package/dist/ui/conversation-blocks.js +0 -313
- package/dist/ui/conversation-blocks.js.map +0 -1
- package/dist/ui/conversation-nvim.d.ts +0 -8
- package/dist/ui/conversation-nvim.d.ts.map +0 -1
- package/dist/ui/conversation-nvim.js +0 -117
- package/dist/ui/conversation-nvim.js.map +0 -1
- package/dist/ui/conversation-role.d.ts +0 -13
- package/dist/ui/conversation-role.d.ts.map +0 -1
- package/dist/ui/conversation-role.js +0 -39
- package/dist/ui/conversation-role.js.map +0 -1
- package/dist/ui/conversation-search.d.ts +0 -39
- package/dist/ui/conversation-search.d.ts.map +0 -1
- package/dist/ui/conversation-search.js +0 -124
- package/dist/ui/conversation-search.js.map +0 -1
- package/dist/ui/conversation-timeline.d.ts +0 -102
- package/dist/ui/conversation-timeline.d.ts.map +0 -1
- package/dist/ui/conversation-timeline.js +0 -555
- package/dist/ui/conversation-timeline.js.map +0 -1
- package/dist/ui/conversation-viewer.d.ts +0 -138
- package/dist/ui/conversation-viewer.d.ts.map +0 -1
- package/dist/ui/conversation-viewer.js +0 -1175
- package/dist/ui/conversation-viewer.js.map +0 -1
- package/dist/ui/schedule-menu.d.ts +0 -17
- package/dist/ui/schedule-menu.d.ts.map +0 -1
- package/dist/ui/schedule-menu.js +0 -95
- package/dist/ui/schedule-menu.js.map +0 -1
- package/dist/ui/select-item.d.ts +0 -28
- package/dist/ui/select-item.d.ts.map +0 -1
- package/dist/ui/select-item.js +0 -35
- package/dist/ui/select-item.js.map +0 -1
- package/dist/ui/viewer-keys.d.ts +0 -21
- package/dist/ui/viewer-keys.d.ts.map +0 -1
- package/dist/ui/viewer-keys.js +0 -18
- package/dist/ui/viewer-keys.js.map +0 -1
- package/dist/ui/workflow-card.d.ts +0 -176
- package/dist/ui/workflow-card.d.ts.map +0 -1
- package/dist/ui/workflow-card.js +0 -333
- package/dist/ui/workflow-card.js.map +0 -1
- package/dist/ui/workflow-dialog.d.ts +0 -306
- package/dist/ui/workflow-dialog.d.ts.map +0 -1
- package/dist/ui/workflow-dialog.js +0 -844
- package/dist/ui/workflow-dialog.js.map +0 -1
- package/dist/ui/workflow-menu.d.ts +0 -42
- package/dist/ui/workflow-menu.d.ts.map +0 -1
- package/dist/ui/workflow-menu.js +0 -127
- package/dist/ui/workflow-menu.js.map +0 -1
- package/dist/usage.d.ts +0 -136
- package/dist/usage.d.ts.map +0 -1
- package/dist/usage.js +0 -121
- package/dist/usage.js.map +0 -1
- package/dist/workflow/collisions.d.ts +0 -96
- package/dist/workflow/collisions.d.ts.map +0 -1
- package/dist/workflow/collisions.js +0 -89
- package/dist/workflow/collisions.js.map +0 -1
- package/dist/workflow/entry.d.ts +0 -33
- package/dist/workflow/entry.d.ts.map +0 -1
- package/dist/workflow/entry.js +0 -30
- package/dist/workflow/entry.js.map +0 -1
- package/dist/workflow/host.d.ts +0 -63
- package/dist/workflow/host.d.ts.map +0 -1
- package/dist/workflow/host.js +0 -363
- package/dist/workflow/host.js.map +0 -1
- package/dist/workflow/journal.d.ts +0 -98
- package/dist/workflow/journal.d.ts.map +0 -1
- package/dist/workflow/journal.js +0 -121
- package/dist/workflow/journal.js.map +0 -1
- package/dist/workflow/json-schema.d.ts +0 -52
- package/dist/workflow/json-schema.d.ts.map +0 -1
- package/dist/workflow/json-schema.js +0 -112
- package/dist/workflow/json-schema.js.map +0 -1
- package/dist/workflow/meta.d.ts +0 -68
- package/dist/workflow/meta.d.ts.map +0 -1
- package/dist/workflow/meta.js +0 -318
- package/dist/workflow/meta.js.map +0 -1
- package/dist/workflow/progress.d.ts +0 -225
- package/dist/workflow/progress.d.ts.map +0 -1
- package/dist/workflow/progress.js +0 -362
- package/dist/workflow/progress.js.map +0 -1
- package/dist/workflow/runtime.d.ts +0 -335
- package/dist/workflow/runtime.d.ts.map +0 -1
- package/dist/workflow/runtime.js +0 -831
- package/dist/workflow/runtime.js.map +0 -1
- package/dist/workflow/saved.d.ts +0 -91
- package/dist/workflow/saved.d.ts.map +0 -1
- package/dist/workflow/saved.js +0 -204
- package/dist/workflow/saved.js.map +0 -1
- package/dist/workflow/task.d.ts +0 -137
- package/dist/workflow/task.d.ts.map +0 -1
- package/dist/workflow/task.js +0 -208
- package/dist/workflow/task.js.map +0 -1
- package/dist/workflow/tool-description.d.ts +0 -39
- package/dist/workflow/tool-description.d.ts.map +0 -1
- package/dist/workflow/tool-description.js +0 -200
- package/dist/workflow/tool-description.js.map +0 -1
- package/dist/workflow/worker-source.d.ts +0 -48
- package/dist/workflow/worker-source.d.ts.map +0 -1
- package/dist/workflow/worker-source.js +0 -779
- package/dist/workflow/worker-source.js.map +0 -1
- package/dist/worktree.d.ts +0 -53
- package/dist/worktree.d.ts.map +0 -1
- package/dist/worktree.js +0 -165
- package/dist/worktree.js.map +0 -1
- package/dist/write-execution.d.ts +0 -42
- package/dist/write-execution.d.ts.map +0 -1
- package/dist/write-execution.js +0 -138
- package/dist/write-execution.js.map +0 -1
- package/dist/xml.d.ts +0 -11
- package/dist/xml.d.ts.map +0 -1
- package/dist/xml.js +0 -13
- package/dist/xml.js.map +0 -1
- package/src/abortable.ts +0 -43
- package/src/agent-color.ts +0 -161
- package/src/agent-file-toggle.ts +0 -269
- package/src/child-context.ts +0 -15
- package/src/mention-clone.ts +0 -196
- package/src/mention.ts +0 -141
- package/src/model-scope.ts +0 -70
- package/src/nested-tools.ts +0 -424
- package/src/structured-output.ts +0 -130
- package/src/ui/agent-mention.ts +0 -216
- package/src/ui/select-item.ts +0 -45
- package/src/ui/workflow-card.ts +0 -470
- package/src/ui/workflow-dialog.ts +0 -1115
- package/src/ui/workflow-menu.ts +0 -166
- package/src/workflow/collisions.ts +0 -123
- package/src/workflow/entry.ts +0 -47
- package/src/workflow/host.ts +0 -403
- package/src/workflow/journal.ts +0 -164
- package/src/workflow/json-schema.ts +0 -128
- package/src/workflow/meta.ts +0 -325
- package/src/workflow/progress.ts +0 -550
- package/src/workflow/runtime.ts +0 -1219
- package/src/workflow/saved.ts +0 -217
- package/src/workflow/task.ts +0 -302
- package/src/workflow/tool-description.ts +0 -200
- package/src/workflow/worker-source.ts +0 -781
- package/src/xml.ts +0 -13
package/src/index.ts
CHANGED
|
@@ -10,36 +10,28 @@
|
|
|
10
10
|
* /agents — Interactive agent management menu
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync
|
|
14
|
-
import {
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
15
|
import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, getAgentDir, getSelectListTheme, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import { Container,
|
|
16
|
+
import { Container, Key, matchesKey, SelectList, type SettingItem, SettingsList, Spacer, Text } from "@earendil-works/pi-tui";
|
|
17
17
|
import { Type } from "@sinclair/typebox";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { AgentManager, isTopLevelAgent } from "./agent-manager.js";
|
|
24
|
-
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, getRememberAgents, normalizeMaxTurns, resolveEffectiveMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, setRememberAgents, steerAgent } from "./agent-runner.js";
|
|
25
|
-
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, getConfig, getFallbackSubagent, isDefaultsDisabled, NO_FALLBACK, registerAgents, resolveSpawnType, resolveType, setDefaultsDisabled, setFallbackSubagent } from "./agent-types.js";
|
|
26
|
-
import { inChildSessionContext } from "./child-context.js";
|
|
18
|
+
import { agentHistoryLocator, createAgentHistoryPath, readAgentHistory, readAgentHistoryResult } from "./agent-history.js";
|
|
19
|
+
import { buildAgentStatusMenuEntries, canOpenActiveAgent, canOpenAgentHistory, formatAgentHistoryOption, splitAgentRecords } from "./agent-history-list.js";
|
|
20
|
+
import { AgentManager } from "./agent-manager.js";
|
|
21
|
+
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
22
|
+
import { applyNicoOverrides, BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
|
|
27
23
|
import { type RpcHandle, registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
28
24
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
25
|
+
import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
|
|
29
26
|
import { GroupJoinManager } from "./group-join.js";
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import { describeModel, type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
34
|
-
import { checkModelScope, isScopeModelsEnabled, setScopeModelsEnabled } from "./model-scope.js";
|
|
35
|
-
import { getMaxSubagentDepth, setMaxSubagentDepth } from "./nested-tools.js";
|
|
36
|
-
import { createOutputFilePath, ensureOutputFile, getOutputTranscriptDefault, sessionTaskDir, setOutputTranscriptDefault, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
27
|
+
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
28
|
+
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
29
|
+
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
37
30
|
import { SubagentScheduler } from "./schedule.js";
|
|
38
31
|
import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
|
|
39
|
-
import { applyAndEmitLoaded,
|
|
40
|
-
import {
|
|
41
|
-
import { type AgentConfig, type AgentInvocation, type
|
|
42
|
-
import { createMentionProvider, mentionRoster, type TypeInfo } from "./ui/agent-mention.js";
|
|
32
|
+
import { applyAndEmitLoaded, type SubagentsSettings, saveAndEmitChanged, type ToolDescriptionMode } from "./settings.js";
|
|
33
|
+
import { getStatusNote } from "./status-note.js";
|
|
34
|
+
import { type AgentConfig, type AgentInvocation, type AgentRecord, type JoinMode, type NotificationDetails, type SubagentType, type WidgetMode } from "./types.js";
|
|
43
35
|
import {
|
|
44
36
|
type AgentActivity,
|
|
45
37
|
type AgentDetails,
|
|
@@ -47,7 +39,6 @@ import {
|
|
|
47
39
|
buildInvocationTags,
|
|
48
40
|
describeActivity,
|
|
49
41
|
fgPreservingNestedStyles,
|
|
50
|
-
formatCost,
|
|
51
42
|
formatDuration,
|
|
52
43
|
formatMs,
|
|
53
44
|
formatTokens,
|
|
@@ -59,21 +50,7 @@ import {
|
|
|
59
50
|
type UICtx,
|
|
60
51
|
} from "./ui/agent-widget.js";
|
|
61
52
|
import { showSchedulesMenu } from "./ui/schedule-menu.js";
|
|
62
|
-
import {
|
|
63
|
-
import { showWorkflowsMenu, type WorkflowMenuDeps } from "./ui/workflow-menu.js";
|
|
64
|
-
import { getLifetimeCost, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage, PendingUsagePool, toReportedUsage } from "./usage.js";
|
|
65
|
-
import { decideWorkflowCollision, FOREIGN_WORKFLOW_TOOL_NAMES } from "./workflow/collisions.js";
|
|
66
|
-
import { WORKFLOW_ENTRY_TYPE, type WorkflowEntryData, workflowEntryData } from "./workflow/entry.js";
|
|
67
|
-
import { createWorkflowHost } from "./workflow/host.js";
|
|
68
|
-
import { appendJournal, readJournal, type WorkflowJournalEntry } from "./workflow/journal.js";
|
|
69
|
-
import { extractMeta, type WorkflowMeta, workflowCallName } from "./workflow/meta.js";
|
|
70
|
-
import { elapsedMs } from "./workflow/progress.js";
|
|
71
|
-
import { runWorkflow } from "./workflow/runtime.js";
|
|
72
|
-
import { resolveWorkflowScript } from "./workflow/saved.js";
|
|
73
|
-
import { completeWorkflowTask, createWorkflowTask, failWorkflowTask, formatWorkflowNotification, resolveResumeTarget, updateWorkflowProgressBatch, type WorkflowTask, workflowResultText, workflowRunId } from "./workflow/task.js";
|
|
74
|
-
import { fullWorkflowToolDescription } from "./workflow/tool-description.js";
|
|
75
|
-
import { isWorktreeIsolationEnabled, setWorktreeIsolationEnabled } from "./worktree.js";
|
|
76
|
-
import { escapeXml } from "./xml.js";
|
|
53
|
+
import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";
|
|
77
54
|
|
|
78
55
|
// ---- Shared helpers ----
|
|
79
56
|
|
|
@@ -82,6 +59,39 @@ function textResult(msg: string, details?: AgentDetails) {
|
|
|
82
59
|
return { content: [{ type: "text" as const, text: msg }], details: details as any };
|
|
83
60
|
}
|
|
84
61
|
|
|
62
|
+
/** Await a promise until it settles or the caller cancels, without aborting the underlying work. */
|
|
63
|
+
function abortable<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
64
|
+
if (!signal) return promise;
|
|
65
|
+
if (signal.aborted) return Promise.reject(signal.reason);
|
|
66
|
+
|
|
67
|
+
return new Promise<T>((resolve, reject) => {
|
|
68
|
+
let settled = false;
|
|
69
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
70
|
+
const onAbort = () => {
|
|
71
|
+
if (settled) return;
|
|
72
|
+
settled = true;
|
|
73
|
+
cleanup();
|
|
74
|
+
reject(signal.reason);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
78
|
+
promise.then(
|
|
79
|
+
(value) => {
|
|
80
|
+
if (settled) return;
|
|
81
|
+
settled = true;
|
|
82
|
+
cleanup();
|
|
83
|
+
resolve(value);
|
|
84
|
+
},
|
|
85
|
+
(error: unknown) => {
|
|
86
|
+
if (settled) return;
|
|
87
|
+
settled = true;
|
|
88
|
+
cleanup();
|
|
89
|
+
reject(error);
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
export function renderRunningAgentStatus(
|
|
86
96
|
frame: string,
|
|
87
97
|
statsText: string,
|
|
@@ -138,9 +148,8 @@ function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
|
|
|
138
148
|
onSessionCreated: (session: any) => {
|
|
139
149
|
state.session = session;
|
|
140
150
|
},
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
onAssistantUsage: (_usage: LifetimeUsage) => {
|
|
151
|
+
onAssistantUsage: (usage: { input: number; output: number; cacheWrite: number }) => {
|
|
152
|
+
addUsage(state.lifetimeUsage, usage);
|
|
144
153
|
onStreamUpdate?.();
|
|
145
154
|
},
|
|
146
155
|
};
|
|
@@ -157,6 +166,16 @@ function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
|
|
|
157
166
|
*/
|
|
158
167
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
159
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Salvaged partial output of a failed run, as a labeled suffix for the error
|
|
171
|
+
* surfaces (or "" if the run produced nothing). `record.result` is bounded to
|
|
172
|
+
* the run's own turns, so this is never a stale earlier answer (#144).
|
|
173
|
+
*/
|
|
174
|
+
function partialOutputSuffix(record: AgentRecord, fallback?: string): string {
|
|
175
|
+
const partial = record.result?.trim() || fallback?.trim();
|
|
176
|
+
return partial ? `\n\nPartial output before the failure:\n${partial}` : "";
|
|
177
|
+
}
|
|
178
|
+
|
|
160
179
|
/** Human-readable status label for agent completion. */
|
|
161
180
|
function getStatusLabel(status: string, error?: string): string {
|
|
162
181
|
switch (status) {
|
|
@@ -168,18 +187,19 @@ function getStatusLabel(status: string, error?: string): string {
|
|
|
168
187
|
}
|
|
169
188
|
}
|
|
170
189
|
|
|
190
|
+
/** Escape XML special characters to prevent injection in structured notifications. */
|
|
191
|
+
function escapeXml(s: string): string {
|
|
192
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
193
|
+
}
|
|
194
|
+
|
|
171
195
|
/** Format a structured task notification matching Claude Code's <task-notification> XML. */
|
|
172
|
-
function formatTaskNotification(record: AgentRecord, resultMaxLen: number
|
|
196
|
+
function formatTaskNotification(record: AgentRecord, resultMaxLen: number): string {
|
|
173
197
|
const status = getStatusLabel(record.status, record.error);
|
|
174
198
|
const durationMs = record.completedAt ? record.completedAt - record.startedAt : 0;
|
|
175
199
|
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
176
200
|
const contextPercent = getSessionContextPercent(record.session);
|
|
177
201
|
const ctxXml = contextPercent !== null ? `<context_percent>${Math.round(contextPercent)}</context_percent>` : "";
|
|
178
202
|
const compactXml = record.compactionCount ? `<compactions>${record.compactionCount}</compactions>` : "";
|
|
179
|
-
// Only under `showCost`: this is LLM context, and a figure the orchestrator
|
|
180
|
-
// did not ask for is a figure it may start reporting unprompted.
|
|
181
|
-
const cost = showCost ? getLifetimeCost(record.lifetimeUsage) : 0;
|
|
182
|
-
const costXml = cost > 0 ? `<estimated_cost_usd>${cost.toFixed(4)}</estimated_cost_usd>` : "";
|
|
183
203
|
|
|
184
204
|
const resultPreview = record.result
|
|
185
205
|
? record.result.length > resultMaxLen
|
|
@@ -195,7 +215,7 @@ function formatTaskNotification(record: AgentRecord, resultMaxLen: number, showC
|
|
|
195
215
|
`<status>${escapeXml(status)}</status>`,
|
|
196
216
|
`<summary>Agent "${escapeXml(record.description)}" ${record.status}${getStatusNote(record.status)}</summary>`,
|
|
197
217
|
`<result>${escapeXml(resultPreview)}</result>`,
|
|
198
|
-
`<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses>${ctxXml}${compactXml}
|
|
218
|
+
`<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses>${ctxXml}${compactXml}<duration_ms>${durationMs}</duration_ms></usage>`,
|
|
199
219
|
`</task-notification>`,
|
|
200
220
|
].filter(Boolean).join('\n');
|
|
201
221
|
}
|
|
@@ -211,10 +231,6 @@ function buildDetails(
|
|
|
211
231
|
...base,
|
|
212
232
|
toolUses: record.toolUses,
|
|
213
233
|
tokens: formatLifetimeTokens(record),
|
|
214
|
-
// Raw, and unconditional: `tokens` is preformatted because it is one stat,
|
|
215
|
-
// but a cost is joined by "·" in one surface, "," in another and "|" in a
|
|
216
|
-
// third — so it travels as a number and each renderer punctuates its own.
|
|
217
|
-
cost: getLifetimeCost(record.lifetimeUsage),
|
|
218
234
|
turnCount: activity?.turnCount,
|
|
219
235
|
maxTurns: activity?.maxTurns,
|
|
220
236
|
durationMs: (record.completedAt ?? Date.now()) - record.startedAt,
|
|
@@ -237,10 +253,6 @@ function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, act
|
|
|
237
253
|
turnCount: activity?.turnCount ?? 0,
|
|
238
254
|
maxTurns: activity?.maxTurns,
|
|
239
255
|
totalTokens,
|
|
240
|
-
// Carried unconditionally; the renderer gates on the setting. Details are
|
|
241
|
-
// data, and a notification rendered before a mid-session toggle should not
|
|
242
|
-
// be stuck with the old answer.
|
|
243
|
-
totalCost: getLifetimeCost(record.lifetimeUsage),
|
|
244
256
|
durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
|
|
245
257
|
outputFile: record.outputFile,
|
|
246
258
|
error: record.error,
|
|
@@ -252,59 +264,7 @@ function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, act
|
|
|
252
264
|
};
|
|
253
265
|
}
|
|
254
266
|
|
|
255
|
-
/**
|
|
256
|
-
* Format an agent's tool scope for the Agent tool description.
|
|
257
|
-
*
|
|
258
|
-
* This suffix describes BUILT-IN scope only — extension tools are resolved when
|
|
259
|
-
* the agent runs (extensions can register asynchronously), so they cannot be
|
|
260
|
-
* enumerated while the description is being built. That is why an agent with
|
|
261
|
-
* `tools: "*, ext:mcp/search"` renders "*" and always has.
|
|
262
|
-
*
|
|
263
|
-
* Two distinctions matter, both of them capability claims the orchestrator acts on:
|
|
264
|
-
*
|
|
265
|
-
* - absent vs empty. `builtinToolNames: undefined` means the agent never narrowed
|
|
266
|
-
* its tools (the shipped defaults); `[]` is what `tools: none` and an `ext:`-only
|
|
267
|
-
* `tools:` parse to, and the runtime really does hand those agents no built-ins.
|
|
268
|
-
* Rendering both "*" tells the orchestrator a tool-less agent can run `bash`.
|
|
269
|
-
* - empty-with-extensions vs empty-without. Zero built-ins does NOT imply zero
|
|
270
|
-
* tools: `tools: none` alongside `extensions:` still surfaces every extension
|
|
271
|
-
* tool (see test/fixtures/.pi/agents/tools-none.md, which expects three). Calling
|
|
272
|
-
* that "none" understates the agent instead of overstating it — better, but still
|
|
273
|
-
* wrong, and it would route work away from the only agent able to do it. "none"
|
|
274
|
-
* is therefore reserved for agents that genuinely can call nothing: `isolated`
|
|
275
|
-
* agents and those with `extensions: false`.
|
|
276
|
-
*/
|
|
277
|
-
export function formatToolsSuffix(cfg: AgentConfig | undefined): string {
|
|
278
|
-
const tools = cfg?.builtinToolNames;
|
|
279
|
-
if (!tools) return "*";
|
|
280
|
-
if (tools.length === 0) {
|
|
281
|
-
// `isolated` overrides extensions to false in the runner, so both mean the
|
|
282
|
-
// agent has no extension tools either — and then it truly has nothing.
|
|
283
|
-
const noExtensionTools = cfg?.isolated === true || cfg?.extensions === false;
|
|
284
|
-
return noExtensionTools ? "none" : "no built-ins, extension tools only";
|
|
285
|
-
}
|
|
286
|
-
const isFullSet =
|
|
287
|
-
tools.length === BUILTIN_TOOL_NAMES.length
|
|
288
|
-
&& BUILTIN_TOOL_NAMES.every((t) => tools.includes(t));
|
|
289
|
-
return isFullSet ? "*" : tools.join(", ");
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/** CLI flag that runs a workflow script at session start. */
|
|
293
|
-
export const WORKFLOW_FILE_FLAG = "subagents-workflow-file";
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Re-exported from where they now live, because this is where they were
|
|
297
|
-
* defined and a consumer (or a test) that matched a session entry on
|
|
298
|
-
* {@link WORKFLOW_ENTRY_TYPE} imports it from here.
|
|
299
|
-
*/
|
|
300
|
-
export { FOREIGN_WORKFLOW_TOOL_NAMES, WORKFLOW_ENTRY_TYPE, type WorkflowEntryData, workflowEntryData };
|
|
301
|
-
|
|
302
267
|
export default function (pi: ExtensionAPI) {
|
|
303
|
-
// Child AgentSessions load normal extensions. Re-entering this extension there
|
|
304
|
-
// would create another manager and leak handlers. Nested orchestration is
|
|
305
|
-
// injected as scoped custom tools by the existing manager instead.
|
|
306
|
-
if (inChildSessionContext()) return;
|
|
307
|
-
|
|
308
268
|
// ---- Register custom notification renderer ----
|
|
309
269
|
pi.registerMessageRenderer<NotificationDetails>(
|
|
310
270
|
"subagent-notification",
|
|
@@ -327,10 +287,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
327
287
|
if (d.turnCount > 0) parts.push(formatTurns(d.turnCount, d.maxTurns));
|
|
328
288
|
if (d.toolUses > 0) parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
|
|
329
289
|
if (d.totalTokens > 0) parts.push(formatTokens(d.totalTokens));
|
|
330
|
-
if (showCost) {
|
|
331
|
-
const costText = formatCost(d.totalCost ?? 0);
|
|
332
|
-
if (costText) parts.push(costText);
|
|
333
|
-
}
|
|
334
290
|
if (d.durationMs > 0) parts.push(formatMs(d.durationMs));
|
|
335
291
|
if (parts.length) {
|
|
336
292
|
line += "\n " + parts.map(p => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
@@ -354,91 +310,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
354
310
|
}
|
|
355
311
|
|
|
356
312
|
const all = [d, ...(d.others ?? [])];
|
|
357
|
-
|
|
358
|
-
// A group of agents lands as one notification, and the number a user wants
|
|
359
|
-
// from it is what the batch cost — not four figures to add up by hand.
|
|
360
|
-
// Derived from the per-agent details rather than carried alongside them:
|
|
361
|
-
// one source, so the total can never disagree with the rows above it.
|
|
362
|
-
if (showCost && all.length > 1) {
|
|
363
|
-
const total = formatCost(all.reduce((sum, a) => sum + (a.totalCost ?? 0), 0));
|
|
364
|
-
if (total) {
|
|
365
|
-
const tokens = all.reduce((sum, a) => sum + a.totalTokens, 0);
|
|
366
|
-
rendered.unshift(theme.fg("dim", `${all.length} agents · ${formatTokens(tokens)} · ${total}`));
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return new Text(rendered.join("\n"), 0, 0);
|
|
313
|
+
return new Text(all.map(renderOne).join("\n"), 0, 0);
|
|
370
314
|
}
|
|
371
315
|
);
|
|
372
316
|
|
|
373
|
-
// ---- Workflow run rendered as a session entry ----
|
|
374
|
-
// A workflow launched from the CLI flag has no tool call to hang its result
|
|
375
|
-
// card on, so it renders here instead — through the SAME layout the tool
|
|
376
|
-
// result uses, not a second one. Custom entries with no registered renderer
|
|
377
|
-
// are silently dropped by the host, which is why this is registered at
|
|
378
|
-
// activation rather than lazily.
|
|
379
|
-
if (typeof pi.registerEntryRenderer === "function") {
|
|
380
|
-
pi.registerEntryRenderer<WorkflowEntryData>(WORKFLOW_ENTRY_TYPE, (entry, _options, theme) =>
|
|
381
|
-
renderWorkflowEntryCard(entry.data, theme));
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Registered at activation; READ from session_start. The host applies CLI
|
|
385
|
-
// values after every extension factory has run, so `getFlag` here would only
|
|
386
|
-
// ever hand back the registered default (see the read site below).
|
|
387
|
-
if (typeof pi.registerFlag === "function") {
|
|
388
|
-
pi.registerFlag(WORKFLOW_FILE_FLAG, {
|
|
389
|
-
type: "string",
|
|
390
|
-
description:
|
|
391
|
-
`Run a workflow script at startup: --${WORKFLOW_FILE_FLAG}=<path>. ` +
|
|
392
|
-
"Use the `=` form — the space form consumes the next argument, which would swallow a following prompt.",
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Read directly rather than waiting for applyAndEmitLoaded below: this decides
|
|
397
|
-
// the initial load, which happens hundreds of lines before settings are applied.
|
|
398
|
-
let strictAgentFiles = loadSettings(process.cwd()).strictAgentFiles === true;
|
|
399
|
-
|
|
400
317
|
/** Reload agents from project/global custom agent dirs and merge with defaults (called on init and each Agent invocation). */
|
|
401
|
-
const reloadCustomAgents = (
|
|
402
|
-
const userAgents = loadCustomAgents(process.cwd()
|
|
318
|
+
const reloadCustomAgents = () => {
|
|
319
|
+
const userAgents = loadCustomAgents(process.cwd());
|
|
403
320
|
registerAgents(userAgents);
|
|
321
|
+
applyNicoOverrides();
|
|
404
322
|
};
|
|
405
323
|
|
|
406
|
-
// Initial load
|
|
407
|
-
|
|
408
|
-
reloadCustomAgents(strictAgentFiles);
|
|
324
|
+
// Initial load
|
|
325
|
+
reloadCustomAgents();
|
|
409
326
|
|
|
410
327
|
// ---- Agent activity tracking + widget ----
|
|
411
328
|
const agentActivity = new Map<string, AgentActivity>();
|
|
412
329
|
|
|
413
|
-
// ---- Usage reporting (both off by default; see SubagentsSettings) ----
|
|
414
|
-
/** Attach subagent spend to tool results, so the parent session counts it. */
|
|
415
|
-
let reportUsage = false;
|
|
416
|
-
function isReportUsageEnabled(): boolean { return reportUsage; }
|
|
417
|
-
function setReportUsage(b: boolean): void {
|
|
418
|
-
reportUsage = b;
|
|
419
|
-
// Whatever accumulated while it was on is stale the moment it goes off:
|
|
420
|
-
// draining it later would bill the parent for a window the user opted out
|
|
421
|
-
// of, in one lump, on some unrelated later tool call.
|
|
422
|
-
if (!b) pendingUsage.drain();
|
|
423
|
-
}
|
|
424
|
-
/** Show `~$X` next to token counts in the subagent surfaces. */
|
|
425
|
-
let showCost = false;
|
|
426
|
-
function isShowCostEnabled(): boolean { return showCost; }
|
|
427
|
-
function setShowCost(b: boolean): void { showCost = b; widget.update(); }
|
|
428
|
-
/** Name the model and thinking level on the widget's running rows. */
|
|
429
|
-
let showModel = false;
|
|
430
|
-
function isShowModelEnabled(): boolean { return showModel; }
|
|
431
|
-
function setShowModel(b: boolean): void { showModel = b; widget.update(); }
|
|
432
|
-
/**
|
|
433
|
-
* How much of the conversation viewer renders as Markdown. Read through a
|
|
434
|
-
* getter by the viewer rather than captured like `showCost`, because the
|
|
435
|
-
* viewer's `m` key writes back here while the overlay is on screen.
|
|
436
|
-
*/
|
|
437
|
-
let viewerMarkdown: ViewerMarkdownMode = "assistant";
|
|
438
|
-
function getViewerMarkdown(): ViewerMarkdownMode { return viewerMarkdown; }
|
|
439
|
-
function setViewerMarkdown(mode: ViewerMarkdownMode): void { viewerMarkdown = mode; }
|
|
440
|
-
const pendingUsage = new PendingUsagePool();
|
|
441
|
-
|
|
442
330
|
// ---- Cancellable pending notifications ----
|
|
443
331
|
// Holds notifications briefly so get_subagent_result can cancel them
|
|
444
332
|
// before they reach pi.sendMessage (fire-and-forget).
|
|
@@ -468,7 +356,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
468
356
|
function emitIndividualNudge(record: AgentRecord) {
|
|
469
357
|
if (record.resultConsumed) return; // re-check at send time
|
|
470
358
|
|
|
471
|
-
const notification = formatTaskNotification(record, 500
|
|
359
|
+
const notification = formatTaskNotification(record, 500);
|
|
472
360
|
const footer = record.outputFile ? `\nFull transcript available at: ${record.outputFile}` : '';
|
|
473
361
|
|
|
474
362
|
pi.sendMessage<NotificationDetails>({
|
|
@@ -495,9 +383,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
495
383
|
scheduleNudge(groupKey, () => {
|
|
496
384
|
// Re-check at send time
|
|
497
385
|
const unconsumed = records.filter(r => !r.resultConsumed);
|
|
498
|
-
if (unconsumed.length === 0) {
|
|
386
|
+
if (unconsumed.length === 0) {
|
|
387
|
+
widget.update();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
499
390
|
|
|
500
|
-
const notifications = unconsumed.map(r => formatTaskNotification(r, 300
|
|
391
|
+
const notifications = unconsumed.map(r => formatTaskNotification(r, 300)).join('\n\n');
|
|
501
392
|
const label = partial
|
|
502
393
|
? `${unconsumed.length} agent(s) finished (partial — others still running)`
|
|
503
394
|
: `${unconsumed.length} agent(s) finished`;
|
|
@@ -532,44 +423,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
532
423
|
const tokens = total > 0
|
|
533
424
|
? { input: u.input, output: u.output, total }
|
|
534
425
|
: undefined;
|
|
535
|
-
// The whole run's spend as a pi `Usage` — pi's convention for handing spend
|
|
536
|
-
// to a consumer, so `usage.cost.total` and `usage.cacheRead` are where a
|
|
537
|
-
// listener already expects them and anything pi adds to `Usage` arrives
|
|
538
|
-
// without a change here. Omitted when nothing was spent, so "spent nothing"
|
|
539
|
-
// and "never ran" stay distinguishable. Ungated by `showCost`: that setting
|
|
540
|
-
// governs what a human is shown, not what the event carries.
|
|
541
|
-
//
|
|
542
|
-
// `tokens` above is the other convention, kept as it shipped: a flat view
|
|
543
|
-
// model like pi's own `SessionStats`, carrying the DISPLAY total, which
|
|
544
|
-
// excludes cacheRead (#38). The two answer different questions and neither
|
|
545
|
-
// derives from the other.
|
|
546
|
-
const usage = toReportedUsage(u);
|
|
547
426
|
return {
|
|
548
427
|
id: record.id,
|
|
549
428
|
type: record.type,
|
|
550
429
|
description: record.description,
|
|
551
|
-
result: record.
|
|
430
|
+
result: record.result,
|
|
552
431
|
error: record.error,
|
|
553
|
-
transcriptPath: record.transcriptPath,
|
|
554
432
|
status: record.status,
|
|
555
433
|
toolUses: record.toolUses,
|
|
556
434
|
durationMs,
|
|
557
435
|
tokens,
|
|
558
|
-
usage,
|
|
559
436
|
};
|
|
560
437
|
}
|
|
561
438
|
|
|
562
439
|
// Background completion: route through group join or send individual nudge
|
|
563
|
-
type AgentMenuSelection = { id?: string; index: number };
|
|
564
|
-
const historyAgentSelection: AgentMenuSelection = { index: 0 };
|
|
565
|
-
const runningAgentSelection: AgentMenuSelection = { index: 0 };
|
|
566
440
|
const manager = new AgentManager((record) => {
|
|
567
|
-
// Owned children — nested, or a workflow's — report only through their
|
|
568
|
-
// owner: the parent's scoped tools, or the workflow's card, notification
|
|
569
|
-
// and dialog. Keep them out of top-level lifecycle, transcript,
|
|
570
|
-
// notification, and UI channels.
|
|
571
|
-
if (!isTopLevelAgent(record)) return;
|
|
572
|
-
|
|
573
441
|
// Emit lifecycle event based on terminal status
|
|
574
442
|
const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
|
|
575
443
|
const eventData = buildEventData(record);
|
|
@@ -583,10 +451,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
583
451
|
pi.appendEntry("subagents:record", {
|
|
584
452
|
id: record.id, type: record.type, description: record.description,
|
|
585
453
|
status: record.status,
|
|
454
|
+
// Durable transcripts are the source of truth for full output. Avoid
|
|
455
|
+
// copying a potentially large result into the parent session branch;
|
|
456
|
+
// get_subagent_result reloads it on demand after cleanup/restart.
|
|
586
457
|
result: record.transcriptPath ? undefined : record.result,
|
|
587
458
|
error: record.error,
|
|
588
|
-
transcriptPath: record.transcriptPath,
|
|
589
459
|
startedAt: record.startedAt, completedAt: record.completedAt,
|
|
460
|
+
toolUses: record.toolUses,
|
|
461
|
+
lifetimeUsage: record.lifetimeUsage,
|
|
462
|
+
invocation: record.invocation,
|
|
463
|
+
transcriptPath: record.transcriptPath,
|
|
590
464
|
});
|
|
591
465
|
|
|
592
466
|
// Skip notification if result was already consumed via get_subagent_result
|
|
@@ -612,21 +486,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
612
486
|
// 'delivered' → group callback already fired
|
|
613
487
|
widget.update();
|
|
614
488
|
}, undefined, (record) => {
|
|
615
|
-
if (!isTopLevelAgent(record)) return;
|
|
616
|
-
// Agent-tool spawns refresh these surfaces in their tool handler, but RPC
|
|
617
|
-
// and scheduler spawns enter through the manager directly.
|
|
618
|
-
if (currentCtx?.hasUI && (currentCtx.mode === undefined || currentCtx.mode === "tui")) {
|
|
619
|
-
widget.ensureTimer();
|
|
620
|
-
widget.update();
|
|
621
|
-
}
|
|
622
489
|
// Emit started event when agent transitions to running (including from queue)
|
|
623
490
|
pi.events.emit("subagents:started", {
|
|
624
491
|
id: record.id,
|
|
625
492
|
type: record.type,
|
|
626
493
|
description: record.description,
|
|
627
494
|
});
|
|
495
|
+
widget.ensureTimer();
|
|
496
|
+
widget.update();
|
|
628
497
|
}, (record, info) => {
|
|
629
|
-
if (!isTopLevelAgent(record)) return;
|
|
630
498
|
// Emit compacted event when agent's session compacts (preserves count on record).
|
|
631
499
|
pi.events.emit("subagents:compacted", {
|
|
632
500
|
id: record.id,
|
|
@@ -636,17 +504,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
636
504
|
tokensBefore: info.tokensBefore,
|
|
637
505
|
compactionCount: record.compactionCount,
|
|
638
506
|
});
|
|
639
|
-
}, (_record, usage) => {
|
|
640
|
-
// Every assistant message from every agent — nested included, exactly once.
|
|
641
|
-
// Parked here until a tool result can carry it back to the parent session;
|
|
642
|
-
// see `PendingUsagePool`. Skipped entirely when the feature is off, so no
|
|
643
|
-
// pool grows in a session that will never drain it.
|
|
644
|
-
if (reportUsage) pendingUsage.add(usage);
|
|
645
507
|
});
|
|
646
508
|
|
|
647
509
|
// Expose manager via Symbol.for() global registry for cross-package access.
|
|
648
510
|
// Standard Node.js pattern for cross-package singletons (used by OpenTelemetry, etc.).
|
|
649
|
-
// Documented for callers in docs/rpc.md ("The manager registry").
|
|
650
511
|
//
|
|
651
512
|
// Claim the slot only if it's free: subagent sessions re-activate this
|
|
652
513
|
// extension in the same process (session.bindExtensions in agent-runner.ts),
|
|
@@ -655,92 +516,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
655
516
|
// session's entry. The first activation (the root session) wins; child
|
|
656
517
|
// activations leave it alone.
|
|
657
518
|
const MANAGER_KEY = Symbol.for("pi-subagents:manager");
|
|
658
|
-
// Process-external callers may supply arbitrary options. Nested ownership and
|
|
659
|
-
// config-root metadata are internal capabilities issued only by scoped tools.
|
|
660
|
-
/**
|
|
661
|
-
* Resolve the agent type and spawn. Trusts its options — every caller must
|
|
662
|
-
* either be in-process or have gone through `spawnTopLevel` first.
|
|
663
|
-
*/
|
|
664
|
-
const spawnResolved = (piRef: any, ctxRef: any, type: string, prompt: string, options: any) => {
|
|
665
|
-
// Cross-extension callers get the same dispatch contract as the LLM (#183).
|
|
666
|
-
// The RPC layer already throws for an unresolvable model rather than falling
|
|
667
|
-
// back silently; a bad agent type should not be quieter. Throws become error
|
|
668
|
-
// envelopes at the RPC boundary. Reload first so an agent file added mid
|
|
669
|
-
// session is spawnable here too, not only through the Agent tool.
|
|
670
|
-
reloadCustomAgents();
|
|
671
|
-
const dispatch = resolveSpawnType(type);
|
|
672
|
-
if (!dispatch.ok) throw new Error(dispatch.message);
|
|
673
|
-
// Every programmatic spawn lands here — cross-extension RPC, both `@handle`
|
|
674
|
-
// mention paths, and the `Symbol.for("pi-subagents:manager")` registry — and
|
|
675
|
-
// none came through the Agent tool, which is where the UI activity tracker is
|
|
676
|
-
// otherwise created. Without one the widget has no tool name
|
|
677
|
-
// and no turn count, so the row reads `thinking…` for the agent's whole life
|
|
678
|
-
// while the header's tool-use count climbs beside it (#181). Double-tracking
|
|
679
|
-
// is not possible: the Agent tool calls `manager.spawn` directly. The tracker
|
|
680
|
-
// callbacks are the funnel's own — a caller's are not honoured, since a
|
|
681
|
-
// half-wired tracker renders worse than none.
|
|
682
|
-
//
|
|
683
|
-
// The turn limit is resolved rather than read off `options`, which a mention
|
|
684
|
-
// spawn deliberately omits so the agent's own config can decide: a tracker
|
|
685
|
-
// built with `undefined` renders `↻3` where the Agent tool renders `↻3≤20`.
|
|
686
|
-
// Like the tool's own, it is a prediction — editing the agent file mid-run
|
|
687
|
-
// leaves the displayed ceiling stale.
|
|
688
|
-
const { state, callbacks } = createActivityTracker(resolveEffectiveMaxTurns(dispatch.type, options?.maxTurns));
|
|
689
|
-
// Repaints are left to the manager's `onStart` callback, which already starts
|
|
690
|
-
// the widget timer for agents that enter this way.
|
|
691
|
-
const id = manager.spawn(piRef, ctxRef, dispatch.type, prompt, { ...options, ...callbacks });
|
|
692
|
-
agentActivity.set(id, state);
|
|
693
|
-
return id;
|
|
694
|
-
};
|
|
695
|
-
|
|
696
|
-
const spawnTopLevel = (piRef: any, ctxRef: any, type: string, prompt: string, options: any) => {
|
|
697
|
-
const safeOptions = { ...(options ?? {}) };
|
|
698
|
-
delete safeOptions.parentAgentId;
|
|
699
|
-
// Internal too: a forged value would hide an RPC-spawned agent inside
|
|
700
|
-
// someone else's workflow, and take it out of the concurrency pool with it.
|
|
701
|
-
delete safeOptions.workflowId;
|
|
702
|
-
delete safeOptions.depth;
|
|
703
|
-
delete safeOptions.maxSubagentDepth;
|
|
704
|
-
delete safeOptions.configCwd;
|
|
705
|
-
// Also internal: it names a transcript directory, so a forged value would
|
|
706
|
-
// be a path-traversal primitive.
|
|
707
|
-
delete safeOptions.rootSessionId;
|
|
708
|
-
// Worse than rootSessionId: this one names a file to OPEN and replay as a
|
|
709
|
-
// conversation. Only the mention dispatcher may set it, and only from a
|
|
710
|
-
// path this extension itself recorded — never from anything a caller sent.
|
|
711
|
-
delete safeOptions.resumeSessionFile;
|
|
712
|
-
// Bypasses handle allocation, so a forged value would duplicate a live
|
|
713
|
-
// agent's name and make `@handle` ambiguous. Same rule: dispatcher only.
|
|
714
|
-
delete safeOptions.reclaim;
|
|
715
|
-
// Every spawn through here is DETACHED — the caller gets an id back and
|
|
716
|
-
// awaits nothing. A forged `blocking` would charge it to the foreground
|
|
717
|
-
// pool and could defer it behind a queue whose gate nobody is holding.
|
|
718
|
-
delete safeOptions.blocking;
|
|
719
|
-
return spawnResolved(piRef, ctxRef, type, prompt, safeOptions);
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
/**
|
|
723
|
-
* Resolve a tool's `agent_id` as an id OR a handle, so the model addresses
|
|
724
|
-
* agents by the same names the user types. Ids are tried first, keeping the
|
|
725
|
-
* existing behaviour exact — a handle is only consulted when the string is
|
|
726
|
-
* not an id at all. Only live records: a tombstone has nothing to steer and
|
|
727
|
-
* no result to read. Callers still enforce the nested-ownership rejection.
|
|
728
|
-
*/
|
|
729
|
-
const resolveAgentRef = (ref: string): AgentRecord | undefined => {
|
|
730
|
-
const byId = manager.getRecord(ref);
|
|
731
|
-
if (byId) return byId;
|
|
732
|
-
const resolved = manager.resolveMention(ref);
|
|
733
|
-
return resolved?.kind === "live" ? resolved.record : undefined;
|
|
734
|
-
};
|
|
735
|
-
|
|
736
519
|
const registryEntry = {
|
|
737
520
|
waitForAll: () => manager.waitForAll(),
|
|
738
521
|
hasRunning: () => manager.hasRunning(),
|
|
739
|
-
spawn:
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
return record !== undefined && isTopLevelAgent(record) ? record : undefined;
|
|
743
|
-
},
|
|
522
|
+
spawn: (piRef: any, ctx: any, type: string, prompt: string, options: any) =>
|
|
523
|
+
manager.spawn(piRef, ctx, type, prompt, options),
|
|
524
|
+
getRecord: (id: string) => manager.getRecord(id),
|
|
744
525
|
};
|
|
745
526
|
const ownsManagerRegistry = (globalThis as any)[MANAGER_KEY] === undefined;
|
|
746
527
|
if (ownsManagerRegistry) {
|
|
@@ -757,8 +538,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
757
538
|
// (currentCtx would stay undefined → spawn always "No active session"). Gating
|
|
758
539
|
// here makes a filtered session behave like an absent one (#142).
|
|
759
540
|
let rpcHandle: RpcHandle | undefined;
|
|
760
|
-
/** Whether the `@handle` autocomplete wrapper has been stacked on pi's provider. */
|
|
761
|
-
let mentionProviderRegistered = false;
|
|
762
541
|
|
|
763
542
|
// ---- Subagent scheduler ----
|
|
764
543
|
// Session-scoped: store is constructed inside session_start once sessionId
|
|
@@ -781,26 +560,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
781
560
|
}
|
|
782
561
|
}
|
|
783
562
|
|
|
563
|
+
type AgentMenuSelection = { id?: string; index: number };
|
|
564
|
+
let runningAgentSelection: AgentMenuSelection = { index: 0 };
|
|
565
|
+
let historyAgentSelection: AgentMenuSelection = { index: 0 };
|
|
566
|
+
|
|
567
|
+
function resetAgentMenuSelections() {
|
|
568
|
+
runningAgentSelection = { index: 0 };
|
|
569
|
+
historyAgentSelection = { index: 0 };
|
|
570
|
+
}
|
|
571
|
+
|
|
784
572
|
// Capture ctx from session_start for RPC spawn handler + start the scheduler.
|
|
785
573
|
// This also wires the RPC handlers and broadcasts readiness — on the first
|
|
786
574
|
// bound session_start, so a filtered-out activation never advertises (#142).
|
|
787
575
|
pi.on("session_start", async (_event, ctx) => {
|
|
576
|
+
resetAgentMenuSelections();
|
|
788
577
|
currentCtx = ctx;
|
|
578
|
+
manager.clearCompleted(true);
|
|
579
|
+
const branch = ctx.sessionManager?.getBranch?.() ?? [];
|
|
580
|
+
manager.restoreCompleted(branch
|
|
581
|
+
.filter((entry: any) => entry?.type === "custom" && entry?.customType === "subagents:record")
|
|
582
|
+
.map((entry: any) => entry.data));
|
|
583
|
+
// Checkpoint files cover agents whose parent session never got a terminal
|
|
584
|
+
// branch entry (shutdown, session switch, or a process restart).
|
|
789
585
|
manager.restoreRecovered(ctx.cwd);
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
.
|
|
794
|
-
manager.restoreCompleted(restoredRecords);
|
|
795
|
-
historyAgentSelection.id = undefined;
|
|
796
|
-
historyAgentSelection.index = 0;
|
|
797
|
-
runningAgentSelection.id = undefined;
|
|
798
|
-
runningAgentSelection.index = 0;
|
|
799
|
-
if (ctx.hasUI && (ctx.mode === undefined || ctx.mode === "tui")) {
|
|
800
|
-
widget.setUICtx(ctx.ui);
|
|
586
|
+
// Attach the panel during TUI startup, after restored records are present,
|
|
587
|
+
// so terminal agents from the session branch are immediately visible.
|
|
588
|
+
if (ctx.mode === "tui") {
|
|
589
|
+
widget.setUICtx(ctx.ui as UICtx);
|
|
801
590
|
widget.update();
|
|
802
591
|
}
|
|
803
|
-
manager.clearCompleted(true);
|
|
804
592
|
// Guard mirrors the `!scheduler.isActive()` pattern below: session_start
|
|
805
593
|
// fires once per activation, but a double-bind must not leak listeners.
|
|
806
594
|
if (!rpcHandle) {
|
|
@@ -808,26 +596,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
808
596
|
events: pi.events,
|
|
809
597
|
pi,
|
|
810
598
|
getCtx: () => currentCtx,
|
|
811
|
-
manager
|
|
812
|
-
spawn: spawnTopLevel,
|
|
813
|
-
awaitStartup: (id) => manager.awaitStartup(id),
|
|
814
|
-
getRecord: (id) => manager.getRecord(id),
|
|
815
|
-
// Unguarded on purpose: the stop handler now runs the top-level check
|
|
816
|
-
// itself off `getRecord`, and reports the refusal instead of the
|
|
817
|
-
// "Agent not found" a false from here used to be read as.
|
|
818
|
-
abort: (id) => manager.abort(id),
|
|
819
|
-
consumeResult: (id) => {
|
|
820
|
-
const record = resolveAgentRef(id);
|
|
821
|
-
// Same guard as get_subagent_result: a running agent has no result
|
|
822
|
-
// to consume, and its notification is still the caller's only
|
|
823
|
-
// signal that it finished.
|
|
824
|
-
if (!record || record.parentAgentId) return false;
|
|
825
|
-
if (record.status === "running" || record.status === "queued") return false;
|
|
826
|
-
record.resultConsumed = true;
|
|
827
|
-
cancelNudge(record.id);
|
|
828
|
-
return true;
|
|
829
|
-
},
|
|
830
|
-
},
|
|
599
|
+
manager,
|
|
831
600
|
});
|
|
832
601
|
// Broadcast readiness so extensions loaded alongside us can discover us.
|
|
833
602
|
// Emitting after all factories have run (rather than at factory time)
|
|
@@ -835,267 +604,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
835
604
|
pi.events.emit("subagents:ready", {});
|
|
836
605
|
}
|
|
837
606
|
if (isSchedulingEnabled() && !scheduler.isActive()) startScheduler(ctx);
|
|
838
|
-
// Stack `@handle` suggestions on pi's built-in autocomplete. Registered at
|
|
839
|
-
// most once per activation: pi appends wrappers to a list it never prunes,
|
|
840
|
-
// so a second call would layer a duplicate provider on the first. TUI only
|
|
841
|
-
// — print mode has no such method, and RPC mode's is a no-op.
|
|
842
|
-
if (ctx.mode === "tui" && !mentionProviderRegistered && typeof ctx.ui.addAutocompleteProvider === "function") {
|
|
843
|
-
mentionProviderRegistered = true;
|
|
844
|
-
ctx.ui.addAutocompleteProvider(current =>
|
|
845
|
-
createMentionProvider(
|
|
846
|
-
current,
|
|
847
|
-
// Plain text, not renderAgentName: the same label the widget shows,
|
|
848
|
-
// but the autocomplete description cannot carry ANSI.
|
|
849
|
-
() => mentionRoster(manager, mentionTypes(), type => getConfig(type).displayName),
|
|
850
|
-
isAgentMentionsEnabled,
|
|
851
|
-
),
|
|
852
|
-
);
|
|
853
|
-
}
|
|
854
|
-
// Last, and only here: CLI flag values are applied by the host AFTER every
|
|
855
|
-
// extension factory has run, so this is the earliest point the real value
|
|
856
|
-
// exists. Detached inside — a workflow must not hold up session startup.
|
|
857
|
-
resolveWorkflowCollisions(ctx);
|
|
858
|
-
runWorkflowFlag(ctx);
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
/** Agent types `@` can start, in the shape the roster wants. */
|
|
862
|
-
const mentionTypes = (): TypeInfo[] =>
|
|
863
|
-
getAvailableTypes().map(name => ({ name, description: getAgentConfig(name)?.description ?? name }));
|
|
864
|
-
|
|
865
|
-
/**
|
|
866
|
-
* `@handle message` typed at the prompt addresses that agent instead of the
|
|
867
|
-
* main model — Claude Code's prompt mention, same grammar (see mention.ts).
|
|
868
|
-
*
|
|
869
|
-
* The handle names the *agent*, not one process, so one syntax covers its
|
|
870
|
-
* whole lifecycle: message it while it runs, resume it once it has finished,
|
|
871
|
-
* start it if it never ran. Everything that isn't an agent mention falls
|
|
872
|
-
* through untouched, which is what keeps `@src/foo.ts summarize this`, a bare
|
|
873
|
-
* `@handle`, and ordinary prose working. A delivered mention costs no
|
|
874
|
-
* main-model turn; the answer arrives through the ordinary completion
|
|
875
|
-
* notification either way.
|
|
876
|
-
*/
|
|
877
|
-
pi.on("input", async (event, ctx) => {
|
|
878
|
-
// Never hijack text the extension layer itself submitted (pi.sendMessage,
|
|
879
|
-
// scheduled prompts) — only something a person typed can be a mention.
|
|
880
|
-
if (event.source === "extension" || !isAgentMentionsEnabled()) return { action: "continue" };
|
|
881
|
-
// Claiming the turn is TUI only, matching the `@` completion that teaches
|
|
882
|
-
// the syntax. Pi defaults `session.prompt()` to source "interactive", so a
|
|
883
|
-
// headless `pi -p "@explore …"` reaches here too — and claiming it would
|
|
884
|
-
// answer with silence, which the background hold cannot fix: `handled`
|
|
885
|
-
// returns from prompt() before any turn starts, so the loop that patch wraps
|
|
886
|
-
// never runs (it holds subagents spawned by the Agent tool MID-turn, a
|
|
887
|
-
// different path). The agent would detach, `ctx.ui.notify` is a no-op
|
|
888
|
-
// outside the TUI, and print mode would exit having printed nothing.
|
|
889
|
-
//
|
|
890
|
-
// `model` mode has none of that problem: it queues a reminder and lets the
|
|
891
|
-
// turn run, so the answer is the model's own, printed as usual. It is the
|
|
892
|
-
// only branch allowed to act headlessly; everything else falls through to
|
|
893
|
-
// the main model exactly as it did before mentions existed.
|
|
894
|
-
const canDispatchDirectly = ctx.mode === "tui";
|
|
895
|
-
if (!canDispatchDirectly && getAgentMentionMode() !== "model") return { action: "continue" };
|
|
896
|
-
|
|
897
|
-
const mention = parseMention(event.text);
|
|
898
|
-
if (!mention) return { action: "continue" };
|
|
899
|
-
|
|
900
|
-
// `@main` addresses the main conversation, never a subagent — the one name
|
|
901
|
-
// `assignHandle` refuses to allocate. An explicit escape hatch for text
|
|
902
|
-
// that would otherwise read as a mention, so the prefix is dropped and the
|
|
903
|
-
// rest goes to the model with its attachments intact.
|
|
904
|
-
if (isReservedHandle(mention.handle)) {
|
|
905
|
-
return { action: "transform", text: mention.message, ...(event.images && { images: event.images }) };
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// As typed first, so an agent actually called `agent-foo` wins over Claude
|
|
909
|
-
// Code's `@agent-` + `foo` spelling rather than being shadowed by it.
|
|
910
|
-
const alias = stripAgentPrefix(mention.handle);
|
|
911
|
-
const resolved = manager.resolveMention(mention.handle)
|
|
912
|
-
?? (alias ? manager.resolveMention(alias) : undefined);
|
|
913
|
-
|
|
914
|
-
// Steering and resuming are direct in every mode, so headless they are not
|
|
915
|
-
// available at all. Falling through here rather than dropping to the start
|
|
916
|
-
// path below matters: the handle names an agent that already exists, and
|
|
917
|
-
// asking the model to start another one is not what was typed.
|
|
918
|
-
if (resolved && !canDispatchDirectly) return { action: "continue" };
|
|
919
|
-
|
|
920
|
-
if (resolved?.kind === "live") {
|
|
921
|
-
const record = resolved.record;
|
|
922
|
-
const target = `@${record.alias ?? record.handle ?? mention.handle}`;
|
|
923
|
-
|
|
924
|
-
if (record.status === "running" || record.status === "queued") {
|
|
925
|
-
// Steering interrupts after the current tool call, exactly like the
|
|
926
|
-
// steer_subagent tool. Un-consume the result so the agent's reply to
|
|
927
|
-
// this message is still relayed even if the LLM read its last answer.
|
|
928
|
-
record.resultConsumed = false;
|
|
929
|
-
manager.steer(record.id, mention.message);
|
|
930
|
-
pi.events.emit("subagents:steered", { id: record.id, message: mention.message });
|
|
931
|
-
ctx.ui.notify(`Sent to ${target}`, "info");
|
|
932
|
-
return { action: "handled" };
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
if (record.session) {
|
|
936
|
-
// Both derived from the record's OWN type: a mention names an existing
|
|
937
|
-
// agent, so its frontmatter is what governs — `output_transcript: false`
|
|
938
|
-
// must keep holding, since record.outputFile is the sole gate every
|
|
939
|
-
// downstream consumer keys off and a resume must not re-open it.
|
|
940
|
-
const config = getAgentConfig(record.type);
|
|
941
|
-
const resumedRecord = await startBackgroundResume(ctx, record, mention.message, {
|
|
942
|
-
outputTranscript: config?.outputTranscript ?? getOutputTranscriptDefault(),
|
|
943
|
-
maxTurns: normalizeMaxTurns(config?.maxTurns ?? getDefaultMaxTurns()),
|
|
944
|
-
});
|
|
945
|
-
ctx.ui.notify(
|
|
946
|
-
resumedRecord ? `Resuming ${target}` : `Could not resume ${target} — it is still running.`,
|
|
947
|
-
resumedRecord ? "info" : "warning",
|
|
948
|
-
);
|
|
949
|
-
return { action: "handled" };
|
|
950
|
-
}
|
|
951
|
-
// A live record with no session never got far enough to continue, so it
|
|
952
|
-
// falls through to the start-fresh path below, like Claude's
|
|
953
|
-
// `no_transcript`.
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// Evicted, but its conversation is still on disk: reopen it. This is an
|
|
957
|
-
// ordinary spawn carrying a session file, so the new record picks up the
|
|
958
|
-
// widget row, transcript and completion notification unchanged —
|
|
959
|
-
// and `reclaim` hands it back the names the tombstone was holding.
|
|
960
|
-
if (resolved?.kind === "tombstone") {
|
|
961
|
-
const entry = resolved.entry;
|
|
962
|
-
const target = `@${entry.alias ?? entry.handle}`;
|
|
963
|
-
|
|
964
|
-
// Checked here rather than left to SessionManager.open: that runs inside
|
|
965
|
-
// runAgent, whose rejection lands on the record as an agent error, not in
|
|
966
|
-
// the catch below. A `/new` in another pi window or a manual delete makes
|
|
967
|
-
// the conversation unrecoverable (Claude Code's `not_reachable`), so drop
|
|
968
|
-
// the entry — a row that can only ever fail is worse than none — and say
|
|
969
|
-
// so rather than quietly sending this message to an unrelated agent.
|
|
970
|
-
if (!existsSync(entry.sessionFile)) {
|
|
971
|
-
manager.dropTombstone(entry.handle);
|
|
972
|
-
ctx.ui.notify(`Could not resume ${target} — its session is gone.`, "warning");
|
|
973
|
-
return { action: "handled" };
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// The Agent tool deliberately falls back to general-purpose for a type it
|
|
977
|
-
// cannot resolve (#183), which covers a deleted file AND a merely
|
|
978
|
-
// disabled one. A resume must not inherit that: reopening this
|
|
979
|
-
// conversation under a different agent's prompt and tools is not
|
|
980
|
-
// continuing it, and the new record would re-tombstone under the
|
|
981
|
-
// substitute, so the handle would never find its way back.
|
|
982
|
-
reloadCustomAgents();
|
|
983
|
-
const dispatch = resolveSpawnType(entry.type);
|
|
984
|
-
if (!dispatch.ok || dispatch.fellBackFrom !== undefined) {
|
|
985
|
-
// The tombstone stays: re-enabling the agent makes the handle work
|
|
986
|
-
// again, which a drop would foreclose.
|
|
987
|
-
ctx.ui.notify(`Could not resume ${target} — the ${entry.type} agent is no longer available.`, "warning");
|
|
988
|
-
return { action: "handled" };
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
try {
|
|
992
|
-
// spawnResolved, not spawnTopLevel: the latter strips
|
|
993
|
-
// `resumeSessionFile` and `reclaim` as untrusted. This path is the
|
|
994
|
-
// exception — both come from a tombstone this extension wrote.
|
|
995
|
-
const id = spawnResolved(pi, ctx, dispatch.type, mention.message, {
|
|
996
|
-
description: entry.description,
|
|
997
|
-
reclaim: { handle: entry.handle, alias: entry.alias },
|
|
998
|
-
resumeSessionFile: entry.sessionFile,
|
|
999
|
-
isBackground: true,
|
|
1000
|
-
});
|
|
1001
|
-
// The agent may still be starting — wait, so a startup failure lands in
|
|
1002
|
-
// the catch below instead of being announced as a resume.
|
|
1003
|
-
await manager.awaitStartup(id);
|
|
1004
|
-
// The tombstone deliberately stays. `resolveMention` prefers the live
|
|
1005
|
-
// record holding these same names, so it cannot shadow the resume — and
|
|
1006
|
-
// if this run dies before establishing its own session, the original
|
|
1007
|
-
// transcript is still the right thing for the next mention to reopen.
|
|
1008
|
-
// Once the resumed record is evicted it overwrites this entry in place,
|
|
1009
|
-
// keyed by the same handle, so nothing accumulates.
|
|
1010
|
-
ctx.ui.notify(`Resuming ${target}`, "info");
|
|
1011
|
-
} catch (err) {
|
|
1012
|
-
// The type is already settled above, so what is left is a spawn-time
|
|
1013
|
-
// failure: a strict worktree-isolation error, an unusable cwd.
|
|
1014
|
-
ctx.ui.notify(
|
|
1015
|
-
`Could not resume ${target}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1016
|
-
"warning",
|
|
1017
|
-
);
|
|
1018
|
-
}
|
|
1019
|
-
return { action: "handled" };
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// No agent under that handle — but the name may still be an agent type, in
|
|
1023
|
-
// which case the mention starts one.
|
|
1024
|
-
const typeHandle = mention.handle;
|
|
1025
|
-
const type = resolveHandleToType(typeHandle, getAvailableTypes())
|
|
1026
|
-
?? (alias ? resolveHandleToType(alias, getAvailableTypes()) : undefined);
|
|
1027
|
-
if (!type) return { action: "continue" };
|
|
1028
|
-
|
|
1029
|
-
// Claude Code never starts the agent itself: `@agent-<type>` becomes an
|
|
1030
|
-
// attachment asking the main model to do it, and the model writes the
|
|
1031
|
-
// agent's prompt from the conversation rather than forwarding the typed
|
|
1032
|
-
// text. That buys a real `Agent` tool call — transcript, per-tool widget
|
|
1033
|
-
// detail, tool-use-id correlation, join grouping — and a prompt with the
|
|
1034
|
-
// context a cold spawn lacks.
|
|
1035
|
-
//
|
|
1036
|
-
// It also costs a visible turn, spent narrating a decision the user already
|
|
1037
|
-
// made by typing the handle. So the turn is taken by a clone of this
|
|
1038
|
-
// conversation instead (mention-clone.ts): same messages, same system
|
|
1039
|
-
// prompt, off-screen, holding only the `Agent` tool. Nothing reaches the
|
|
1040
|
-
// chat, and what it starts is an ordinary top-level agent.
|
|
1041
|
-
if (getAgentMentionMode() === "model") {
|
|
1042
|
-
const label = `@${handleBase(type)}`;
|
|
1043
|
-
// "Prompting", not "Starting": in this mode nothing starts until the
|
|
1044
|
-
// off-screen clone has taken a whole model turn writing the agent's
|
|
1045
|
-
// prompt, and that wait is the one thing the chat cannot show. `direct`
|
|
1046
|
-
// says "Started" because by then it has. The distinction tells the user
|
|
1047
|
-
// which of the two they are waiting on.
|
|
1048
|
-
ctx.ui.notify(`Prompting ${label}…`, "info");
|
|
1049
|
-
// Not awaited: the clone runs a full model turn, and prompt() is blocked
|
|
1050
|
-
// until this hook returns. The user gets their prompt back immediately
|
|
1051
|
-
// and the agent appears in the widget when it starts.
|
|
1052
|
-
void runMentionClone({ ctx, type, message: mention.message, agentTool: registeredAgentTool })
|
|
1053
|
-
.then(async (result) => {
|
|
1054
|
-
if (result.spawned) return;
|
|
1055
|
-
// A clone that could not run must not swallow the mention: start the
|
|
1056
|
-
// agent the direct way rather than leaving the user with a toast and
|
|
1057
|
-
// nothing running.
|
|
1058
|
-
try {
|
|
1059
|
-
const id = spawnTopLevel(pi, ctx, type, mention.message, {
|
|
1060
|
-
description: describeMention(mention.message),
|
|
1061
|
-
isBackground: true,
|
|
1062
|
-
});
|
|
1063
|
-
// Same reason as the direct path below: the agent may still be
|
|
1064
|
-
// starting, and a failure there must reach this catch.
|
|
1065
|
-
await manager.awaitStartup(id);
|
|
1066
|
-
ctx.ui.notify(`Started ${label} directly — ${result.error}`, "warning");
|
|
1067
|
-
} catch (err) {
|
|
1068
|
-
ctx.ui.notify(
|
|
1069
|
-
`Could not start ${label}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1070
|
-
"error",
|
|
1071
|
-
);
|
|
1072
|
-
}
|
|
1073
|
-
});
|
|
1074
|
-
return { action: "handled" };
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
try {
|
|
1078
|
-
// Nothing else to pass: runAgent resolves model, thinking and max turns
|
|
1079
|
-
// from the agent's own config when the spawn omits them, and the
|
|
1080
|
-
// manager's onStart/onComplete callbacks own the widget and completion
|
|
1081
|
-
// notification — the same contract the scheduler and
|
|
1082
|
-
// cross-extension RPC spawns run under.
|
|
1083
|
-
const id = spawnTopLevel(pi, ctx, type, mention.message, {
|
|
1084
|
-
description: describeMention(mention.message),
|
|
1085
|
-
isBackground: true,
|
|
1086
|
-
});
|
|
1087
|
-
// The agent may still be starting (a worktree copy is an awaited git
|
|
1088
|
-
// call) — report a failure that lands there as a failed start, not as a
|
|
1089
|
-
// "Started" toast for an agent that never ran.
|
|
1090
|
-
await manager.awaitStartup(id);
|
|
1091
|
-
ctx.ui.notify(`Started @${handleBase(type)}`, "info");
|
|
1092
|
-
} catch (err) {
|
|
1093
|
-
ctx.ui.notify(`Could not start @${handleBase(type)}: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1094
|
-
}
|
|
1095
|
-
return { action: "handled" };
|
|
1096
607
|
});
|
|
1097
608
|
|
|
1098
609
|
pi.on("session_before_switch", () => {
|
|
610
|
+
resetAgentMenuSelections();
|
|
611
|
+
// A switch is catchable. Stop and checkpoint live/queued agents before the
|
|
612
|
+
// old session context is discarded, then retain their unread history.
|
|
613
|
+
manager.abortAll();
|
|
1099
614
|
manager.clearCompleted(true);
|
|
1100
615
|
scheduler.stop();
|
|
1101
616
|
});
|
|
@@ -1103,10 +618,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1103
618
|
// On shutdown, abort all agents immediately and clean up.
|
|
1104
619
|
// If the session is going down, there's nothing left to consume agent results.
|
|
1105
620
|
pi.on("session_shutdown", async () => {
|
|
621
|
+
resetAgentMenuSelections();
|
|
1106
622
|
rpcHandle?.unsubSpawn();
|
|
1107
623
|
rpcHandle?.unsubStop();
|
|
1108
624
|
rpcHandle?.unsubPing();
|
|
1109
|
-
rpcHandle?.unsubConsume();
|
|
1110
625
|
rpcHandle = undefined;
|
|
1111
626
|
currentCtx = undefined;
|
|
1112
627
|
// Only release the global slot if this activation claimed it — a child
|
|
@@ -1115,66 +630,45 @@ export default function (pi: ExtensionAPI) {
|
|
|
1115
630
|
delete (globalThis as any)[MANAGER_KEY];
|
|
1116
631
|
}
|
|
1117
632
|
scheduler.stop();
|
|
1118
|
-
// Before abortAll, and not folded into it: a workflow owns a worker thread
|
|
1119
|
-
// as well as its children, and only its own signal terminates that.
|
|
1120
|
-
for (const task of workflowTasks.values()) task.abortController.abort();
|
|
1121
|
-
workflowTasks.clear();
|
|
1122
633
|
manager.abortAll();
|
|
1123
634
|
for (const timer of pendingNudges.values()) clearTimeout(timer);
|
|
1124
635
|
pendingNudges.clear();
|
|
1125
636
|
widget.dispose();
|
|
1126
|
-
|
|
1127
|
-
// extensions bound there can release what they armed in `session_start` (#242).
|
|
1128
|
-
// pi awaits this handler, and the process exits right after — unawaited, those
|
|
1129
|
-
// handlers would never run. Internally bounded, so a hung one can't strand quit.
|
|
1130
|
-
await manager.dispose(pi);
|
|
637
|
+
manager.dispose();
|
|
1131
638
|
});
|
|
1132
639
|
|
|
1133
|
-
// Live widget: show
|
|
1134
|
-
|
|
1135
|
-
// every agent; "background" = hide foreground (they already render inline as
|
|
1136
|
-
// the Agent tool result, so showing them here too is a duplicate, #118), keep
|
|
1137
|
-
// everything else; "off" = hide the widget entirely. Read live at render time.
|
|
1138
|
-
let widgetMode: WidgetMode = "background";
|
|
640
|
+
// Live widget: show all agents above the editor. Read live at render time.
|
|
641
|
+
let widgetMode: WidgetMode = "all";
|
|
1139
642
|
function getWidgetMode(): WidgetMode { return widgetMode; }
|
|
1140
|
-
const widget = new AgentWidget(
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
643
|
+
const widget = new AgentWidget(
|
|
644
|
+
manager,
|
|
645
|
+
agentActivity,
|
|
646
|
+
getWidgetMode,
|
|
647
|
+
{
|
|
648
|
+
canOpenHistory: (record) => canOpenAgentHistory(record, currentCtx?.cwd),
|
|
649
|
+
onOpen: (record, mode) => {
|
|
650
|
+
const ctx = currentCtx;
|
|
651
|
+
if (ctx) void viewAgentConversation(ctx as ExtensionCommandContext, record, mode);
|
|
652
|
+
},
|
|
1144
653
|
},
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
//
|
|
1152
|
-
//
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
function
|
|
1156
|
-
|
|
1157
|
-
// everything that just asks "are mentions live at all" — the suggestion list,
|
|
1158
|
-
// the steer and resume branches — reads this instead of the mode.
|
|
1159
|
-
function isAgentMentionsEnabled(): boolean { return agentMentionMode !== "off"; }
|
|
1160
|
-
|
|
1161
|
-
// Project/global default for writing the subagent .output transcript lives in
|
|
1162
|
-
// output-file.ts (both spawn paths read it). A custom agent's
|
|
1163
|
-
// `output_transcript` frontmatter overrides it per spawn; when the frontmatter
|
|
1164
|
-
// is silent, this default applies. Read live at spawn time.
|
|
654
|
+
);
|
|
655
|
+
function setWidgetMode(m: WidgetMode): void {
|
|
656
|
+
widgetMode = m;
|
|
657
|
+
widget.update();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Project/global default for writing the subagent .output transcript. A custom
|
|
661
|
+
// agent's `output_transcript` frontmatter overrides this per spawn; when the
|
|
662
|
+
// frontmatter is silent, this default applies. Read live at spawn time.
|
|
663
|
+
let outputTranscriptDefault = true;
|
|
664
|
+
function getOutputTranscriptDefault(): boolean { return outputTranscriptDefault; }
|
|
665
|
+
function setOutputTranscript(b: boolean): void { outputTranscriptDefault = b; }
|
|
1165
666
|
|
|
1166
667
|
// ---- Join mode configuration ----
|
|
1167
668
|
let defaultJoinMode: JoinMode = 'smart';
|
|
1168
669
|
function getDefaultJoinMode(): JoinMode { return defaultJoinMode; }
|
|
1169
670
|
function setDefaultJoinMode(mode: JoinMode) { defaultJoinMode = mode; }
|
|
1170
671
|
|
|
1171
|
-
// What an unqualified top-level spawn means. Defaults to background,
|
|
1172
|
-
// following Claude Code; `backgroundByDefault: false` restores the previous
|
|
1173
|
-
// foreground default. Nested spawns ignore this — see nested-tools.ts.
|
|
1174
|
-
let backgroundByDefault = true;
|
|
1175
|
-
function getBackgroundByDefault(): boolean { return backgroundByDefault; }
|
|
1176
|
-
function setBackgroundByDefault(b: boolean) { backgroundByDefault = b; }
|
|
1177
|
-
|
|
1178
672
|
// Master switch for the schedule subagent feature. Defaults to enabled.
|
|
1179
673
|
// Read once at extension init (before tool registration) so the Agent tool's
|
|
1180
674
|
// param schema reflects the persisted setting. Runtime toggles via /agents
|
|
@@ -1185,25 +679,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
1185
679
|
function isSchedulingEnabled(): boolean { return schedulingEnabled; }
|
|
1186
680
|
function setSchedulingEnabled(b: boolean) { schedulingEnabled = b; }
|
|
1187
681
|
|
|
1188
|
-
//
|
|
1189
|
-
//
|
|
1190
|
-
//
|
|
1191
|
-
//
|
|
1192
|
-
//
|
|
1193
|
-
//
|
|
1194
|
-
//
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
// loaded, an explicit choice may not.
|
|
1199
|
-
let workflowsEnabled = true;
|
|
1200
|
-
let workflowsPinned = false;
|
|
1201
|
-
function isWorkflowsEnabled(): boolean { return workflowsEnabled; }
|
|
1202
|
-
function isWorkflowsPinned(): boolean { return workflowsPinned; }
|
|
1203
|
-
function setWorkflowsEnabled(b: boolean) {
|
|
1204
|
-
workflowsEnabled = b;
|
|
1205
|
-
workflowsPinned = true;
|
|
1206
|
-
}
|
|
682
|
+
// ---- Scope models configuration ----
|
|
683
|
+
// When enabled, subagent model choices are validated against `enabledModels`
|
|
684
|
+
// from pi's settings — both global `<agentDir>/settings.json` and
|
|
685
|
+
// project-local `<cwd>/.pi/settings.json` (project overrides global).
|
|
686
|
+
// Off by default; opt-in via `/agents → Settings`. See docstring on
|
|
687
|
+
// SubagentsSettings.scopeModels for the hard-error vs warn-and-proceed
|
|
688
|
+
// policy and its rationale.
|
|
689
|
+
let scopeModelsEnabled = false;
|
|
690
|
+
function isScopeModelsEnabled(): boolean { return scopeModelsEnabled; }
|
|
691
|
+
function setScopeModelsEnabled(enabled: boolean): void { scopeModelsEnabled = enabled; }
|
|
1207
692
|
|
|
1208
693
|
// ---- Disable default agents configuration ----
|
|
1209
694
|
// When enabled, the three hardcoded default agents (general-purpose, Explore,
|
|
@@ -1268,104 +753,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
1268
753
|
}
|
|
1269
754
|
}
|
|
1270
755
|
|
|
1271
|
-
/**
|
|
1272
|
-
* Launch a detached resume of an existing agent and wire everything a
|
|
1273
|
-
* re-running agent needs: transcript anchoring, activity tracking, join-mode
|
|
1274
|
-
* batching, the widget refresh, and the `subagents:created` event.
|
|
1275
|
-
*
|
|
1276
|
-
* Shared by the Agent tool's `resume` + `run_in_background` branch and the
|
|
1277
|
-
* `@handle message` prompt mention — they differ only in how they report the
|
|
1278
|
-
* outcome. Returns the record, or undefined when the manager refused because
|
|
1279
|
-
* the agent is still running (see AgentManager.resume).
|
|
1280
|
-
*
|
|
1281
|
-
* Callers must have already established that the record has a session.
|
|
1282
|
-
*/
|
|
1283
|
-
async function startBackgroundResume(
|
|
1284
|
-
ctx: ExtensionContext,
|
|
1285
|
-
existing: AgentRecord,
|
|
1286
|
-
prompt: string,
|
|
1287
|
-
opts: { outputTranscript: boolean; maxTurns?: number; toolCallId?: string },
|
|
1288
|
-
): Promise<AgentRecord | undefined> {
|
|
1289
|
-
const id = existing.id;
|
|
1290
|
-
const joinMode = resolveJoinMode(defaultJoinMode, true);
|
|
1291
|
-
// Assigned unconditionally: the completion notification carries this as
|
|
1292
|
-
// `<tool-use-id>`, so a mention-resume (which passes none) has to CLEAR the
|
|
1293
|
-
// id left by the spawn that created the record. Keeping it would point the
|
|
1294
|
-
// orchestrator's new result at a tool call that was answered runs ago.
|
|
1295
|
-
existing.toolCallId = opts.toolCallId;
|
|
1296
|
-
if (joinMode) existing.joinMode = joinMode;
|
|
1297
|
-
// Reuse the agent's transcript rather than starting a fresh one: the
|
|
1298
|
-
// path is deterministic per agent+session, so writing an initial entry
|
|
1299
|
-
// would truncate the previous run's turns (see ensureOutputFile).
|
|
1300
|
-
if (opts.outputTranscript) {
|
|
1301
|
-
existing.outputFile = createOutputFilePath(ctx.cwd, id, ctx.sessionManager.getSessionId());
|
|
1302
|
-
ensureOutputFile(existing.outputFile);
|
|
1303
|
-
}
|
|
1304
|
-
// Anchor streaming past the turns already on disk, captured BEFORE the
|
|
1305
|
-
// run starts. The resumed prompt lands as an ordinary user message at
|
|
1306
|
-
// this index, so it is written exactly once.
|
|
1307
|
-
const transcriptAnchor = existing.session?.messages.length ?? 0;
|
|
1308
|
-
|
|
1309
|
-
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(opts.maxTurns);
|
|
1310
|
-
// resumeAgent has no onSessionCreated — the session predates this run —
|
|
1311
|
-
// so seed it directly, or the widget shows no context % for the agent.
|
|
1312
|
-
bgState.session = existing.session;
|
|
1313
|
-
|
|
1314
|
-
// No `signal`: a background spawn deliberately omits it, and a detached
|
|
1315
|
-
// resume must behave the same. Passing it would abort this agent when
|
|
1316
|
-
// the parent turn is interrupted (user Esc), while agents started with
|
|
1317
|
-
// run_in_background in that same turn keep going.
|
|
1318
|
-
const record = await manager.resume(id, prompt, undefined, {
|
|
1319
|
-
isBackground: true,
|
|
1320
|
-
onToolActivity: bgCallbacks.onToolActivity,
|
|
1321
|
-
onAssistantUsage: bgCallbacks.onAssistantUsage,
|
|
1322
|
-
// Fires when the run actually starts — immediately, or on queue
|
|
1323
|
-
// drain. Wiring it here (rather than after resume() returns) means a
|
|
1324
|
-
// resume stopped while still queued never started streaming, so
|
|
1325
|
-
// there is no subscription left behind for a later run to trip over.
|
|
1326
|
-
onStarted: () => {
|
|
1327
|
-
const rec = manager.getRecord(id);
|
|
1328
|
-
if (rec?.session && rec.outputFile) {
|
|
1329
|
-
rec.outputCleanup = streamToOutputFile(rec.session, rec.outputFile, id, ctx.cwd, transcriptAnchor);
|
|
1330
|
-
}
|
|
1331
|
-
},
|
|
1332
|
-
});
|
|
1333
|
-
if (!record) return undefined;
|
|
1334
|
-
|
|
1335
|
-
if (joinMode != null && joinMode !== 'async') {
|
|
1336
|
-
currentBatchAgents.push({ id, joinMode });
|
|
1337
|
-
if (batchFinalizeTimer) clearTimeout(batchFinalizeTimer);
|
|
1338
|
-
batchFinalizeTimer = setTimeout(finalizeBatch, 100);
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
agentActivity.set(id, bgState);
|
|
1342
|
-
// This agent already finished once, so the widget holds a finished-age
|
|
1343
|
-
// for it that is past the linger limit — without clearing it, the
|
|
1344
|
-
// resumed run's ✓/✗ line never renders and the agent just vanishes.
|
|
1345
|
-
widget.markRunning(id);
|
|
1346
|
-
widget.ensureTimer();
|
|
1347
|
-
widget.update();
|
|
1348
|
-
|
|
1349
|
-
// Resume ignores subagent_type (the record keeps the type it was
|
|
1350
|
-
// spawned with), so report the record's own identity — a "created"
|
|
1351
|
-
// event carrying the caller's type would re-register the agent under
|
|
1352
|
-
// the wrong one in cross-extension mirrors keyed by id.
|
|
1353
|
-
pi.events.emit("subagents:created", {
|
|
1354
|
-
id,
|
|
1355
|
-
type: existing.type,
|
|
1356
|
-
description: existing.description,
|
|
1357
|
-
isBackground: true,
|
|
1358
|
-
});
|
|
1359
|
-
|
|
1360
|
-
return record;
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
756
|
// Grab UI context from first tool execution + clear lingering widget on new turn
|
|
1364
757
|
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
1365
|
-
|
|
758
|
+
widget.setUICtx(ctx.ui as UICtx);
|
|
1366
759
|
widget.onTurnStart();
|
|
1367
760
|
});
|
|
1368
761
|
|
|
762
|
+
/** Format an agent's tool scope: "*" when it has all built-ins, else a comma-separated list. */
|
|
763
|
+
const formatToolsSuffix = (cfg: AgentConfig | undefined): string => {
|
|
764
|
+
const tools = cfg?.builtinToolNames;
|
|
765
|
+
if (!tools || tools.length === 0) return "*";
|
|
766
|
+
const isFullSet =
|
|
767
|
+
tools.length === BUILTIN_TOOL_NAMES.length
|
|
768
|
+
&& BUILTIN_TOOL_NAMES.every((t) => tools.includes(t));
|
|
769
|
+
return isFullSet ? "*" : tools.join(", ");
|
|
770
|
+
};
|
|
771
|
+
|
|
1369
772
|
/** Build the full type list text dynamically from available agents only. */
|
|
1370
773
|
const buildTypeListText = () => {
|
|
1371
774
|
const available = getAvailableTypes();
|
|
@@ -1405,28 +808,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1405
808
|
applyAndEmitLoaded(
|
|
1406
809
|
{
|
|
1407
810
|
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
1408
|
-
setMaxConcurrentForeground: (n) => manager.setMaxConcurrentForeground(n),
|
|
1409
811
|
setDefaultMaxTurns,
|
|
1410
812
|
setGraceTurns,
|
|
1411
813
|
setDefaultJoinMode,
|
|
1412
|
-
setBackgroundByDefault,
|
|
1413
814
|
setSchedulingEnabled,
|
|
1414
815
|
setScopeModels: setScopeModelsEnabled,
|
|
1415
|
-
setStrictAgentFiles: (b) => { strictAgentFiles = b; },
|
|
1416
816
|
setDisableDefaultAgents: setDisableDefaultAgents,
|
|
1417
817
|
setToolDescriptionMode: setToolDescriptionMode,
|
|
1418
|
-
setAgentMentions: setAgentMentionMode,
|
|
1419
|
-
setRememberAgents,
|
|
1420
818
|
setWidgetMode: setWidgetMode,
|
|
1421
|
-
setOutputTranscript:
|
|
1422
|
-
setWorktreeIsolation: setWorktreeIsolationEnabled,
|
|
1423
|
-
setWorkflowsEnabled: setWorkflowsEnabled,
|
|
1424
|
-
setMaxSubagentDepth: setMaxSubagentDepth,
|
|
1425
|
-
setFallbackSubagent: setFallbackSubagent,
|
|
1426
|
-
setReportUsage,
|
|
1427
|
-
setShowCost,
|
|
1428
|
-
setShowModel,
|
|
1429
|
-
setViewerMarkdown,
|
|
819
|
+
setOutputTranscript: setOutputTranscript,
|
|
1430
820
|
},
|
|
1431
821
|
(event, payload) => pi.events.emit(event, payload),
|
|
1432
822
|
);
|
|
@@ -1455,21 +845,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1455
845
|
? `\n- Use \`schedule\` only when the user explicitly asked for scheduled / recurring / delayed execution (e.g. "every Monday", "in an hour"). Don't auto-schedule from vague intent like "monitor X" — run once now or ask.`
|
|
1456
846
|
: "";
|
|
1457
847
|
|
|
1458
|
-
// Same trade as scheduleParam/scheduleGuideline above: `isolationParam` drops
|
|
1459
|
-
// the field from the schema when the project set `worktreeIsolation: false`,
|
|
1460
|
-
// so the prose has to go with it. Left in, it would teach the model to pass a
|
|
1461
|
-
// parameter that isn't declared — accepted (TypeBox sets no
|
|
1462
|
-
// `additionalProperties: false`) and then silently dropped by the resolver.
|
|
1463
|
-
// With no per-result note by design, the model would have every reason to go
|
|
1464
|
-
// on reporting a `pi-agent-*` branch that was never created.
|
|
1465
|
-
const isolationGuideline = isWorktreeIsolationEnabled()
|
|
1466
|
-
? `\n- Use isolation: "worktree" to give the agent its own git worktree (safe parallel file modifications); leave it unset, or pass "off", for none. The worktree is removed when the agent finishes; if it made changes, they are committed to a branch and the branch is named in the result.`
|
|
1467
|
-
: "";
|
|
1468
|
-
|
|
1469
|
-
const isolationCompactGuideline = isWorktreeIsolationEnabled()
|
|
1470
|
-
? `\n- isolation: "worktree" gives the agent its own git worktree (removed on completion); changes land on a branch named in the result.`
|
|
1471
|
-
: "";
|
|
1472
|
-
|
|
1473
848
|
// Compact Agent tool description (#91, `toolDescriptionMode: "compact"`) —
|
|
1474
849
|
// the same load-bearing facts as the full version at ~75% fewer tokens, for
|
|
1475
850
|
// small/local models. Per-option details live in the param descriptions.
|
|
@@ -1480,10 +855,10 @@ Custom agents: .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.
|
|
|
1480
855
|
|
|
1481
856
|
Notes:
|
|
1482
857
|
- description: 3-5 words (shown in UI). Prompts must be self-contained — the agent has not seen this conversation.
|
|
1483
|
-
- Parallel work: one message, multiple Agent calls —
|
|
1484
|
-
- Subagents run in the background by default; you'll be notified when one completes. Pass run_in_background: false only when your very next action depends on the result and nothing else could usefully happen while it runs. Never fabricate or predict a pending agent's results — if the user asks before the notification arrives, say it's still running.
|
|
858
|
+
- Parallel work: one message, multiple Agent calls, run_in_background: true on each. You are notified when background agents finish — never poll or sleep.
|
|
1485
859
|
- The result is not shown to the user — summarize it for them. Verify an agent's claimed code changes before reporting work done.
|
|
1486
|
-
- resume continues a previous agent by ID; steer_subagent messages a running one
|
|
860
|
+
- resume continues a previous agent by ID; steer_subagent messages a running one.
|
|
861
|
+
- isolation: "worktree" runs the agent in an isolated git worktree; changes land on a branch.`;
|
|
1487
862
|
|
|
1488
863
|
const fullAgentToolDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
|
|
1489
864
|
|
|
@@ -1501,23 +876,23 @@ If the target is already known, use a direct tool — \`read\` for a known path,
|
|
|
1501
876
|
## Usage notes
|
|
1502
877
|
|
|
1503
878
|
- Always include a short (3-5 word) description summarizing what the agent will do (shown in UI).
|
|
1504
|
-
- When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently. If the user specifies that they want
|
|
879
|
+
- When you launch multiple agents for independent work, send them in a single message with multiple tool uses, with run_in_background: true on each, so they run concurrently. If the user specifies that they want agents run "in parallel", you MUST send a single message with multiple tool calls. Foreground calls run sequentially — only one executes at a time.
|
|
1505
880
|
- When the agent is done, it returns a single message back to you. The result is not visible to the user — to show the user, send a text message with a concise summary.
|
|
1506
|
-
- Trust but verify: an agent's summary describes what it intended to do, not necessarily what it did. When an agent writes or edits code, check the actual changes before reporting
|
|
1507
|
-
-
|
|
1508
|
-
-
|
|
1509
|
-
- **Don't race**: after launching a background agent, you know nothing about its results. Never fabricate or predict them in any format — not as prose, summary, or structured output. The completion notification arrives in a later turn; it is never something you write yourself. If the user asks before it lands, say the agent is still running — give status, not a guess.
|
|
881
|
+
- Trust but verify: an agent's summary describes what it intended to do, not necessarily what it did. When an agent writes or edits code, check the actual changes before reporting work as done.
|
|
882
|
+
- Use run_in_background for work you don't need immediately. You will be notified when it completes — do NOT poll or sleep waiting for it. Continue with other work or respond to the user instead.
|
|
883
|
+
- Foreground vs background: use foreground (default) when you need the agent's results before you can proceed. Use background when you have genuinely independent work to do in parallel.
|
|
1510
884
|
- Use resume with an agent ID to continue a previous agent's work. A new (non-resume) Agent call starts a fresh agent with no memory of prior runs, so the prompt must be self-contained.
|
|
1511
885
|
- Use steer_subagent to send mid-run messages to a running background agent.
|
|
1512
886
|
- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent.
|
|
1513
887
|
- If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
|
|
1514
888
|
- Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
|
|
1515
889
|
- Use thinking to control extended thinking level.
|
|
1516
|
-
- Use inherit_context if the agent needs the parent conversation history
|
|
890
|
+
- Use inherit_context if the agent needs the parent conversation history.
|
|
891
|
+
- Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications). The worktree is automatically cleaned up if the agent makes no changes; otherwise the path and branch are returned in the result.${scheduleGuideline}
|
|
1517
892
|
|
|
1518
893
|
## Writing the prompt
|
|
1519
894
|
|
|
1520
|
-
|
|
895
|
+
Provide clear, detailed prompts so the agent can work autonomously. Brief it like a smart colleague who just walked into the room — it hasn't seen this conversation, doesn't know what you've tried, doesn't understand why this task matters.
|
|
1521
896
|
- Explain what you're trying to accomplish and why.
|
|
1522
897
|
- Describe what you've already learned or ruled out.
|
|
1523
898
|
- Give enough context about the surrounding problem that the agent can make judgment calls rather than just following a narrow instruction.
|
|
@@ -1537,7 +912,6 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1537
912
|
typeList: buildTypeListText,
|
|
1538
913
|
compactTypeList: buildCompactTypeListText,
|
|
1539
914
|
agentDir: getAgentDir,
|
|
1540
|
-
isolationGuideline: () => isolationGuideline,
|
|
1541
915
|
scheduleGuideline: () => scheduleGuideline,
|
|
1542
916
|
};
|
|
1543
917
|
// Replacement callback (not a string) — agent descriptions may contain `$&` etc.
|
|
@@ -1576,10 +950,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1576
950
|
return fullAgentToolDescription;
|
|
1577
951
|
})();
|
|
1578
952
|
|
|
1579
|
-
|
|
1580
|
-
// definition, so the agent it starts is an ordinary top-level spawn instead
|
|
1581
|
-
// of a second implementation that has to be kept in step with this one.
|
|
1582
|
-
const agentTool = defineTool({
|
|
953
|
+
pi.registerTool(defineTool({
|
|
1583
954
|
name: SUBAGENT_TOOL_NAMES.AGENT,
|
|
1584
955
|
label: "Agent",
|
|
1585
956
|
description: agentToolDescription,
|
|
@@ -1597,12 +968,6 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1597
968
|
description: Type.String({
|
|
1598
969
|
description: "A short (3-5 word) description of the task (shown in UI).",
|
|
1599
970
|
}),
|
|
1600
|
-
name: Type.Optional(
|
|
1601
|
-
Type.String({
|
|
1602
|
-
description:
|
|
1603
|
-
'Optional memorable name for this agent, e.g. "auth-audit", so it can be addressed as `@name` at the prompt and by steer_subagent / get_subagent_result. Letters, digits, `_` and `-`. Worth setting when several agents of the same type run at once; omit for one-off work. The agent stays reachable by its type either way.',
|
|
1604
|
-
}),
|
|
1605
|
-
),
|
|
1606
971
|
subagent_type: Type.String({
|
|
1607
972
|
description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available.`,
|
|
1608
973
|
}),
|
|
@@ -1625,12 +990,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1625
990
|
),
|
|
1626
991
|
run_in_background: Type.Optional(
|
|
1627
992
|
Type.Boolean({
|
|
1628
|
-
description: "
|
|
993
|
+
description: "Set to true to run in background. Returns agent ID immediately. You will be notified on completion.",
|
|
1629
994
|
}),
|
|
1630
995
|
),
|
|
1631
996
|
resume: Type.Optional(
|
|
1632
997
|
Type.String({
|
|
1633
|
-
description: "Optional agent ID to resume from. Continues from previous context.
|
|
998
|
+
description: "Optional agent ID to resume from. Continues from previous context.",
|
|
1634
999
|
}),
|
|
1635
1000
|
),
|
|
1636
1001
|
isolated: Type.Optional(
|
|
@@ -1643,40 +1008,26 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1643
1008
|
description: "If true, fork parent conversation into the agent. Default: false (fresh context).",
|
|
1644
1009
|
}),
|
|
1645
1010
|
),
|
|
1646
|
-
|
|
1011
|
+
isolation: Type.Optional(
|
|
1012
|
+
Type.Literal("worktree", {
|
|
1013
|
+
description: 'Set to "worktree" to run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.',
|
|
1014
|
+
}),
|
|
1015
|
+
),
|
|
1647
1016
|
...scheduleParam,
|
|
1648
1017
|
}),
|
|
1649
1018
|
|
|
1650
1019
|
// ---- Custom rendering: Claude Code style ----
|
|
1651
1020
|
|
|
1652
|
-
renderCall(args, theme
|
|
1653
|
-
|
|
1654
|
-
// for the rest of the line, so the badge restores it. The tint is opened here too:
|
|
1655
|
-
// the TUI's Box paints it, but HTML export takes it from CSS, and restoring a
|
|
1656
|
-
// background the line never opened is what banded the export before. The line is
|
|
1657
|
-
// deliberately left open — Box.applyBackgroundToLine pads to width and *then*
|
|
1658
|
-
// wraps, so closing here would leave that padding untinted, and HTML export closes
|
|
1659
|
-
// any open span per line anyway. No badge means no tint, so an uncolored agent
|
|
1660
|
-
// renders exactly the line it always did.
|
|
1661
|
-
const rowBackground = hasAgentBadge(args.subagent_type)
|
|
1662
|
-
? theme.getBgAnsi(context.isPartial ? "toolPendingBg" : context.isError ? "toolErrorBg" : "toolSuccessBg")
|
|
1663
|
-
: "";
|
|
1021
|
+
renderCall(args, theme) {
|
|
1022
|
+
const displayName = args.subagent_type ? getDisplayName(args.subagent_type) : "Agent";
|
|
1664
1023
|
const desc = args.description ?? "";
|
|
1665
|
-
|
|
1666
|
-
fallbackColor: "toolTitle",
|
|
1667
|
-
restoreBackground: rowBackground,
|
|
1668
|
-
bold: true,
|
|
1669
|
-
});
|
|
1670
|
-
return new Text(rowBackground + "▸ " + name + (desc ? " " + theme.fg("muted", desc) : ""), 0, 0);
|
|
1024
|
+
return new Text("▸ " + theme.fg("toolTitle", theme.bold(displayName)) + (desc ? " " + theme.fg("muted", desc) : ""), 0, 0);
|
|
1671
1025
|
},
|
|
1672
1026
|
|
|
1673
|
-
renderResult(result, { expanded, isPartial }, theme
|
|
1027
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
1674
1028
|
const details = result.details as AgentDetails | undefined;
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
// validation) as `{ content: [reason], details: {} }` with isError set —
|
|
1678
|
-
// no status to render, so show the reason instead of inventing one (#199).
|
|
1679
|
-
if (renderContext.isError || !details?.status) {
|
|
1029
|
+
if (!details) {
|
|
1030
|
+
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
1680
1031
|
return new Text(text, 0, 0);
|
|
1681
1032
|
}
|
|
1682
1033
|
|
|
@@ -1690,10 +1041,6 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1690
1041
|
}
|
|
1691
1042
|
if (d.toolUses > 0) parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
|
|
1692
1043
|
if (d.tokens) parts.push(d.tokens);
|
|
1693
|
-
if (showCost) {
|
|
1694
|
-
const costText = formatCost(d.cost ?? 0);
|
|
1695
|
-
if (costText) parts.push(costText);
|
|
1696
|
-
}
|
|
1697
1044
|
return parts.map(p => fgPreservingNestedStyles(theme, "dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
1698
1045
|
};
|
|
1699
1046
|
|
|
@@ -1744,12 +1091,6 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1744
1091
|
return new Text(line, 0, 0);
|
|
1745
1092
|
}
|
|
1746
1093
|
|
|
1747
|
-
// Anything left ("queued", or a status added later) has no rendering of
|
|
1748
|
-
// its own — the turn-limit wording below must not be the catch-all.
|
|
1749
|
-
if (details.status !== "error" && details.status !== "aborted") {
|
|
1750
|
-
return new Text(text, 0, 0);
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
1094
|
// ---- Error / Aborted (hard max_turns) ----
|
|
1754
1095
|
const s = stats(details);
|
|
1755
1096
|
let line = theme.fg("error", "✗") + (s ? " " + s : "");
|
|
@@ -1773,40 +1114,16 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1773
1114
|
reloadCustomAgents();
|
|
1774
1115
|
|
|
1775
1116
|
const rawType = params.subagent_type as SubagentType;
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
// the caller is still unaware. `fallbackSubagent` decides whether an
|
|
1780
|
-
// unresolvable type falls back or fails closed.
|
|
1781
|
-
const dispatch = resolveSpawnType(rawType);
|
|
1782
|
-
// `resume` replays a stored session and ignores `subagent_type` entirely,
|
|
1783
|
-
// but the parameter is required by the schema — so gating it here would
|
|
1784
|
-
// make a live agent unresumable the moment its type is deleted, disabled,
|
|
1785
|
-
// or gains a case-clashing sibling. Only a real spawn is gated.
|
|
1786
|
-
if (!dispatch.ok && !params.resume) return textResult(dispatch.message);
|
|
1787
|
-
const subagentType = dispatch.ok ? dispatch.type : rawType;
|
|
1788
|
-
// What the caller actually asked for, named once: `fellBackFrom` is "" for
|
|
1789
|
-
// a blank request, so reading it inline invites the `??`-vs-`||` slip that
|
|
1790
|
-
// once persisted an empty type into a scheduled job.
|
|
1791
|
-
const requestedType = (dispatch.ok && dispatch.fellBackFrom) || subagentType;
|
|
1792
|
-
// Computed at resolution rather than after the run, so the background and
|
|
1793
|
-
// schedule branches carry it too — previously it existed only on the
|
|
1794
|
-
// foreground path. Resume deliberately doesn't: it replays the stored
|
|
1795
|
-
// session and ignores `subagent_type` entirely, so a note about type
|
|
1796
|
-
// substitution would be describing something that didn't happen.
|
|
1797
|
-
const fallbackNote = dispatch.ok && dispatch.fellBackFrom !== undefined
|
|
1798
|
-
? `Note: Unknown agent type "${dispatch.fellBackFrom}" — using ${resolveType(subagentType) ? subagentType : "the fallback agent config"}.\n\n`
|
|
1799
|
-
: "";
|
|
1117
|
+
const resolved = resolveType(rawType);
|
|
1118
|
+
const subagentType = resolved ?? "general-purpose";
|
|
1119
|
+
const fellBack = resolved === undefined;
|
|
1800
1120
|
|
|
1801
1121
|
const displayName = getDisplayName(subagentType);
|
|
1802
1122
|
|
|
1803
1123
|
// Get agent config (if any)
|
|
1804
1124
|
const customConfig = getAgentConfig(subagentType);
|
|
1805
1125
|
|
|
1806
|
-
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params
|
|
1807
|
-
worktreeAllowed: isWorktreeIsolationEnabled(),
|
|
1808
|
-
defaultRunInBackground: getBackgroundByDefault(),
|
|
1809
|
-
});
|
|
1126
|
+
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params);
|
|
1810
1127
|
|
|
1811
1128
|
// Resolve model from agent config first; tool-call params only fill gaps.
|
|
1812
1129
|
let model = ctx.model;
|
|
@@ -1821,18 +1138,33 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1821
1138
|
}
|
|
1822
1139
|
|
|
1823
1140
|
// Scope validation: the effective resolved model is checked against the
|
|
1824
|
-
// user's enabledModels list
|
|
1825
|
-
//
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1141
|
+
// user's enabledModels list (read in `enabled-models.ts`).
|
|
1142
|
+
//
|
|
1143
|
+
// Design: scopeModels guards against *runtime* LLM choices, not user-level config.
|
|
1144
|
+
// - Caller-supplied out-of-scope → hard error (the orchestrator made an explicit
|
|
1145
|
+
// out-of-scope choice; surface it so it picks differently).
|
|
1146
|
+
// - Frontmatter-pinned or parent-inherited out-of-scope → warn but proceed (the
|
|
1147
|
+
// user authored/installed this agent or chose the parent's model; trust it).
|
|
1148
|
+
// See SubagentsSettings.scopeModels docstring for the full policy.
|
|
1149
|
+
if (isScopeModelsEnabled() && model) {
|
|
1150
|
+
const allowed = resolveEnabledModels(readEnabledModels(ctx.cwd), ctx.modelRegistry, ctx.cwd);
|
|
1151
|
+
if (allowed && !isModelInScope(model, allowed)) {
|
|
1152
|
+
if (resolvedConfig.modelFromParams) {
|
|
1153
|
+
const list = [...allowed].sort().map(m => ` ${m}`).join("\n");
|
|
1154
|
+
return textResult(
|
|
1155
|
+
`Model not in scope: "${resolvedConfig.modelInput}".\n\n` +
|
|
1156
|
+
`Allowed models (from enabledModels):\n${list}`,
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
// Frontmatter-pinned or parent-inherited: warn + proceed.
|
|
1160
|
+
const agentLabel = customConfig?.displayName ?? subagentType;
|
|
1161
|
+
const modelLabel = resolvedConfig.modelInput ?? `${model.provider}/${model.id}`;
|
|
1162
|
+
ctx.ui.notify(
|
|
1163
|
+
`Agent "${agentLabel}" using out-of-scope model "${modelLabel}"`,
|
|
1164
|
+
"warning",
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1836
1168
|
|
|
1837
1169
|
const thinking = resolvedConfig.thinking;
|
|
1838
1170
|
const inheritContext = resolvedConfig.inheritContext;
|
|
@@ -1849,34 +1181,33 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1849
1181
|
if (!rec || !outputTranscript) return;
|
|
1850
1182
|
rec.outputFile = createOutputFilePath(ctx.cwd, agentId, ctx.sessionManager.getSessionId());
|
|
1851
1183
|
writeInitialEntry(rec.outputFile, agentId, params.prompt, ctx.cwd);
|
|
1184
|
+
|
|
1185
|
+
try {
|
|
1186
|
+
rec.historyFile = createAgentHistoryPath(ctx.cwd, agentId);
|
|
1187
|
+
rec.transcriptPath = agentHistoryLocator(ctx.cwd, rec.historyFile);
|
|
1188
|
+
writeInitialEntry(rec.historyFile, agentId, params.prompt, ctx.cwd);
|
|
1189
|
+
manager.setTranscript(agentId, rec.historyFile, rec.transcriptPath, ctx.cwd);
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
rec.historyFile = undefined;
|
|
1192
|
+
rec.transcriptPath = undefined;
|
|
1193
|
+
ctx.ui.notify(
|
|
1194
|
+
`Could not create durable transcript for agent ${agentId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1195
|
+
"warning",
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1852
1198
|
};
|
|
1853
1199
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
const { modelName, modelId } = model ? describeModel(model) : { modelName: undefined, modelId: undefined };
|
|
1860
|
-
// What the caller SPELLED, kept only if it names a different model than the
|
|
1861
|
-
// one that won. Model input is fuzzy — `"haiku"` and
|
|
1862
|
-
// `"anthropic/claude-haiku-4-5"` are the same model — so comparing the two
|
|
1863
|
-
// strings would disclose an override that never happened. A spelling that
|
|
1864
|
-
// resolves to nothing is still worth disclosing: it cannot have taken effect.
|
|
1865
|
-
const askedModel = ((asked: string | undefined) => {
|
|
1866
|
-
if (!asked) return undefined;
|
|
1867
|
-
const resolvedAsked = resolveModel(asked, ctx.modelRegistry);
|
|
1868
|
-
if (typeof resolvedAsked === "string") return asked;
|
|
1869
|
-
return resolvedAsked.provider === model?.provider && resolvedAsked.id === model?.id ? undefined : asked;
|
|
1870
|
-
})(resolvedConfig.overridden?.model);
|
|
1200
|
+
const parentModelId = ctx.model?.id;
|
|
1201
|
+
const effectiveModelId = model?.id;
|
|
1202
|
+
const modelName = effectiveModelId && effectiveModelId !== parentModelId
|
|
1203
|
+
? (model?.name ?? effectiveModelId).replace(/^Claude\s+/i, "").toLowerCase()
|
|
1204
|
+
: undefined;
|
|
1871
1205
|
const effectiveMaxTurns = normalizeMaxTurns(resolvedConfig.maxTurns ?? getDefaultMaxTurns());
|
|
1872
1206
|
const agentInvocation: AgentInvocation = {
|
|
1873
1207
|
modelName,
|
|
1874
|
-
|
|
1208
|
+
effectiveModelName: model?.name ?? model?.id ?? ctx.model?.name ?? ctx.model?.id,
|
|
1875
1209
|
thinking,
|
|
1876
|
-
|
|
1877
|
-
// disclose a parameter that was accepted but could not take effect (#182).
|
|
1878
|
-
requestedThinking: resolvedConfig.overridden?.thinking,
|
|
1879
|
-
requestedModel: askedModel,
|
|
1210
|
+
effectiveThinking: thinking,
|
|
1880
1211
|
// Explicit value only — the default fallback would just add noise.
|
|
1881
1212
|
// Normalize so `0` (unlimited) doesn't surface as a misleading "max turns: 0".
|
|
1882
1213
|
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
@@ -1897,34 +1228,6 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1897
1228
|
tags: agentTags.length > 0 ? agentTags : undefined,
|
|
1898
1229
|
};
|
|
1899
1230
|
|
|
1900
|
-
/**
|
|
1901
|
-
* `detailBase` for a record that exists, which outranks it: the base is a
|
|
1902
|
-
* snapshot of what this call REQUESTED, and pi may have resolved a
|
|
1903
|
-
* different model or clamped the thinking level (agent-manager writes the
|
|
1904
|
-
* effective values back when the session reports them). Resume goes
|
|
1905
|
-
* further and ignores the model/thinking parameters outright — it runs on
|
|
1906
|
-
* the session it is reopening — so rendering the base there advertises
|
|
1907
|
-
* settings the run never used.
|
|
1908
|
-
*
|
|
1909
|
-
* The mode label is rebuilt rather than carried over: it hangs off the
|
|
1910
|
-
* agent TYPE, not the invocation, so tags taken straight from
|
|
1911
|
-
* buildInvocationTags would silently drop `twin`.
|
|
1912
|
-
*/
|
|
1913
|
-
const detailBaseFor = (rec: AgentRecord | undefined): typeof detailBase => {
|
|
1914
|
-
if (!rec?.invocation) return detailBase;
|
|
1915
|
-
const type = rec.type;
|
|
1916
|
-
const { modelName: recModelName, tags } = buildInvocationTags(rec.invocation);
|
|
1917
|
-
const recModeLabel = getPromptModeLabel(type);
|
|
1918
|
-
const recTags = recModeLabel ? [recModeLabel, ...tags] : tags;
|
|
1919
|
-
return {
|
|
1920
|
-
displayName: getDisplayName(type),
|
|
1921
|
-
description: rec.description,
|
|
1922
|
-
subagentType: type,
|
|
1923
|
-
modelName: recModelName,
|
|
1924
|
-
tags: recTags.length > 0 ? recTags : undefined,
|
|
1925
|
-
};
|
|
1926
|
-
};
|
|
1927
|
-
|
|
1928
1231
|
// ---- Schedule: register a job, don't spawn now ----
|
|
1929
1232
|
if (params.schedule) {
|
|
1930
1233
|
if (!isSchedulingEnabled()) {
|
|
@@ -1947,9 +1250,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1947
1250
|
name: params.description as string,
|
|
1948
1251
|
description: params.description as string,
|
|
1949
1252
|
schedule: params.schedule as string,
|
|
1950
|
-
|
|
1951
|
-
// at fire time, and the original is what a user edits.
|
|
1952
|
-
subagent_type: requestedType,
|
|
1253
|
+
subagent_type: subagentType,
|
|
1953
1254
|
prompt: params.prompt as string,
|
|
1954
1255
|
model: params.model as string | undefined,
|
|
1955
1256
|
thinking: thinking,
|
|
@@ -1959,7 +1260,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1959
1260
|
});
|
|
1960
1261
|
const next = scheduler.getNextRun(job.id);
|
|
1961
1262
|
return textResult(
|
|
1962
|
-
|
|
1263
|
+
`Scheduled "${job.name}" (id: ${job.id}, type: ${job.scheduleType}). ` +
|
|
1963
1264
|
`Next run: ${next ?? "(unknown)"}. ` +
|
|
1964
1265
|
`Manage via /agents → Scheduled jobs.`,
|
|
1965
1266
|
);
|
|
@@ -1971,53 +1272,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1971
1272
|
// Resume existing agent
|
|
1972
1273
|
if (params.resume) {
|
|
1973
1274
|
const existing = manager.getRecord(params.resume);
|
|
1974
|
-
if (!existing
|
|
1275
|
+
if (!existing) {
|
|
1975
1276
|
return textResult(`Agent not found: "${params.resume}". It may have been cleaned up.`);
|
|
1976
1277
|
}
|
|
1977
1278
|
if (!existing.session) {
|
|
1978
1279
|
return textResult(`Agent "${params.resume}" has no active session to resume.`);
|
|
1979
1280
|
}
|
|
1980
|
-
|
|
1981
|
-
// Background resume: detached run that notifies on completion, mirroring
|
|
1982
|
-
// a background spawn. Previously run_in_background was silently ignored
|
|
1983
|
-
// on resume (this branch returned before the background branch below),
|
|
1984
|
-
// so a resumed agent always blocked the main loop until it finished.
|
|
1985
|
-
if (runInBackground) {
|
|
1986
|
-
const id = existing.id;
|
|
1987
|
-
// A detached resume hands control back while the record stays
|
|
1988
|
-
// "running", so nothing stops the model from resuming the same agent
|
|
1989
|
-
// again mid-run. manager.resume() refuses that (it would orphan the
|
|
1990
|
-
// live run's abort controller); say why here, where the model can act
|
|
1991
|
-
// on it, instead of letting it read as a generic failure.
|
|
1992
|
-
if (existing.status === "running" || existing.status === "queued") {
|
|
1993
|
-
return textResult(
|
|
1994
|
-
`Agent "${params.resume}" is still ${existing.status} — it can only be resumed once its current run finishes.\n` +
|
|
1995
|
-
`Use steer_subagent to send it a message mid-run, or get_subagent_result to wait for it.`,
|
|
1996
|
-
);
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
const record = await startBackgroundResume(ctx, existing, params.prompt, {
|
|
2000
|
-
outputTranscript,
|
|
2001
|
-
maxTurns: effectiveMaxTurns,
|
|
2002
|
-
toolCallId,
|
|
2003
|
-
});
|
|
2004
|
-
if (!record) {
|
|
2005
|
-
return textResult(`Failed to resume agent "${params.resume}".`);
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
const isQueued = record.status === "queued";
|
|
2009
|
-
return textResult(
|
|
2010
|
-
`Agent ${isQueued ? "queued" : "resumed"} in background.\n` +
|
|
2011
|
-
`Agent ID: ${id}\n` +
|
|
2012
|
-
`Type: ${existing.type}\n` +
|
|
2013
|
-
(record.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
2014
|
-
(isQueued ? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n` : "") +
|
|
2015
|
-
`\nYou will be notified when this agent completes.\n` +
|
|
2016
|
-
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.`,
|
|
2017
|
-
{ ...detailBaseFor(record), toolUses: record.toolUses, tokens: "", durationMs: 0, status: "background" as const, agentId: id },
|
|
2018
|
-
);
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
1281
|
const record = await manager.resume(params.resume, params.prompt, signal);
|
|
2022
1282
|
if (!record) {
|
|
2023
1283
|
return textResult(`Failed to resume agent "${params.resume}".`);
|
|
@@ -2025,11 +1285,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2025
1285
|
// A failed resume surfaces the error, plus any partial output THIS
|
|
2026
1286
|
// resume produced (never the previous turn's answer, #144).
|
|
2027
1287
|
if (record.status === "error") {
|
|
2028
|
-
return textResult(`Agent failed: ${record.error}${partialOutputSuffix(record)}`, buildDetails(
|
|
1288
|
+
return textResult(`Agent failed: ${record.error}${partialOutputSuffix(record)}`, buildDetails(detailBase, record));
|
|
2029
1289
|
}
|
|
2030
1290
|
return textResult(
|
|
2031
1291
|
record.result?.trim() || "No output.",
|
|
2032
|
-
buildDetails(
|
|
1292
|
+
buildDetails(detailBase, record),
|
|
2033
1293
|
);
|
|
2034
1294
|
}
|
|
2035
1295
|
|
|
@@ -2038,53 +1298,47 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2038
1298
|
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(effectiveMaxTurns);
|
|
2039
1299
|
|
|
2040
1300
|
// Wrap onSessionCreated to wire output file streaming.
|
|
2041
|
-
// The callback
|
|
2042
|
-
//
|
|
1301
|
+
// The callback reads the transcript paths installed synchronously by
|
|
1302
|
+
// onSpawned before the agent can queue or start.
|
|
2043
1303
|
let id: string;
|
|
1304
|
+
const joinMode = resolveJoinMode(defaultJoinMode, true);
|
|
2044
1305
|
const origBgOnSession = bgCallbacks.onSessionCreated;
|
|
2045
1306
|
bgCallbacks.onSessionCreated = (session: any) => {
|
|
2046
1307
|
origBgOnSession(session);
|
|
2047
1308
|
const rec = manager.getRecord(id);
|
|
2048
1309
|
if (rec?.outputFile) {
|
|
2049
|
-
rec.outputCleanup = streamToOutputFile(session, rec.outputFile, id, ctx.cwd,
|
|
1310
|
+
rec.outputCleanup = streamToOutputFile(session, rec.outputFile, id, ctx.cwd, rec.historyFile);
|
|
2050
1311
|
}
|
|
2051
1312
|
};
|
|
2052
1313
|
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
1314
|
+
try {
|
|
1315
|
+
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
1316
|
+
description: params.description,
|
|
1317
|
+
model,
|
|
1318
|
+
maxTurns: effectiveMaxTurns,
|
|
1319
|
+
isolated,
|
|
1320
|
+
inheritContext,
|
|
1321
|
+
thinkingLevel: thinking,
|
|
1322
|
+
isBackground: true,
|
|
1323
|
+
isolation,
|
|
1324
|
+
invocation: agentInvocation,
|
|
1325
|
+
onSpawned: (spawnedId) => {
|
|
1326
|
+
attachTranscript(manager.getRecord(spawnedId), spawnedId);
|
|
1327
|
+
},
|
|
1328
|
+
...bgCallbacks,
|
|
1329
|
+
});
|
|
1330
|
+
} catch (err) {
|
|
1331
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
1332
|
+
}
|
|
2071
1333
|
|
|
2072
|
-
// Set
|
|
2073
|
-
//
|
|
2074
|
-
const joinMode = resolveJoinMode(defaultJoinMode, true);
|
|
1334
|
+
// Set join metadata after spawn. Transcript metadata was installed by
|
|
1335
|
+
// the manager's synchronous onSpawned callback before this point.
|
|
2075
1336
|
const record = manager.getRecord(id);
|
|
2076
1337
|
if (record && joinMode) {
|
|
2077
1338
|
record.joinMode = joinMode;
|
|
2078
1339
|
record.toolCallId = toolCallId;
|
|
2079
|
-
attachTranscript(record, id);
|
|
2080
1340
|
}
|
|
2081
1341
|
|
|
2082
|
-
// With isolation: "worktree" the agent isn't running yet — the repo
|
|
2083
|
-
// copy is an awaited git call. Wait for it here, after the synchronous
|
|
2084
|
-
// wiring above, so a strict-isolation failure still fails THIS tool
|
|
2085
|
-
// call instead of being reported as a subagent that ran (#179).
|
|
2086
|
-
await manager.awaitStartup(id);
|
|
2087
|
-
|
|
2088
1342
|
if (joinMode == null || joinMode === 'async') {
|
|
2089
1343
|
// Foreground/no join mode or explicit async — not part of any batch
|
|
2090
1344
|
} else {
|
|
@@ -2099,6 +1353,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2099
1353
|
agentActivity.set(id, bgState);
|
|
2100
1354
|
widget.ensureTimer();
|
|
2101
1355
|
widget.update();
|
|
1356
|
+
|
|
2102
1357
|
// Emit created event
|
|
2103
1358
|
pi.events.emit("subagents:created", {
|
|
2104
1359
|
id,
|
|
@@ -2109,7 +1364,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2109
1364
|
|
|
2110
1365
|
const isQueued = record?.status === "queued";
|
|
2111
1366
|
return textResult(
|
|
2112
|
-
|
|
1367
|
+
`Agent ${isQueued ? "queued" : "started"} in background.\n` +
|
|
2113
1368
|
`Agent ID: ${id}\n` +
|
|
2114
1369
|
`Type: ${displayName}\n` +
|
|
2115
1370
|
`Description: ${params.description}\n` +
|
|
@@ -2118,7 +1373,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2118
1373
|
`\nYou will be notified when this agent completes.\n` +
|
|
2119
1374
|
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
|
|
2120
1375
|
`Do not duplicate this agent's work.`,
|
|
2121
|
-
{ ...
|
|
1376
|
+
{ ...detailBase, toolUses: 0, tokens: "", durationMs: 0, status: "background" as const, agentId: id },
|
|
2122
1377
|
);
|
|
2123
1378
|
}
|
|
2124
1379
|
|
|
@@ -2126,33 +1381,17 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2126
1381
|
let spinnerFrame = 0;
|
|
2127
1382
|
const startedAt = Date.now();
|
|
2128
1383
|
let fgId: string | undefined;
|
|
2129
|
-
// Set only while the spawn is parked on a foreground concurrency slot
|
|
2130
|
-
// (maxConcurrentForeground); undefined the rest of the time, including
|
|
2131
|
-
// always when the limit is unset.
|
|
2132
|
-
let queuedAhead: number | undefined;
|
|
2133
1384
|
|
|
2134
1385
|
const streamUpdate = () => {
|
|
2135
|
-
// Spend from the record, everything else from the live tracker. `fgId`
|
|
2136
|
-
// is set in onSessionCreated below, which fires before the first
|
|
2137
|
-
// assistant message — so nothing is spent while this reads zero.
|
|
2138
|
-
const fgRecord = fgId ? manager.getRecord(fgId) : undefined;
|
|
2139
1386
|
const details: AgentDetails = {
|
|
2140
|
-
...
|
|
1387
|
+
...detailBase,
|
|
2141
1388
|
toolUses: fgState.toolUses,
|
|
2142
|
-
tokens:
|
|
2143
|
-
cost: fgRecord ? getLifetimeCost(fgRecord.lifetimeUsage) : 0,
|
|
1389
|
+
tokens: formatLifetimeTokens(fgState),
|
|
2144
1390
|
turnCount: fgState.turnCount,
|
|
2145
1391
|
maxTurns: fgState.maxTurns,
|
|
2146
1392
|
durationMs: Date.now() - startedAt,
|
|
2147
|
-
// Deliberately still "running" while queued: the renderer routes any
|
|
2148
|
-
// status it doesn't know to raw text (see the catch-all below), which
|
|
2149
|
-
// would drop the spinner and read as hung. Only the activity line
|
|
2150
|
-
// changes — "thinking…" would be a lie for an agent that has not
|
|
2151
|
-
// started and may not for minutes.
|
|
2152
1393
|
status: "running",
|
|
2153
|
-
activity:
|
|
2154
|
-
? describeActivity(fgState.activeTools, fgState.responseText)
|
|
2155
|
-
: `queued — waiting for a foreground slot${queuedAhead > 0 ? ` (${queuedAhead} ahead)` : ""}`,
|
|
1394
|
+
activity: describeActivity(fgState.activeTools, fgState.responseText),
|
|
2156
1395
|
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
2157
1396
|
};
|
|
2158
1397
|
onUpdate?.({
|
|
@@ -2169,18 +1408,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2169
1408
|
const origOnSession = fgCallbacks.onSessionCreated;
|
|
2170
1409
|
fgCallbacks.onSessionCreated = (session: any) => {
|
|
2171
1410
|
origOnSession(session);
|
|
2172
|
-
// It really started — stop reporting it as queued, and repaint now
|
|
2173
|
-
// rather than leaving the stale line up for the next spinner tick.
|
|
2174
|
-
// Guarded, so a spawn that never queued emits no extra update.
|
|
2175
|
-
if (queuedAhead !== undefined) {
|
|
2176
|
-
queuedAhead = undefined;
|
|
2177
|
-
streamUpdate();
|
|
2178
|
-
}
|
|
2179
1411
|
for (const a of manager.listAgents()) {
|
|
2180
1412
|
if (a.session === session) {
|
|
2181
1413
|
fgId = a.id;
|
|
2182
1414
|
agentActivity.set(a.id, fgState);
|
|
2183
1415
|
widget.ensureTimer();
|
|
1416
|
+
widget.update();
|
|
2184
1417
|
break;
|
|
2185
1418
|
}
|
|
2186
1419
|
}
|
|
@@ -2188,7 +1421,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2188
1421
|
if (fgId) {
|
|
2189
1422
|
const rec = manager.getRecord(fgId);
|
|
2190
1423
|
if (rec?.outputFile) {
|
|
2191
|
-
rec.outputCleanup = streamToOutputFile(session, rec.outputFile, fgId, ctx.cwd,
|
|
1424
|
+
rec.outputCleanup = streamToOutputFile(session, rec.outputFile, fgId, ctx.cwd, rec.historyFile);
|
|
2192
1425
|
}
|
|
2193
1426
|
}
|
|
2194
1427
|
};
|
|
@@ -2205,7 +1438,6 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2205
1438
|
try {
|
|
2206
1439
|
const fgResult = await manager.spawnAndWait(pi, ctx, subagentType, params.prompt, {
|
|
2207
1440
|
description: params.description,
|
|
2208
|
-
name: params.name as string | undefined,
|
|
2209
1441
|
model,
|
|
2210
1442
|
maxTurns: effectiveMaxTurns,
|
|
2211
1443
|
isolated,
|
|
@@ -2213,13 +1445,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2213
1445
|
thinkingLevel: thinking,
|
|
2214
1446
|
isolation,
|
|
2215
1447
|
invocation: agentInvocation,
|
|
2216
|
-
outputTranscript,
|
|
2217
1448
|
signal,
|
|
2218
|
-
rootSessionId: ctx.sessionManager.getSessionId(),
|
|
2219
|
-
// Deliberately does NOT set fgId: that drives agentActivity, the
|
|
2220
|
-
// widget and the `finally` cleanup below, none of which should see an
|
|
2221
|
-
// agent that has no session and may never get one.
|
|
2222
|
-
onQueued: (_id, ahead) => { queuedAhead = ahead; streamUpdate(); },
|
|
2223
1449
|
...fgCallbacks,
|
|
2224
1450
|
}, (fgAgentId) => {
|
|
2225
1451
|
// onSpawned: called synchronously after spawn, before onSessionCreated fires.
|
|
@@ -2228,22 +1454,29 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2228
1454
|
attachTranscript(fgRec, fgAgentId);
|
|
2229
1455
|
});
|
|
2230
1456
|
record = fgResult.record;
|
|
2231
|
-
}
|
|
2232
|
-
// Runs on both paths, so a startup throw — which now propagates, see
|
|
2233
|
-
// the background spawn above (#179) — no longer leaves the spinner
|
|
2234
|
-
// ticking or a finished agent on the widget.
|
|
1457
|
+
} catch (err) {
|
|
2235
1458
|
clearInterval(spinnerInterval);
|
|
2236
|
-
|
|
2237
|
-
agentActivity.delete(fgId);
|
|
2238
|
-
widget.markFinished(fgId);
|
|
2239
|
-
}
|
|
1459
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
2240
1460
|
}
|
|
2241
1461
|
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
1462
|
+
clearInterval(spinnerInterval);
|
|
1463
|
+
|
|
1464
|
+
// Clean up foreground agent from widget
|
|
1465
|
+
if (fgId) {
|
|
1466
|
+
agentActivity.delete(fgId);
|
|
1467
|
+
widget.markFinished(fgId);
|
|
1468
|
+
}
|
|
2245
1469
|
|
|
2246
|
-
|
|
1470
|
+
// Get final token count
|
|
1471
|
+
const tokenText = formatLifetimeTokens(fgState);
|
|
1472
|
+
|
|
1473
|
+
const details = buildDetails(detailBase, record, fgState, { tokens: tokenText });
|
|
1474
|
+
|
|
1475
|
+
// "general-purpose" may itself be unregistered (defaults disabled, no
|
|
1476
|
+
// user override) — getConfig then uses the hardcoded fallback config.
|
|
1477
|
+
const fallbackNote = fellBack
|
|
1478
|
+
? `Note: Unknown agent type "${rawType}" — using ${resolveType("general-purpose") ? "general-purpose" : "the fallback agent config"}.\n\n`
|
|
1479
|
+
: "";
|
|
2247
1480
|
|
|
2248
1481
|
if (record.status === "error") {
|
|
2249
1482
|
// Error headline + any partial output the run produced before failing.
|
|
@@ -2253,459 +1486,25 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2253
1486
|
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
2254
1487
|
const statsParts = [`${record.toolUses} tool uses`];
|
|
2255
1488
|
if (tokenText) statsParts.push(tokenText);
|
|
2256
|
-
if (showCost) {
|
|
2257
|
-
const costText = formatCost(getLifetimeCost(record.lifetimeUsage));
|
|
2258
|
-
if (costText) statsParts.push(costText);
|
|
2259
|
-
}
|
|
2260
1489
|
return textResult(
|
|
2261
|
-
`${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${
|
|
1490
|
+
`${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
|
|
2262
1491
|
(record.result?.trim() || "No output."),
|
|
2263
1492
|
details,
|
|
2264
1493
|
);
|
|
2265
1494
|
},
|
|
2266
|
-
});
|
|
2267
|
-
/**
|
|
2268
|
-
* Wrap a tool so its results carry back whatever subagent spend the parent
|
|
2269
|
-
* session has not been told about yet (see `PendingUsagePool`).
|
|
2270
|
-
*
|
|
2271
|
-
* Pi copies `AgentToolResult.usage` onto the persisted tool-result message and
|
|
2272
|
-
* folds it into `getSessionStats()`, which is what the footer, the statusline
|
|
2273
|
-
* and `/cost` read — so this is the whole of "report usage to the parent".
|
|
2274
|
-
*
|
|
2275
|
-
* Nothing is attached to a call with no tool-call id. That is the `@handle`
|
|
2276
|
-
* mention path (`mention-clone.ts`), which invokes this tool from a fork of the
|
|
2277
|
-
* conversation that is discarded moments later: the result never becomes a
|
|
2278
|
-
* message in the real session, so usage hung on it would be spend the user paid
|
|
2279
|
-
* for and nobody counted. Skipping leaves it pending for the next real result.
|
|
2280
|
-
*/
|
|
2281
|
-
function withUsageReporting<T extends { execute: (...args: any[]) => any }>(tool: T): T {
|
|
2282
|
-
return {
|
|
2283
|
-
...tool,
|
|
2284
|
-
execute: async (toolCallId: string | undefined, ...rest: any[]) => {
|
|
2285
|
-
const result = await tool.execute(toolCallId, ...rest);
|
|
2286
|
-
if (!reportUsage || !toolCallId) return result;
|
|
2287
|
-
const usage = pendingUsage.drain();
|
|
2288
|
-
return usage ? { ...result, usage } : result;
|
|
2289
|
-
},
|
|
2290
|
-
};
|
|
2291
|
-
}
|
|
2292
|
-
function registerToolReportingUsage(tool: any): void {
|
|
2293
|
-
pi.registerTool(withUsageReporting(tool));
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
// The mention path is handed THIS object, not the bare `agentTool` — see the
|
|
2297
|
-
// mention-clone header on why the clone must call the registered tool.
|
|
2298
|
-
const registeredAgentTool = withUsageReporting(agentTool);
|
|
2299
|
-
pi.registerTool(registeredAgentTool);
|
|
2300
|
-
|
|
2301
|
-
// ---- Workflow tool ----
|
|
2302
|
-
|
|
2303
|
-
/**
|
|
2304
|
-
* Live runs, by task id. The tool returns before the run finishes, so its
|
|
2305
|
-
* result card looks the task up here on every render rather than freezing a
|
|
2306
|
-
* snapshot into `details` — that is what makes the inline card follow a
|
|
2307
|
-
* background run.
|
|
2308
|
-
*/
|
|
2309
|
-
const workflowTasks = new Map<string, WorkflowTask>();
|
|
2310
|
-
|
|
2311
|
-
/**
|
|
2312
|
-
* Run a task to completion against the real manager, settling the record
|
|
2313
|
-
* either way. Never rejects: a run that cannot start (bad `meta`, oversized
|
|
2314
|
-
* source, non-JSON `args`) is a failed workflow, and both callers here are
|
|
2315
|
-
* detached — a rejection would surface as an unhandled one.
|
|
2316
|
-
*/
|
|
2317
|
-
async function runWorkflowTask(ctx: ExtensionContext, task: WorkflowTask): Promise<void> {
|
|
2318
|
-
try {
|
|
2319
|
-
const result = await runWorkflow({
|
|
2320
|
-
script: task.script,
|
|
2321
|
-
args: task.args,
|
|
2322
|
-
signal: task.abortController.signal,
|
|
2323
|
-
host: createWorkflowHost({
|
|
2324
|
-
pi,
|
|
2325
|
-
ctx,
|
|
2326
|
-
manager,
|
|
2327
|
-
signal: task.abortController.signal,
|
|
2328
|
-
rootSessionId: ctx.sessionManager.getSessionId(),
|
|
2329
|
-
workflowId: task.id,
|
|
2330
|
-
}),
|
|
2331
|
-
onProgress: entries => updateWorkflowProgressBatch(task, entries),
|
|
2332
|
-
// The dialog's pause / skip / retry keys run through this; it is dropped
|
|
2333
|
-
// again when the task settles.
|
|
2334
|
-
onControl: control => { task.control = control; },
|
|
2335
|
-
journal: {
|
|
2336
|
-
...(task.replay !== undefined ? { entries: task.replay } : {}),
|
|
2337
|
-
...(task.journalPath !== undefined
|
|
2338
|
-
? { append: (entry: WorkflowJournalEntry) => appendJournal(task.journalPath!, entry) }
|
|
2339
|
-
: {}),
|
|
2340
|
-
},
|
|
2341
|
-
});
|
|
2342
|
-
completeWorkflowTask(task, result);
|
|
2343
|
-
} catch (err) {
|
|
2344
|
-
failWorkflowTask(task, err instanceof Error ? err.message : String(err));
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
|
-
/**
|
|
2349
|
-
* Hand a finished run back to the model through the SAME channel a background
|
|
2350
|
-
* agent uses — held briefly by `scheduleNudge`, delivered as a follow-up that
|
|
2351
|
-
* triggers a turn, rendered by the existing `subagent-notification` renderer.
|
|
2352
|
-
*/
|
|
2353
|
-
function notifyWorkflowFinished(task: WorkflowTask) {
|
|
2354
|
-
widget.update();
|
|
2355
|
-
const result = workflowResultText(task);
|
|
2356
|
-
scheduleNudge(task.id, () => {
|
|
2357
|
-
pi.sendMessage<NotificationDetails>({
|
|
2358
|
-
customType: "subagent-notification",
|
|
2359
|
-
content: formatWorkflowNotification(task),
|
|
2360
|
-
display: true,
|
|
2361
|
-
details: {
|
|
2362
|
-
id: task.id,
|
|
2363
|
-
description: `Workflow ${task.workflowName ?? task.id}`,
|
|
2364
|
-
status: task.status === "completed" ? "completed" : task.status === "killed" ? "stopped" : "error",
|
|
2365
|
-
toolUses: task.totalToolCalls,
|
|
2366
|
-
// A workflow has agents, not turns; rendering "↻0" would be noise.
|
|
2367
|
-
turnCount: 0,
|
|
2368
|
-
totalTokens: task.totalTokens,
|
|
2369
|
-
durationMs: elapsedMs(task, Date.now()),
|
|
2370
|
-
error: task.error,
|
|
2371
|
-
resultPreview: result.length > 500 ? `${result.slice(0, 500)}…` : result,
|
|
2372
|
-
},
|
|
2373
|
-
}, { deliverAs: "followUp", triggerTurn: true });
|
|
2374
|
-
});
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
// Defined unconditionally, registered only when the feature is on — the same
|
|
2378
|
-
// shape the Agent tool uses. Keeping the definition out of the `if` means the
|
|
2379
|
-
// switch changes exactly one thing: whether pi is ever told about the tool.
|
|
2380
|
-
const workflowTool = defineTool({
|
|
2381
|
-
name: SUBAGENT_TOOL_NAMES.WORKFLOW,
|
|
2382
|
-
label: "SubagentWorkflow",
|
|
2383
|
-
description: renderToolDescriptionTemplate(fullWorkflowToolDescription),
|
|
2384
|
-
promptSnippet: "Run a deterministic script that orchestrates many subagents",
|
|
2385
|
-
promptGuidelines: [
|
|
2386
|
-
"Use SubagentWorkflow when the number of agents depends on something discovered at runtime, when work flows through stages, or when findings should be independently verified. Use Agent for one delegated task or a handful you can name up front.",
|
|
2387
|
-
"Prefer `pipeline` over `parallel` — a barrier costs wall-clock whenever the stages are unevenly sized.",
|
|
2388
|
-
"A workflow runs in the background and notifies you when it finishes — do not poll or sleep waiting for it.",
|
|
2389
|
-
],
|
|
2390
|
-
parameters: Type.Object({
|
|
2391
|
-
script: Type.Optional(
|
|
2392
|
-
Type.String({
|
|
2393
|
-
maxLength: 524288,
|
|
2394
|
-
description: "Inline workflow source. Must begin with `export const meta = { name, description }`.",
|
|
2395
|
-
}),
|
|
2396
|
-
),
|
|
2397
|
-
scriptPath: Type.Optional(
|
|
2398
|
-
Type.String({
|
|
2399
|
-
description:
|
|
2400
|
-
"Path to a workflow script file, absolute or relative to the project. Takes precedence over `script` — this is how you re-run an edited workflow.",
|
|
2401
|
-
}),
|
|
2402
|
-
),
|
|
2403
|
-
name: Type.Optional(
|
|
2404
|
-
Type.String({
|
|
2405
|
-
description:
|
|
2406
|
-
"Name of a saved workflow — `<name>.js` in .pi/workflows/, .agents/workflows/ or the user's agent dir. Lowest precedence: `scriptPath` and `script` both win over it.",
|
|
2407
|
-
}),
|
|
2408
|
-
),
|
|
2409
|
-
args: Type.Optional(
|
|
2410
|
-
Type.Any({
|
|
2411
|
-
description: "Exposed to the script as the global `args`, verbatim. Must be JSON-shaped.",
|
|
2412
|
-
}),
|
|
2413
|
-
),
|
|
2414
|
-
resumeFromRunId: Type.Optional(
|
|
2415
|
-
Type.String({
|
|
2416
|
-
pattern: "^wf_[a-z0-9-]{6,}$",
|
|
2417
|
-
description:
|
|
2418
|
-
"Run id of an earlier workflow in this session. Its unchanged leading agent() calls return their recorded results instantly; the first changed or failed call, and everything after it, runs live. Same script and args means nothing re-runs.",
|
|
2419
|
-
}),
|
|
2420
|
-
),
|
|
2421
|
-
// Accepted and ignored, as in Claude Code. Models reach for them because
|
|
2422
|
-
// every other tool has them, and a hard schema rejection would cost a
|
|
2423
|
-
// whole turn to re-emit a script that was already correct. The `meta`
|
|
2424
|
-
// block is the one place a workflow is named.
|
|
2425
|
-
title: Type.Optional(
|
|
2426
|
-
Type.String({ description: "Ignored — set the workflow title in the script's `meta` block." }),
|
|
2427
|
-
),
|
|
2428
|
-
description: Type.Optional(
|
|
2429
|
-
Type.String({ description: "Ignored — set the workflow description in the script's `meta` block." }),
|
|
2430
|
-
),
|
|
2431
|
-
}),
|
|
2432
|
-
|
|
2433
|
-
renderCall(args, theme) {
|
|
2434
|
-
return new Text(
|
|
2435
|
-
`${theme.fg("toolTitle", "▸ ")}${theme.bold(theme.fg("toolTitle", "SubagentWorkflow"))} ${theme.fg("muted", workflowCallName(args))}`,
|
|
2436
|
-
0,
|
|
2437
|
-
0,
|
|
2438
|
-
);
|
|
2439
|
-
},
|
|
2440
|
-
|
|
2441
|
-
renderResult(result, _options, theme, renderContext) {
|
|
2442
|
-
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
2443
|
-
const taskId = (result.details as { taskId?: string } | undefined)?.taskId;
|
|
2444
|
-
const task = taskId !== undefined ? workflowTasks.get(taskId) : undefined;
|
|
2445
|
-
// No task means the run predates this session (a reloaded transcript) or
|
|
2446
|
-
// the call never started one — show what `execute` said instead.
|
|
2447
|
-
if (renderContext.isError || !task) return new Text(text, 0, 0);
|
|
2448
|
-
return renderWorkflowCard(
|
|
2449
|
-
{
|
|
2450
|
-
progress: task.workflowProgress,
|
|
2451
|
-
task: {
|
|
2452
|
-
status: task.status,
|
|
2453
|
-
workflowName: task.workflowName,
|
|
2454
|
-
startTime: task.startTime,
|
|
2455
|
-
endTime: task.endTime,
|
|
2456
|
-
totalPausedMs: task.totalPausedMs,
|
|
2457
|
-
},
|
|
2458
|
-
meta: task.meta,
|
|
2459
|
-
agentCount: task.agentCount,
|
|
2460
|
-
totalTokens: task.totalTokens,
|
|
2461
|
-
},
|
|
2462
|
-
theme,
|
|
2463
|
-
);
|
|
2464
|
-
},
|
|
2465
|
-
|
|
2466
|
-
execute: async (toolCallId, params, _signal, _onUpdate, ctx) => {
|
|
2467
|
-
const resumeFrom = resolveResumeTarget(params.resumeFromRunId, workflowTasks);
|
|
2468
|
-
if (resumeFrom !== undefined && !resumeFrom.ok) return textResult(resumeFrom.message);
|
|
2469
|
-
|
|
2470
|
-
// A resume with no source of its own re-runs what that run ran. The
|
|
2471
|
-
// common case is an edited script, but "run that again, cheaply" should
|
|
2472
|
-
// not require repeating a path the run already knows.
|
|
2473
|
-
const resolved = resolveWorkflowScript(
|
|
2474
|
-
params.script === undefined && params.scriptPath === undefined && params.name === undefined
|
|
2475
|
-
&& resumeFrom !== undefined
|
|
2476
|
-
? { scriptPath: resumeFrom.scriptPath }
|
|
2477
|
-
: params,
|
|
2478
|
-
ctx.cwd,
|
|
2479
|
-
);
|
|
2480
|
-
if (!resolved.ok) return textResult(resolved.message);
|
|
2481
|
-
|
|
2482
|
-
// Parsed before anything is scheduled: a bad `meta` is an authoring error
|
|
2483
|
-
// the model can fix immediately, and reporting it as a background run
|
|
2484
|
-
// that failed a second later would just cost a turn.
|
|
2485
|
-
let meta: WorkflowMeta;
|
|
2486
|
-
try {
|
|
2487
|
-
meta = extractMeta(resolved.script).meta;
|
|
2488
|
-
} catch (err) {
|
|
2489
|
-
return textResult(err instanceof Error ? err.message : String(err));
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
const runId = workflowRunId();
|
|
2493
|
-
// Every invocation lands on disk next to the agent transcripts, so
|
|
2494
|
-
// iterating is edit-the-file-then-rerun-with-scriptPath rather than
|
|
2495
|
-
// re-emitting the whole source. The journal sits beside it under the same
|
|
2496
|
-
// id, which is what makes a run id enough to resume from.
|
|
2497
|
-
let savedPath: string | undefined;
|
|
2498
|
-
let journalPath: string | undefined;
|
|
2499
|
-
try {
|
|
2500
|
-
const dir = sessionTaskDir(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
2501
|
-
savedPath = join(dir, `${runId}.workflow.js`);
|
|
2502
|
-
writeFileSync(savedPath, resolved.script, "utf-8");
|
|
2503
|
-
journalPath = join(dir, `${runId}.workflow.jsonl`);
|
|
2504
|
-
} catch (err) {
|
|
2505
|
-
savedPath = undefined;
|
|
2506
|
-
journalPath = undefined;
|
|
2507
|
-
console.warn(`[pi-subagents] could not persist workflow script: ${err instanceof Error ? err.message : String(err)}`);
|
|
2508
|
-
}
|
|
2509
|
-
|
|
2510
|
-
const replay = resumeFrom !== undefined ? readJournal(resumeFrom.journalPath) : undefined;
|
|
2511
|
-
|
|
2512
|
-
const task = createWorkflowTask({
|
|
2513
|
-
id: runId,
|
|
2514
|
-
script: resolved.script,
|
|
2515
|
-
scriptPath: resolved.scriptPath ?? savedPath,
|
|
2516
|
-
args: params.args,
|
|
2517
|
-
meta,
|
|
2518
|
-
toolCallId,
|
|
2519
|
-
...(journalPath !== undefined ? { journalPath } : {}),
|
|
2520
|
-
...(replay !== undefined && replay.length > 0 ? { replay, resumedFrom: resumeFrom!.runId } : {}),
|
|
2521
|
-
});
|
|
2522
|
-
workflowTasks.set(runId, task);
|
|
2523
|
-
// The run's own row has to appear now, not when it settles. Its agents
|
|
2524
|
-
// are owned by it, so their lifecycle callbacks no longer refresh these
|
|
2525
|
-
// surfaces — nothing else would register the widget for a run whose
|
|
2526
|
-
// first agent has not started yet.
|
|
2527
|
-
widget.update();
|
|
2528
|
-
|
|
2529
|
-
// Background, like Claude Code: the id comes back now and the run keeps
|
|
2530
|
-
// going without the tool call.
|
|
2531
|
-
void runWorkflowTask(ctx, task).then(() => notifyWorkflowFinished(task));
|
|
2532
|
-
|
|
2533
|
-
return {
|
|
2534
|
-
content: [{
|
|
2535
|
-
type: "text" as const,
|
|
2536
|
-
text:
|
|
2537
|
-
`Workflow "${meta.name}" started in the background.\n` +
|
|
2538
|
-
`Task ID: ${runId}\n` +
|
|
2539
|
-
(task.scriptPath ? `Script: ${task.scriptPath}\n` : "") +
|
|
2540
|
-
(task.resumedFrom !== undefined
|
|
2541
|
-
? `Resuming ${task.resumedFrom}: ${task.replay?.length ?? 0} recorded call(s) available to replay.\n`
|
|
2542
|
-
: params.resumeFromRunId !== undefined
|
|
2543
|
-
? `Nothing to replay from ${params.resumeFromRunId} — every agent runs live.\n`
|
|
2544
|
-
: "") +
|
|
2545
|
-
`\nYou will be notified when it finishes — do NOT poll or sleep waiting for it.\n` +
|
|
2546
|
-
`To iterate, edit the script file and call SubagentWorkflow again with scriptPath.`,
|
|
2547
|
-
}],
|
|
2548
|
-
details: { taskId: runId },
|
|
2549
|
-
};
|
|
2550
|
-
},
|
|
2551
|
-
});
|
|
2552
|
-
|
|
2553
|
-
if (isWorkflowsEnabled()) pi.registerTool(workflowTool);
|
|
2554
|
-
|
|
2555
|
-
/**
|
|
2556
|
-
* Act on {@link decideWorkflowCollision} — the half that needs the host.
|
|
2557
|
-
*
|
|
2558
|
-
* The policy (what counts as a conflict, what a pin changes, whether there is
|
|
2559
|
-
* anything left to withdraw) lives in `workflow/collisions.ts`; this is the
|
|
2560
|
-
* host-facing shell around it: read the registry, warn, and take our tool out
|
|
2561
|
-
* of the active set.
|
|
2562
|
-
*
|
|
2563
|
-
* ## Why this can only happen at session_start
|
|
2564
|
-
*
|
|
2565
|
-
* `getAllTools` throws during extension loading ("Action methods cannot be
|
|
2566
|
-
* called during extension loading"), and load order means a check at
|
|
2567
|
-
* registration time could not see an extension that has not loaded yet. So
|
|
2568
|
-
* the decision cannot gate `registerTool`; it has to undo it. `setActiveTools`
|
|
2569
|
-
* is what makes that real rather than cosmetic — pi rebuilds the system
|
|
2570
|
-
* prompt from the new set, and `session_start` runs before any turn, so the
|
|
2571
|
-
* model never sees a spec we withdrew. A later `_refreshToolRegistry` keeps
|
|
2572
|
-
* the active set it had and only adds names new to the registry, so ours does
|
|
2573
|
-
* not creep back.
|
|
2574
|
-
*
|
|
2575
|
-
* Best-effort and swallowed. A diagnostic that took the session down would be
|
|
2576
|
-
* worse than the collision it reports.
|
|
2577
|
-
*/
|
|
2578
|
-
let collisionsChecked = false;
|
|
2579
|
-
function resolveWorkflowCollisions(ctx: ExtensionContext): void {
|
|
2580
|
-
if (collisionsChecked) return;
|
|
2581
|
-
collisionsChecked = true;
|
|
2582
|
-
|
|
2583
|
-
const warn = (message: string) => {
|
|
2584
|
-
if (ctx.hasUI) ctx.ui.notify(message, "warning");
|
|
2585
|
-
else console.warn(`[pi-subagents] ${message}`);
|
|
2586
|
-
};
|
|
2587
|
-
|
|
2588
|
-
try {
|
|
2589
|
-
if (!isWorkflowsEnabled()) return;
|
|
2590
|
-
|
|
2591
|
-
const verdict = decideWorkflowCollision({
|
|
2592
|
-
tools: pi.getAllTools(),
|
|
2593
|
-
// Identifies our own registration: this extension does not know its
|
|
2594
|
-
// install path, and the description is the one field certainly ours.
|
|
2595
|
-
ownDescription: workflowTool.description,
|
|
2596
|
-
pinned: isWorkflowsPinned(),
|
|
2597
|
-
});
|
|
2598
|
-
if (verdict.kind === "none") return;
|
|
2599
|
-
if (verdict.kind === "report") {
|
|
2600
|
-
warn(verdict.message);
|
|
2601
|
-
return;
|
|
2602
|
-
}
|
|
2603
|
-
|
|
2604
|
-
workflowsEnabled = false; // not setWorkflowsEnabled: this is not the user pinning it
|
|
2605
|
-
widget.update();
|
|
2606
|
-
warn(verdict.message);
|
|
2607
|
-
|
|
2608
|
-
if (!verdict.withdraw) return;
|
|
2609
|
-
const active = pi.getActiveTools();
|
|
2610
|
-
if (active.includes(SUBAGENT_TOOL_NAMES.WORKFLOW)) {
|
|
2611
|
-
pi.setActiveTools(active.filter(name => name !== SUBAGENT_TOOL_NAMES.WORKFLOW));
|
|
2612
|
-
}
|
|
2613
|
-
} catch {
|
|
2614
|
-
// getAllTools/setActiveTools are unavailable in some hosts (print mode,
|
|
2615
|
-
// RPC). Not being able to check is not a reason to fail the session.
|
|
2616
|
-
}
|
|
2617
|
-
}
|
|
2618
|
-
|
|
2619
|
-
/**
|
|
2620
|
-
* `--subagents-workflow-file=<path>` — run a script at startup, with no LLM
|
|
2621
|
-
* round-trip deciding whether to call the tool.
|
|
2622
|
-
*
|
|
2623
|
-
* Read here rather than at activation because that is the only place the real
|
|
2624
|
-
* value exists: the host activates extensions first and applies collected CLI
|
|
2625
|
-
* flags second, so `getFlag` during activation returns the registered default
|
|
2626
|
-
* and nothing else. `examples/extensions/ssh.ts` reads its flag from
|
|
2627
|
-
* session_start for exactly this reason.
|
|
2628
|
-
*/
|
|
2629
|
-
let workflowFlagHandled = false;
|
|
2630
|
-
function runWorkflowFlag(ctx: ExtensionContext): void {
|
|
2631
|
-
if (workflowFlagHandled) return;
|
|
2632
|
-
const flag = typeof pi.getFlag === "function" ? pi.getFlag(WORKFLOW_FILE_FLAG) : undefined;
|
|
2633
|
-
if (flag === undefined || flag === false) return;
|
|
2634
|
-
workflowFlagHandled = true;
|
|
2635
|
-
|
|
2636
|
-
const report = (message: string, level: "info" | "warning") => {
|
|
2637
|
-
if (ctx.hasUI) ctx.ui.notify(message, level);
|
|
2638
|
-
else console.warn(`[pi-subagents] ${message}`);
|
|
2639
|
-
};
|
|
2640
|
-
|
|
2641
|
-
// The flag is the same machinery by another door, so the master switch has
|
|
2642
|
-
// to close it too — silently ignoring a flag the user typed would be worse
|
|
2643
|
-
// than saying why nothing ran.
|
|
2644
|
-
if (!isWorkflowsEnabled()) {
|
|
2645
|
-
report(
|
|
2646
|
-
`--${WORKFLOW_FILE_FLAG} ignored: workflows are off. Turn them on in /agents → Settings → Workflows, ` +
|
|
2647
|
-
'or set `"workflowsEnabled": true` in .pi/subagents.json.',
|
|
2648
|
-
"warning",
|
|
2649
|
-
);
|
|
2650
|
-
return;
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
|
-
// A bare `--subagents-workflow-file` parses to boolean `true`. Say what was
|
|
2654
|
-
// missing rather than reading a file called "true".
|
|
2655
|
-
if (typeof flag !== "string" || flag.trim() === "") {
|
|
2656
|
-
report(`--${WORKFLOW_FILE_FLAG} needs a path: --${WORKFLOW_FILE_FLAG}=<path>`, "warning");
|
|
2657
|
-
return;
|
|
2658
|
-
}
|
|
2659
|
-
|
|
2660
|
-
const path = isAbsolute(flag.trim()) ? flag.trim() : join(ctx.cwd, flag.trim());
|
|
2661
|
-
let script: string;
|
|
2662
|
-
try {
|
|
2663
|
-
script = readFileSync(path, "utf-8");
|
|
2664
|
-
} catch (err) {
|
|
2665
|
-
report(`Could not read ${path}: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
2666
|
-
return;
|
|
2667
|
-
}
|
|
2668
|
-
|
|
2669
|
-
let meta: WorkflowMeta | undefined;
|
|
2670
|
-
try {
|
|
2671
|
-
meta = extractMeta(script).meta;
|
|
2672
|
-
} catch (err) {
|
|
2673
|
-
report(err instanceof Error ? err.message : String(err), "warning");
|
|
2674
|
-
return;
|
|
2675
|
-
}
|
|
2676
|
-
|
|
2677
|
-
const task = createWorkflowTask({ id: workflowRunId(), script, scriptPath: path, meta });
|
|
2678
|
-
workflowTasks.set(task.id, task);
|
|
2679
|
-
widget.update();
|
|
2680
|
-
report(`Running workflow ${meta.name}…`, "info");
|
|
2681
|
-
|
|
2682
|
-
// Detached: session_start is awaited by the host, and a workflow can run for
|
|
2683
|
-
// minutes — blocking here would hold the whole session's startup.
|
|
2684
|
-
void runWorkflowTask(ctx, task).then(() => {
|
|
2685
|
-
// No tool call to attach a result card to, so the card becomes a session
|
|
2686
|
-
// entry (same layout), and the outcome is handed to the model as context
|
|
2687
|
-
// for its next turn rather than forcing one.
|
|
2688
|
-
pi.appendEntry<WorkflowEntryData>(WORKFLOW_ENTRY_TYPE, workflowEntryData(task));
|
|
2689
|
-
pi.sendMessage({
|
|
2690
|
-
customType: "workflow-result",
|
|
2691
|
-
content: formatWorkflowNotification(task),
|
|
2692
|
-
display: false,
|
|
2693
|
-
}, { deliverAs: "nextTurn" });
|
|
2694
|
-
widget.update();
|
|
2695
|
-
});
|
|
2696
|
-
}
|
|
1495
|
+
}));
|
|
2697
1496
|
|
|
2698
1497
|
// ---- get_subagent_result tool ----
|
|
2699
1498
|
|
|
2700
|
-
|
|
1499
|
+
pi.registerTool(defineTool({
|
|
2701
1500
|
name: SUBAGENT_TOOL_NAMES.GET_RESULT,
|
|
2702
1501
|
label: "Get Agent Result",
|
|
2703
1502
|
description:
|
|
2704
|
-
"Check status and retrieve a background agent
|
|
1503
|
+
"Check status and retrieve results from a background agent. Use the agent ID returned by Agent with run_in_background.",
|
|
2705
1504
|
promptSnippet: "Check status and retrieve results from a background agent",
|
|
2706
1505
|
parameters: Type.Object({
|
|
2707
1506
|
agent_id: Type.String({
|
|
2708
|
-
description: "The agent ID to check.
|
|
1507
|
+
description: "The agent ID to check.",
|
|
2709
1508
|
}),
|
|
2710
1509
|
wait: Type.Optional(
|
|
2711
1510
|
Type.Boolean({
|
|
@@ -2719,8 +1518,8 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2719
1518
|
),
|
|
2720
1519
|
}),
|
|
2721
1520
|
execute: async (_toolCallId, params, signal, _onUpdate, _ctx) => {
|
|
2722
|
-
const record =
|
|
2723
|
-
if (!record
|
|
1521
|
+
const record = manager.getRecord(params.agent_id);
|
|
1522
|
+
if (!record) {
|
|
2724
1523
|
return textResult(`Agent not found: "${params.agent_id}". It may have been cleaned up.`);
|
|
2725
1524
|
}
|
|
2726
1525
|
|
|
@@ -2739,16 +1538,15 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2739
1538
|
if (record.promise) await abortable(record.promise, signal);
|
|
2740
1539
|
}
|
|
2741
1540
|
|
|
1541
|
+
const durableResult = !record.result?.trim() && record.transcriptPath && currentCtx?.cwd
|
|
1542
|
+
? readAgentHistoryResult(currentCtx.cwd, record.transcriptPath)
|
|
1543
|
+
: undefined;
|
|
2742
1544
|
const displayName = getDisplayName(record.type);
|
|
2743
1545
|
const duration = formatDuration(record.startedAt, record.completedAt);
|
|
2744
1546
|
const tokens = formatLifetimeTokens(record);
|
|
2745
1547
|
const contextPercent = getSessionContextPercent(record.session);
|
|
2746
1548
|
const statsParts = [`Tool uses: ${record.toolUses}`];
|
|
2747
1549
|
if (tokens) statsParts.push(tokens);
|
|
2748
|
-
if (showCost) {
|
|
2749
|
-
const costText = formatCost(getLifetimeCost(record.lifetimeUsage));
|
|
2750
|
-
if (costText) statsParts.push(`Cost: ${costText}`);
|
|
2751
|
-
}
|
|
2752
1550
|
if (contextPercent !== null) statsParts.push(`Context: ${Math.round(contextPercent)}%`);
|
|
2753
1551
|
if (record.compactionCount) statsParts.push(`Compactions: ${record.compactionCount}`);
|
|
2754
1552
|
statsParts.push(`Duration: ${duration}`);
|
|
@@ -2761,9 +1559,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2761
1559
|
if (record.status === "running") {
|
|
2762
1560
|
output += "Agent is still running. Use wait: true or check back later.";
|
|
2763
1561
|
} else if (record.status === "error") {
|
|
2764
|
-
output += `Error: ${record.error}${partialOutputSuffix(record)}`;
|
|
1562
|
+
output += `Error: ${record.error}${partialOutputSuffix(record, durableResult)}`;
|
|
2765
1563
|
} else {
|
|
2766
|
-
output += record.result?.trim() || "No output.";
|
|
1564
|
+
output += durableResult || record.result?.trim() || "No output.";
|
|
2767
1565
|
}
|
|
2768
1566
|
|
|
2769
1567
|
// Mark result as consumed — suppresses the completion notification
|
|
@@ -2786,7 +1584,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2786
1584
|
|
|
2787
1585
|
// ---- steer_subagent tool ----
|
|
2788
1586
|
|
|
2789
|
-
|
|
1587
|
+
pi.registerTool(defineTool({
|
|
2790
1588
|
name: SUBAGENT_TOOL_NAMES.STEER,
|
|
2791
1589
|
label: "Steer Agent",
|
|
2792
1590
|
description:
|
|
@@ -2795,15 +1593,15 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2795
1593
|
promptSnippet: "Send a steering message to redirect a running background agent",
|
|
2796
1594
|
parameters: Type.Object({
|
|
2797
1595
|
agent_id: Type.String({
|
|
2798
|
-
description: "The agent ID to steer (must be currently running).
|
|
1596
|
+
description: "The agent ID to steer (must be currently running).",
|
|
2799
1597
|
}),
|
|
2800
1598
|
message: Type.String({
|
|
2801
1599
|
description: "The steering message to send. This will appear as a user message in the agent's conversation.",
|
|
2802
1600
|
}),
|
|
2803
1601
|
}),
|
|
2804
1602
|
execute: async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
|
|
2805
|
-
const record =
|
|
2806
|
-
if (!record
|
|
1603
|
+
const record = manager.getRecord(params.agent_id);
|
|
1604
|
+
if (!record) {
|
|
2807
1605
|
return textResult(`Agent not found: "${params.agent_id}". It may have been cleaned up.`);
|
|
2808
1606
|
}
|
|
2809
1607
|
if (record.status !== "running") {
|
|
@@ -2824,10 +1622,6 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2824
1622
|
const contextPercent = getSessionContextPercent(record.session);
|
|
2825
1623
|
const stateParts: string[] = [];
|
|
2826
1624
|
if (tokens) stateParts.push(tokens);
|
|
2827
|
-
if (showCost) {
|
|
2828
|
-
const costText = formatCost(getLifetimeCost(record.lifetimeUsage));
|
|
2829
|
-
if (costText) stateParts.push(costText);
|
|
2830
|
-
}
|
|
2831
1625
|
stateParts.push(`${record.toolUses} tool ${record.toolUses === 1 ? "use" : "uses"}`);
|
|
2832
1626
|
if (contextPercent !== null) stateParts.push(`context ${Math.round(contextPercent)}% full`);
|
|
2833
1627
|
if (record.compactionCount) stateParts.push(`${record.compactionCount} compaction${record.compactionCount === 1 ? "" : "s"}`);
|
|
@@ -2843,9 +1637,20 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2843
1637
|
|
|
2844
1638
|
// ---- /agents interactive menu ----
|
|
2845
1639
|
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
1640
|
+
const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");
|
|
1641
|
+
const workspaceAgentsDir = () => join(process.cwd(), ".agents", "agents");
|
|
1642
|
+
const personalAgentsDir = () => join(getAgentDir(), "agents");
|
|
1643
|
+
|
|
1644
|
+
/** Find the file path of a custom agent by name, in discovery-precedence order (project, workspace, then global). */
|
|
1645
|
+
function findAgentFile(name: string): { path: string; location: "project" | "workspace" | "personal" } | undefined {
|
|
1646
|
+
const projectPath = join(projectAgentsDir(), `${name}.md`);
|
|
1647
|
+
if (existsSync(projectPath)) return { path: projectPath, location: "project" };
|
|
1648
|
+
const workspacePath = join(workspaceAgentsDir(), `${name}.md`);
|
|
1649
|
+
if (existsSync(workspacePath)) return { path: workspacePath, location: "workspace" };
|
|
1650
|
+
const personalPath = join(personalAgentsDir(), `${name}.md`);
|
|
1651
|
+
if (existsSync(personalPath)) return { path: personalPath, location: "personal" };
|
|
1652
|
+
return undefined;
|
|
1653
|
+
}
|
|
2849
1654
|
|
|
2850
1655
|
function getModelLabel(type: string, registry?: ModelRegistry): string {
|
|
2851
1656
|
const cfg = getAgentConfig(type);
|
|
@@ -2872,15 +1677,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2872
1677
|
// Build select options
|
|
2873
1678
|
const options: string[] = [];
|
|
2874
1679
|
|
|
2875
|
-
// Keep active
|
|
2876
|
-
const
|
|
2877
|
-
const { active, history } = splitAgentRecords(
|
|
2878
|
-
|
|
2879
|
-
const running = active.filter(a => a.status === "running").length;
|
|
2880
|
-
const queued = active.filter(a => a.status === "queued").length;
|
|
2881
|
-
options.push(`Running agents (${active.length}) — ${running} running, ${queued} queued`);
|
|
2882
|
-
}
|
|
2883
|
-
if (history.length > 0) options.push(`Agent history (${history.length})`);
|
|
1680
|
+
// Keep active agents and terminal history in separate menu entries.
|
|
1681
|
+
const records = manager.listAgents();
|
|
1682
|
+
const { active, history } = splitAgentRecords(records, ctx.cwd);
|
|
1683
|
+
options.push(...buildAgentStatusMenuEntries(records, ctx.cwd));
|
|
2884
1684
|
|
|
2885
1685
|
// Agent types list
|
|
2886
1686
|
if (allNames.length > 0) {
|
|
@@ -2893,17 +1693,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2893
1693
|
options.push(`Scheduled jobs (${jobCount})`);
|
|
2894
1694
|
}
|
|
2895
1695
|
|
|
2896
|
-
// Workflow runs, on the same terms as scheduled jobs: shown only when the
|
|
2897
|
-
// feature is on, so the menu never advertises something switched off.
|
|
2898
|
-
if (isWorkflowsEnabled()) {
|
|
2899
|
-
options.push(`Workflows (${workflowTasks.size})`);
|
|
2900
|
-
}
|
|
2901
|
-
|
|
2902
1696
|
// Actions
|
|
2903
1697
|
options.push("Create new agent");
|
|
2904
1698
|
options.push("Settings");
|
|
2905
1699
|
|
|
2906
|
-
const noAgentsMsg = allNames.length === 0 &&
|
|
1700
|
+
const noAgentsMsg = allNames.length === 0 && active.length === 0 && history.length === 0
|
|
2907
1701
|
? "No agents found. Create specialized subagents that can be delegated to.\n\n" +
|
|
2908
1702
|
"Each subagent has its own context window, custom system prompt, and specific tools.\n\n" +
|
|
2909
1703
|
"Try creating: Code Reviewer, Security Auditor, Test Writer, or Documentation Writer.\n\n"
|
|
@@ -2928,9 +1722,6 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2928
1722
|
} else if (choice.startsWith("Scheduled jobs (")) {
|
|
2929
1723
|
await showSchedulesMenu(ctx, scheduler);
|
|
2930
1724
|
await showAgentsMenu(ctx);
|
|
2931
|
-
} else if (choice.startsWith("Workflows (")) {
|
|
2932
|
-
await showWorkflowsMenu(ctx, workflowMenuDeps);
|
|
2933
|
-
await showAgentsMenu(ctx);
|
|
2934
1725
|
} else if (choice === "Create new agent") {
|
|
2935
1726
|
await showCreateWizard(ctx);
|
|
2936
1727
|
} else if (choice === "Settings") {
|
|
@@ -3007,22 +1798,24 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3007
1798
|
}
|
|
3008
1799
|
}
|
|
3009
1800
|
|
|
3010
|
-
function makeUniqueAgentOptionLabels(pairs: Array<{ record: AgentRecord; label: string }>):
|
|
1801
|
+
function makeUniqueAgentOptionLabels(pairs: Array<{ record: AgentRecord; label: string }>): string[] {
|
|
3011
1802
|
const counts = new Map<string, number>();
|
|
3012
1803
|
for (const pair of pairs) counts.set(pair.label, (counts.get(pair.label) ?? 0) + 1);
|
|
3013
1804
|
const used = new Set<string>();
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
1805
|
+
return pairs.map((pair) => {
|
|
1806
|
+
const { record, label } = pair;
|
|
1807
|
+
if ((counts.get(label) ?? 0) === 1) {
|
|
1808
|
+
used.add(label);
|
|
1809
|
+
return label;
|
|
3018
1810
|
}
|
|
3019
|
-
const suffix = ` · #${
|
|
3020
|
-
let candidate = `${
|
|
1811
|
+
const suffix = ` · #${record.id.slice(-8)}`;
|
|
1812
|
+
let candidate = `${label}${suffix}`;
|
|
3021
1813
|
let n = 2;
|
|
3022
|
-
while (used.has(candidate)) candidate = `${
|
|
3023
|
-
pair.label = candidate;
|
|
1814
|
+
while (used.has(candidate)) candidate = `${label}${suffix}-${n++}`;
|
|
3024
1815
|
used.add(candidate);
|
|
3025
|
-
|
|
1816
|
+
pair.label = candidate;
|
|
1817
|
+
return candidate;
|
|
1818
|
+
});
|
|
3026
1819
|
}
|
|
3027
1820
|
|
|
3028
1821
|
async function selectAgentFromReadOnlyList(
|
|
@@ -3032,7 +1825,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3032
1825
|
selection: AgentMenuSelection,
|
|
3033
1826
|
): Promise<AgentRecord | undefined> {
|
|
3034
1827
|
const options = pairs.map(({ record, label }) => ({ value: record.id, label }));
|
|
3035
|
-
const rememberedIndex = selection.id
|
|
1828
|
+
const rememberedIndex = selection.id
|
|
1829
|
+
? pairs.findIndex(({ record }) => record.id === selection.id)
|
|
1830
|
+
: -1;
|
|
3036
1831
|
const initialIndex = rememberedIndex >= 0
|
|
3037
1832
|
? rememberedIndex
|
|
3038
1833
|
: Math.max(0, Math.min(selection.index, pairs.length - 1));
|
|
@@ -3046,7 +1841,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3046
1841
|
};
|
|
3047
1842
|
|
|
3048
1843
|
const choice = await ctx.ui.custom<string | undefined>((_tui, _theme, _kb, done) => {
|
|
3049
|
-
const list = new SelectList(
|
|
1844
|
+
const list = new SelectList(
|
|
1845
|
+
options,
|
|
1846
|
+
Math.min(options.length, 10),
|
|
1847
|
+
getSelectListTheme(),
|
|
1848
|
+
);
|
|
3050
1849
|
list.setSelectedIndex(initialIndex);
|
|
3051
1850
|
const initialItem = options[initialIndex];
|
|
3052
1851
|
if (initialItem) remember(initialItem.value);
|
|
@@ -3062,11 +1861,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3062
1861
|
container.addChild(new Spacer(1));
|
|
3063
1862
|
container.addChild(list);
|
|
3064
1863
|
return {
|
|
3065
|
-
render: (
|
|
1864
|
+
render: (w: number) => container.render(w),
|
|
3066
1865
|
invalidate: () => container.invalidate(),
|
|
3067
|
-
handleInput: (data: string) =>
|
|
3068
|
-
if (!isKeyRelease(data)) list.handleInput(data);
|
|
3069
|
-
},
|
|
1866
|
+
handleInput: (data: string) => list.handleInput(data),
|
|
3070
1867
|
};
|
|
3071
1868
|
});
|
|
3072
1869
|
|
|
@@ -3075,64 +1872,90 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3075
1872
|
}
|
|
3076
1873
|
|
|
3077
1874
|
async function showRunningAgents(ctx: ExtensionCommandContext) {
|
|
3078
|
-
const agents = manager.listAgents().
|
|
3079
|
-
isTopLevelAgent(record) && (record.status === "running" || record.status === "queued"),
|
|
3080
|
-
);
|
|
1875
|
+
const { active: agents } = splitAgentRecords(manager.listAgents(), ctx.cwd);
|
|
3081
1876
|
if (agents.length === 0) {
|
|
3082
1877
|
ctx.ui.notify("No agents.", "info");
|
|
3083
1878
|
return;
|
|
3084
1879
|
}
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
1880
|
+
|
|
1881
|
+
const pairs = agents.map((record) => {
|
|
1882
|
+
const dn = getDisplayName(record.type);
|
|
1883
|
+
const dur = formatDuration(record.startedAt, record.completedAt);
|
|
1884
|
+
return { record, label: `${dn} (${record.description}) · ${record.toolUses} tools · ${record.status} · ${dur}` };
|
|
1885
|
+
});
|
|
3089
1886
|
makeUniqueAgentOptionLabels(pairs);
|
|
1887
|
+
|
|
3090
1888
|
const record = await selectAgentFromReadOnlyList(ctx, "Running agents", pairs, runningAgentSelection);
|
|
3091
1889
|
if (!record) return;
|
|
3092
|
-
|
|
1890
|
+
|
|
1891
|
+
await viewAgentConversation(ctx, record, "live");
|
|
1892
|
+
// Back-navigation: re-show the list at the previously selected agent.
|
|
3093
1893
|
await showRunningAgents(ctx);
|
|
3094
1894
|
}
|
|
3095
1895
|
|
|
3096
|
-
async function showAgentHistory(ctx: ExtensionCommandContext)
|
|
3097
|
-
const { history } = splitAgentRecords(manager.listAgents()
|
|
1896
|
+
async function showAgentHistory(ctx: ExtensionCommandContext) {
|
|
1897
|
+
const { history } = splitAgentRecords(manager.listAgents(), ctx.cwd);
|
|
3098
1898
|
if (history.length === 0) {
|
|
3099
1899
|
ctx.ui.notify("No agent history.", "info");
|
|
3100
1900
|
return;
|
|
3101
1901
|
}
|
|
3102
|
-
|
|
1902
|
+
|
|
1903
|
+
const pairs = history.map((record) => ({ record, label: formatAgentHistoryOption(record, Date.now()) }));
|
|
3103
1904
|
makeUniqueAgentOptionLabels(pairs);
|
|
3104
1905
|
const record = await selectAgentFromReadOnlyList(ctx, "Agent history", pairs, historyAgentSelection);
|
|
3105
1906
|
if (!record) return;
|
|
3106
|
-
|
|
1907
|
+
|
|
1908
|
+
await viewAgentConversation(ctx, record, "history");
|
|
1909
|
+
// Back-navigation: re-show the list at the previously selected agent.
|
|
3107
1910
|
await showAgentHistory(ctx);
|
|
3108
1911
|
}
|
|
3109
1912
|
|
|
3110
|
-
async function viewAgentConversation(
|
|
1913
|
+
async function viewAgentConversation(
|
|
1914
|
+
ctx: ExtensionCommandContext,
|
|
1915
|
+
record: AgentRecord,
|
|
1916
|
+
mode: "live" | "history",
|
|
1917
|
+
) {
|
|
1918
|
+
if (mode === "live" && !canOpenActiveAgent(record)) {
|
|
1919
|
+
ctx.ui.notify(`Agent is ${record.status === "queued" ? "queued" : "expired"} — no history available.`, "info");
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
if (mode === "history" && !canOpenAgentHistory(record, ctx.cwd)) {
|
|
1923
|
+
ctx.ui.notify("No agent history.", "info");
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
3111
1927
|
const { ConversationViewer, VIEWPORT_HEIGHT_PCT, createStaticConversationSource } = await import("./ui/conversation-viewer.js");
|
|
3112
|
-
const
|
|
3113
|
-
|
|
1928
|
+
const session = mode === "live"
|
|
1929
|
+
? record.session
|
|
1930
|
+
: (() => {
|
|
1931
|
+
const messages = record.transcriptPath
|
|
1932
|
+
? readAgentHistory(ctx.cwd, record.transcriptPath)
|
|
1933
|
+
: undefined;
|
|
1934
|
+
return messages
|
|
1935
|
+
? createStaticConversationSource(messages)
|
|
1936
|
+
: record.session
|
|
1937
|
+
? createStaticConversationSource(record.session.messages)
|
|
1938
|
+
: undefined;
|
|
1939
|
+
})();
|
|
3114
1940
|
if (!session) {
|
|
3115
|
-
ctx.ui.notify(
|
|
1941
|
+
ctx.ui.notify("No agent history.", "info");
|
|
3116
1942
|
return;
|
|
3117
1943
|
}
|
|
3118
|
-
const isHistory = record.session === undefined;
|
|
3119
|
-
const activity = agentActivity.get(record.id);
|
|
3120
1944
|
|
|
1945
|
+
const activity = agentActivity.get(record.id);
|
|
1946
|
+
const isLive = mode === "live";
|
|
3121
1947
|
await ctx.ui.custom<undefined>(
|
|
3122
|
-
(tui, theme, keybindings, done) =>
|
|
3123
|
-
tui,
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
isHistory ? undefined : (message: string) => manager.steer(record.id, message),
|
|
3134
|
-
{ pi, ctx, readOnly: isHistory },
|
|
3135
|
-
),
|
|
1948
|
+
(tui, theme, keybindings, done) => {
|
|
1949
|
+
return new ConversationViewer(tui, session, record, activity, theme, done,
|
|
1950
|
+
isLive ? () => {
|
|
1951
|
+
if (manager.abort(record.id)) {
|
|
1952
|
+
ctx.ui.notify(`Stopped "${record.description}".`, "info");
|
|
1953
|
+
}
|
|
1954
|
+
} : undefined,
|
|
1955
|
+
keybindings,
|
|
1956
|
+
isLive ? (message: string) => manager.steer(record.id, message) : undefined,
|
|
1957
|
+
mode === "history" ? { pi, ctx, readOnly: true } : { pi, ctx });
|
|
1958
|
+
},
|
|
3136
1959
|
{
|
|
3137
1960
|
overlay: true,
|
|
3138
1961
|
overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
|
|
@@ -3147,7 +1970,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3147
1970
|
return;
|
|
3148
1971
|
}
|
|
3149
1972
|
|
|
3150
|
-
const file =
|
|
1973
|
+
const file = findAgentFile(name);
|
|
3151
1974
|
const isDefault = cfg.isDefault === true;
|
|
3152
1975
|
const disabled = cfg.enabled === false;
|
|
3153
1976
|
|
|
@@ -3222,7 +2045,29 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3222
2045
|
if (!overwrite) return;
|
|
3223
2046
|
}
|
|
3224
2047
|
|
|
3225
|
-
|
|
2048
|
+
// Build the .md file content
|
|
2049
|
+
const fmFields: string[] = [];
|
|
2050
|
+
fmFields.push(`description: ${JSON.stringify(cfg.description)}`);
|
|
2051
|
+
if (cfg.displayName) fmFields.push(`display_name: ${cfg.displayName}`);
|
|
2052
|
+
fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ") || "all"}`);
|
|
2053
|
+
if (cfg.model) fmFields.push(`model: ${cfg.model}`);
|
|
2054
|
+
if (cfg.thinking) fmFields.push(`thinking: ${cfg.thinking}`);
|
|
2055
|
+
if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`);
|
|
2056
|
+
fmFields.push(`prompt_mode: ${cfg.promptMode}`);
|
|
2057
|
+
if (cfg.extensions === false) fmFields.push("extensions: false");
|
|
2058
|
+
else if (Array.isArray(cfg.extensions)) fmFields.push(`extensions: ${cfg.extensions.join(", ")}`);
|
|
2059
|
+
if (cfg.excludeExtensions?.length) fmFields.push(`exclude_extensions: ${cfg.excludeExtensions.join(", ")}`);
|
|
2060
|
+
if (cfg.skills === false) fmFields.push("skills: false");
|
|
2061
|
+
else if (Array.isArray(cfg.skills)) fmFields.push(`skills: ${cfg.skills.join(", ")}`);
|
|
2062
|
+
if (cfg.disallowedTools?.length) fmFields.push(`disallowed_tools: ${cfg.disallowedTools.join(", ")}`);
|
|
2063
|
+
if (cfg.inheritContext) fmFields.push("inherit_context: true");
|
|
2064
|
+
if (cfg.runInBackground) fmFields.push("run_in_background: true");
|
|
2065
|
+
if (cfg.outputTranscript === false) fmFields.push("output_transcript: false");
|
|
2066
|
+
if (cfg.isolated) fmFields.push("isolated: true");
|
|
2067
|
+
if (cfg.memory) fmFields.push(`memory: ${cfg.memory}`);
|
|
2068
|
+
if (cfg.isolation) fmFields.push(`isolation: ${cfg.isolation}`);
|
|
2069
|
+
|
|
2070
|
+
const content = `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
|
|
3226
2071
|
|
|
3227
2072
|
const { writeFileSync } = await import("node:fs");
|
|
3228
2073
|
writeFileSync(targetPath, content, "utf-8");
|
|
@@ -3232,21 +2077,15 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3232
2077
|
|
|
3233
2078
|
/** Disable an agent: set enabled: false in its .md file, or create a stub for built-in defaults. */
|
|
3234
2079
|
async function disableAgent(ctx: ExtensionCommandContext, name: string) {
|
|
3235
|
-
const file =
|
|
2080
|
+
const file = findAgentFile(name);
|
|
3236
2081
|
if (file) {
|
|
3237
2082
|
// Existing file — set enabled: false in frontmatter (idempotent)
|
|
3238
2083
|
const content = readFileSync(file.path, "utf-8");
|
|
3239
|
-
|
|
3240
|
-
if (outcome === "already-disabled") {
|
|
2084
|
+
if (content.includes("\nenabled: false\n")) {
|
|
3241
2085
|
ctx.ui.notify(`${name} is already disabled.`, "info");
|
|
3242
2086
|
return;
|
|
3243
2087
|
}
|
|
3244
|
-
|
|
3245
|
-
// Nothing to edit — say so rather than rewriting the file unchanged and
|
|
3246
|
-
// reporting success for a change that never happened.
|
|
3247
|
-
ctx.ui.notify(`Cannot disable ${name}: ${file.path} has no frontmatter block.`, "error");
|
|
3248
|
-
return;
|
|
3249
|
-
}
|
|
2088
|
+
const updated = content.replace(/^---\n/, "---\nenabled: false\n");
|
|
3250
2089
|
const { writeFileSync } = await import("node:fs");
|
|
3251
2090
|
writeFileSync(file.path, updated, "utf-8");
|
|
3252
2091
|
reloadCustomAgents();
|
|
@@ -3273,21 +2112,15 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3273
2112
|
|
|
3274
2113
|
/** Enable a disabled agent by removing enabled: false from its frontmatter. */
|
|
3275
2114
|
async function enableAgent(ctx: ExtensionCommandContext, name: string) {
|
|
3276
|
-
const file =
|
|
2115
|
+
const file = findAgentFile(name);
|
|
3277
2116
|
if (!file) return;
|
|
3278
2117
|
|
|
3279
2118
|
const content = readFileSync(file.path, "utf-8");
|
|
3280
|
-
const
|
|
3281
|
-
if (!changed && !isEmptyStub(updated)) {
|
|
3282
|
-
// The file carries no `enabled: false` to remove, so it was never disabled
|
|
3283
|
-
// by us — reporting success here would hide a no-op.
|
|
3284
|
-
ctx.ui.notify(`${name} is not disabled in ${file.path}.`, "info");
|
|
3285
|
-
return;
|
|
3286
|
-
}
|
|
2119
|
+
const updated = content.replace(/^(---\n)enabled: false\n/, "$1");
|
|
3287
2120
|
const { writeFileSync } = await import("node:fs");
|
|
3288
2121
|
|
|
3289
2122
|
// If the file was just a stub ("---\n---\n"), delete it to restore the built-in default
|
|
3290
|
-
if (
|
|
2123
|
+
if (updated.trim() === "---\n---" || updated.trim() === "---\n---\n") {
|
|
3291
2124
|
unlinkSync(file.path);
|
|
3292
2125
|
reloadCustomAgents();
|
|
3293
2126
|
ctx.ui.notify(`Enabled ${name} (removed ${file.path})`, "info");
|
|
@@ -3346,7 +2179,6 @@ The file format is a markdown file with YAML frontmatter and a system prompt bod
|
|
|
3346
2179
|
\`\`\`markdown
|
|
3347
2180
|
---
|
|
3348
2181
|
description: <one-line description shown in UI>
|
|
3349
|
-
color: <optional agent name badge color: red, blue, green, yellow, purple, orange, pink, cyan, an Agency Agents alias, or quoted "#RRGGBB">
|
|
3350
2182
|
tools: <comma-separated built-in tools: read, bash, edit, write, grep, find, ls. Use "none" for no tools. Omit for all tools>
|
|
3351
2183
|
model: <optional model as "provider/modelId", e.g. "anthropic/claude-haiku-4-5". Omit to inherit parent model>
|
|
3352
2184
|
thinking: <optional thinking level: ${THINKING_LEVELS.join(", ")}. Omit to inherit>
|
|
@@ -3356,18 +2188,11 @@ extensions: <true (inherit all MCP/extension tools), false (none), or comma-sepa
|
|
|
3356
2188
|
skills: <true (inherit all), false (none), or comma-separated skill names to preload into prompt. Default: true>
|
|
3357
2189
|
disallowed_tools: <comma-separated tool names to block, even if otherwise available. Omit for none>
|
|
3358
2190
|
inherit_context: <true to fork parent conversation into agent so it sees chat history. Default: false>
|
|
3359
|
-
run_in_background: <
|
|
2191
|
+
run_in_background: <true to run in background by default. Default: false>
|
|
3360
2192
|
output_transcript: <false to write no transcript file or path for this agent. Independent of persist_session. Default: true>
|
|
3361
2193
|
isolated: <true for no extension/MCP tools, only built-in tools. Default: false>
|
|
3362
|
-
memory: <"user" (global), "project" (per-project), or "local" (gitignored per-project) for persistent memory. Omit for none
|
|
3363
|
-
|
|
3364
|
-
// request that is refused at spawn time into a file that outlives the
|
|
3365
|
-
// session — the #231 pathology (models fill the fields they are shown)
|
|
3366
|
-
// one layer up. Built per invocation, so this read is live.
|
|
3367
|
-
isWorktreeIsolationEnabled()
|
|
3368
|
-
? `\nisolation: <"worktree" to run in isolated git worktree; "off" to refuse one even when the caller asks. Omit for normal>`
|
|
3369
|
-
: ""
|
|
3370
|
-
}
|
|
2194
|
+
memory: <"user" (global), "project" (per-project), or "local" (gitignored per-project) for persistent memory. Omit for none>
|
|
2195
|
+
isolation: <"worktree" to run in isolated git worktree. Omit for normal>
|
|
3371
2196
|
---
|
|
3372
2197
|
|
|
3373
2198
|
<system prompt body — instructions for the agent>
|
|
@@ -3388,12 +2213,6 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3388
2213
|
const { record } = await manager.spawnAndWait(pi, ctx, "general-purpose", generatePrompt, {
|
|
3389
2214
|
description: `Generate ${name} agent`,
|
|
3390
2215
|
maxTurns: 5,
|
|
3391
|
-
// Exempt from maxConcurrentForeground. This runs from a modal wizard, not
|
|
3392
|
-
// a tool call: it passes no signal, and Esc in `ctx.ui` never reaches the
|
|
3393
|
-
// manager — so a user waiting behind a full pool would have no way to
|
|
3394
|
-
// cancel at all. It is also one human action that cannot fan out, which
|
|
3395
|
-
// is what the limit exists to bound. It still counts once started.
|
|
3396
|
-
bypassQueue: true,
|
|
3397
2216
|
});
|
|
3398
2217
|
|
|
3399
2218
|
if (record.status === "error") {
|
|
@@ -3446,12 +2265,13 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3446
2265
|
]);
|
|
3447
2266
|
if (!modelChoice) return;
|
|
3448
2267
|
|
|
3449
|
-
let
|
|
3450
|
-
if (modelChoice === "haiku")
|
|
3451
|
-
else if (modelChoice === "sonnet")
|
|
3452
|
-
else if (modelChoice === "opus")
|
|
2268
|
+
let modelLine = "";
|
|
2269
|
+
if (modelChoice === "haiku") modelLine = "\nmodel: anthropic/claude-haiku-4-5";
|
|
2270
|
+
else if (modelChoice === "sonnet") modelLine = "\nmodel: anthropic/claude-sonnet-4-6";
|
|
2271
|
+
else if (modelChoice === "opus") modelLine = "\nmodel: anthropic/claude-opus-4-6";
|
|
3453
2272
|
else if (modelChoice === "custom...") {
|
|
3454
|
-
|
|
2273
|
+
const customModel = await ctx.ui.input("Model (provider/modelId)");
|
|
2274
|
+
if (customModel) modelLine = `\nmodel: ${customModel}`;
|
|
3455
2275
|
}
|
|
3456
2276
|
|
|
3457
2277
|
// 5. Thinking
|
|
@@ -3459,17 +2279,22 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3459
2279
|
const thinkingChoice = await ctx.ui.select("Thinking level", ["inherit", ...THINKING_LEVELS]);
|
|
3460
2280
|
if (!thinkingChoice) return;
|
|
3461
2281
|
|
|
2282
|
+
let thinkingLine = "";
|
|
2283
|
+
if (thinkingChoice !== "inherit") thinkingLine = `\nthinking: ${thinkingChoice}`;
|
|
2284
|
+
|
|
3462
2285
|
// 6. System prompt
|
|
3463
2286
|
const systemPrompt = await ctx.ui.editor("System prompt", "");
|
|
3464
2287
|
if (systemPrompt === undefined) return;
|
|
3465
2288
|
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
2289
|
+
// Build the file
|
|
2290
|
+
const content = `---
|
|
2291
|
+
description: ${description}
|
|
2292
|
+
tools: ${tools}${modelLine}${thinkingLine}
|
|
2293
|
+
prompt_mode: replace
|
|
2294
|
+
---
|
|
2295
|
+
|
|
2296
|
+
${systemPrompt}
|
|
2297
|
+
`;
|
|
3473
2298
|
|
|
3474
2299
|
mkdirSync(targetDir, { recursive: true });
|
|
3475
2300
|
const targetPath = join(targetDir, `${name}.md`);
|
|
@@ -3485,86 +2310,30 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3485
2310
|
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
3486
2311
|
}
|
|
3487
2312
|
|
|
3488
|
-
|
|
3489
|
-
* Every settings mutation writes this WHOLE object back to disk, so a field
|
|
3490
|
-
* missing here is erased from the user's subagents.json the next time they
|
|
3491
|
-
* toggle something unrelated. `SubagentsSettings` has every field optional,
|
|
3492
|
-
* so a `: SubagentsSettings` return annotation would let a newly-added setting
|
|
3493
|
-
* be forgotten here and still type-check. `satisfies` instead: it still checks
|
|
3494
|
-
* each value's type and rejects a mistyped key, but leaves the return type
|
|
3495
|
-
* inferred so `_NoMissingSettingsKeys` below can check completeness.
|
|
3496
|
-
*/
|
|
3497
|
-
function snapshotSettings() {
|
|
2313
|
+
function snapshotSettings(): SubagentsSettings {
|
|
3498
2314
|
return {
|
|
3499
2315
|
maxConcurrent: manager.getMaxConcurrent(),
|
|
3500
|
-
// 0 = unlimited, and the default — see SubagentsSettings.
|
|
3501
|
-
maxConcurrentForeground: manager.getMaxConcurrentForeground(),
|
|
3502
2316
|
// 0 = unlimited — per SubagentsSettings.defaultMaxTurns docstring and
|
|
3503
2317
|
// normalizeMaxTurns() in agent-runner.ts (which maps 0 → undefined).
|
|
3504
2318
|
defaultMaxTurns: getDefaultMaxTurns() ?? 0,
|
|
3505
2319
|
graceTurns: getGraceTurns(),
|
|
3506
2320
|
defaultJoinMode: getDefaultJoinMode(),
|
|
3507
|
-
backgroundByDefault: getBackgroundByDefault(),
|
|
3508
2321
|
schedulingEnabled: isSchedulingEnabled(),
|
|
3509
2322
|
scopeModels: isScopeModelsEnabled(),
|
|
3510
|
-
strictAgentFiles,
|
|
3511
2323
|
disableDefaultAgents: isDefaultsDisabled(),
|
|
3512
2324
|
toolDescriptionMode: getToolDescriptionMode(),
|
|
3513
|
-
agentMentions: getAgentMentionMode(),
|
|
3514
|
-
rememberAgents: getRememberAgents(),
|
|
3515
2325
|
widgetMode: getWidgetMode(),
|
|
3516
2326
|
outputTranscript: getOutputTranscriptDefault(),
|
|
3517
|
-
|
|
3518
|
-
// The user's answer, not the effective one. A stand-down for another
|
|
3519
|
-
// extension's workflow tool is scoped to the session it was detected in;
|
|
3520
|
-
// writing it here would let an unrelated settings change three menus away
|
|
3521
|
-
// freeze it into the file as an explicit `false`, which then survives
|
|
3522
|
-
// uninstalling the extension it was deferring to. undefined is dropped by
|
|
3523
|
-
// JSON.stringify, so unset stays unset — same reasoning as
|
|
3524
|
-
// `fallbackSubagent` below.
|
|
3525
|
-
workflowsEnabled: isWorkflowsPinned() ? isWorkflowsEnabled() : undefined,
|
|
3526
|
-
maxSubagentDepth: getMaxSubagentDepth(),
|
|
3527
|
-
// Deliberately NOT `?? "general-purpose"`: every settings change writes the
|
|
3528
|
-
// whole snapshot, and materializing the implicit default would turn it into
|
|
3529
|
-
// explicit configuration — which then fails loudly if general-purpose later
|
|
3530
|
-
// goes away. undefined is dropped by JSON.stringify.
|
|
3531
|
-
fallbackSubagent: getFallbackSubagent(),
|
|
3532
|
-
reportUsage: isReportUsageEnabled(),
|
|
3533
|
-
showCost: isShowCostEnabled(),
|
|
3534
|
-
showModel: isShowModelEnabled(),
|
|
3535
|
-
viewerMarkdown: getViewerMarkdown(),
|
|
3536
|
-
} satisfies SubagentsSettings;
|
|
2327
|
+
};
|
|
3537
2328
|
}
|
|
3538
2329
|
|
|
3539
|
-
|
|
3540
|
-
// to SubagentsSettings and not mirrored above, this Exclude is non-empty and
|
|
3541
|
-
// fails to satisfy `never` — turning a silent settings-erasure bug into a
|
|
3542
|
-
// typecheck error. `npm run typecheck` runs in CI.
|
|
3543
|
-
type _NoMissingSettingsKeys =
|
|
3544
|
-
Exclude<keyof SubagentsSettings, keyof ReturnType<typeof snapshotSettings>> extends never
|
|
3545
|
-
? true
|
|
3546
|
-
: ["snapshotSettings() is missing a SubagentsSettings key"];
|
|
3547
|
-
const _settingsSnapshotIsComplete: _NoMissingSettingsKeys = true;
|
|
3548
|
-
void _settingsSnapshotIsComplete;
|
|
3549
|
-
|
|
3550
|
-
const NUMERIC_IDS = new Set([
|
|
3551
|
-
"maxConcurrent", "maxConcurrentForeground", "defaultMaxTurns", "graceTurns", "maxSubagentDepth",
|
|
3552
|
-
]);
|
|
2330
|
+
const NUMERIC_IDS = new Set(["maxConcurrent", "defaultMaxTurns", "graceTurns"]);
|
|
3553
2331
|
|
|
3554
2332
|
async function showSettings(ctx: ExtensionCommandContext) {
|
|
3555
2333
|
function buildItems(): SettingItem[] {
|
|
3556
2334
|
const mc = manager.getMaxConcurrent();
|
|
3557
|
-
const mcf = manager.getMaxConcurrentForeground();
|
|
3558
2335
|
const dmt = getDefaultMaxTurns() ?? 0;
|
|
3559
2336
|
const gt = getGraceTurns();
|
|
3560
|
-
const msd = getMaxSubagentDepth();
|
|
3561
|
-
// Label what unset actually does — it targets general-purpose even when
|
|
3562
|
-
// that is unregistered (the permissive hardcoded tier), so showing "none"
|
|
3563
|
-
// there would advertise strict dispatch for the most permissive state.
|
|
3564
|
-
// `values` still offers only resolvable targets, so the user cannot
|
|
3565
|
-
// persist a fallback that would hard-error on every dispatch.
|
|
3566
|
-
const fallbackValue = getFallbackSubagent() ?? "general-purpose";
|
|
3567
|
-
const fallbackValues = [...new Set([...getAvailableTypes(), NO_FALLBACK])];
|
|
3568
2337
|
|
|
3569
2338
|
return [
|
|
3570
2339
|
{
|
|
@@ -3574,13 +2343,6 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3574
2343
|
currentValue: String(mc),
|
|
3575
2344
|
values: [String(mc)],
|
|
3576
2345
|
},
|
|
3577
|
-
{
|
|
3578
|
-
id: "maxConcurrentForeground",
|
|
3579
|
-
label: "Max foreground concurrency",
|
|
3580
|
-
description: "Max concurrent foreground (blocking) agents (0 = unlimited, Enter to type)",
|
|
3581
|
-
currentValue: String(mcf),
|
|
3582
|
-
values: [String(mcf)],
|
|
3583
|
-
},
|
|
3584
2346
|
{
|
|
3585
2347
|
id: "defaultMaxTurns",
|
|
3586
2348
|
label: "Default max turns",
|
|
@@ -3595,13 +2357,6 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3595
2357
|
currentValue: String(gt),
|
|
3596
2358
|
values: [String(gt)],
|
|
3597
2359
|
},
|
|
3598
|
-
{
|
|
3599
|
-
id: "maxSubagentDepth",
|
|
3600
|
-
label: "Nested depth",
|
|
3601
|
-
description: "Hard cap on nested delegation — main is 0, its subagents 1 (0/1 = nesting off, Enter to type)",
|
|
3602
|
-
currentValue: String(msd),
|
|
3603
|
-
values: [String(msd)],
|
|
3604
|
-
},
|
|
3605
2360
|
{
|
|
3606
2361
|
id: "joinMode",
|
|
3607
2362
|
label: "Join mode",
|
|
@@ -3609,13 +2364,6 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3609
2364
|
currentValue: getDefaultJoinMode(),
|
|
3610
2365
|
values: ["smart", "async", "group"],
|
|
3611
2366
|
},
|
|
3612
|
-
{
|
|
3613
|
-
id: "backgroundByDefault",
|
|
3614
|
-
label: "Background by default",
|
|
3615
|
-
description: "An Agent call that doesn't say runs detached (off = blocks the turn and returns inline)",
|
|
3616
|
-
currentValue: getBackgroundByDefault() ? "on" : "off",
|
|
3617
|
-
values: ["on", "off"],
|
|
3618
|
-
},
|
|
3619
2367
|
{
|
|
3620
2368
|
id: "schedulingEnabled",
|
|
3621
2369
|
label: "Scheduling",
|
|
@@ -3623,15 +2371,6 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3623
2371
|
currentValue: isSchedulingEnabled() ? "on" : "off",
|
|
3624
2372
|
values: ["on", "off"],
|
|
3625
2373
|
},
|
|
3626
|
-
{
|
|
3627
|
-
id: "workflowsEnabled",
|
|
3628
|
-
label: "Workflows",
|
|
3629
|
-
description:
|
|
3630
|
-
"Scripted workflows, on unless another extension provides a workflow tool "
|
|
3631
|
-
+ "(off keeps the SubagentWorkflow tool out of the tool spec; applies on next pi session)",
|
|
3632
|
-
currentValue: isWorkflowsEnabled() ? "on" : "off",
|
|
3633
|
-
values: ["on", "off"],
|
|
3634
|
-
},
|
|
3635
2374
|
{
|
|
3636
2375
|
id: "scopeModels",
|
|
3637
2376
|
label: "Scope models",
|
|
@@ -3639,13 +2378,6 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3639
2378
|
currentValue: isScopeModelsEnabled() ? "on" : "off",
|
|
3640
2379
|
values: ["on", "off"],
|
|
3641
2380
|
},
|
|
3642
|
-
{
|
|
3643
|
-
id: "strictAgentFiles",
|
|
3644
|
-
label: "Strict agent files",
|
|
3645
|
-
description: "Fail startup on an unreadable/unparseable agent .md instead of skipping it with a warning",
|
|
3646
|
-
currentValue: strictAgentFiles ? "on" : "off",
|
|
3647
|
-
values: ["on", "off"],
|
|
3648
|
-
},
|
|
3649
2381
|
{
|
|
3650
2382
|
id: "disableDefaultAgents",
|
|
3651
2383
|
label: "Disable defaults",
|
|
@@ -3653,13 +2385,6 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3653
2385
|
currentValue: isDefaultsDisabled() ? "on" : "off",
|
|
3654
2386
|
values: ["on", "off"],
|
|
3655
2387
|
},
|
|
3656
|
-
{
|
|
3657
|
-
id: "fallbackSubagent",
|
|
3658
|
-
label: "Fallback agent",
|
|
3659
|
-
description: `Agent used when subagent_type is unknown, disabled, or ambiguous; "${NO_FALLBACK}" rejects the call instead (strict dispatch)`,
|
|
3660
|
-
currentValue: fallbackValue,
|
|
3661
|
-
values: fallbackValues,
|
|
3662
|
-
},
|
|
3663
2388
|
{
|
|
3664
2389
|
id: "outputTranscript",
|
|
3665
2390
|
label: "Output transcript",
|
|
@@ -3667,60 +2392,6 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3667
2392
|
currentValue: getOutputTranscriptDefault() ? "on" : "off",
|
|
3668
2393
|
values: ["on", "off"],
|
|
3669
2394
|
},
|
|
3670
|
-
{
|
|
3671
|
-
id: "worktreeIsolation",
|
|
3672
|
-
label: "Worktree isolation",
|
|
3673
|
-
description:
|
|
3674
|
-
"Allow isolation: worktree to copy the repo. Off refuses worktrees on every path immediately — for repos where a copy costs too much time or disk — and drops the `isolation` param from the Agent tool spec on next pi session.",
|
|
3675
|
-
currentValue: isWorktreeIsolationEnabled() ? "on" : "off",
|
|
3676
|
-
values: ["on", "off"],
|
|
3677
|
-
},
|
|
3678
|
-
{
|
|
3679
|
-
id: "reportUsage",
|
|
3680
|
-
label: "Report usage to session",
|
|
3681
|
-
description:
|
|
3682
|
-
"Add subagent tokens and cost to this session's own totals, so pi's footer and /cost stop reading a delegating session as nearly free. Reported on the next tool result (agents that finish in the background are counted on the one after). Context-window % is unaffected.",
|
|
3683
|
-
currentValue: isReportUsageEnabled() ? "on" : "off",
|
|
3684
|
-
values: ["on", "off"],
|
|
3685
|
-
},
|
|
3686
|
-
{
|
|
3687
|
-
id: "showCost",
|
|
3688
|
-
label: "Show cost",
|
|
3689
|
-
description:
|
|
3690
|
-
"Show an estimated `~$0.0042` beside subagent token counts in the widget, results and notifications. Priced by pi from the model's rates — omitted entirely for a model it has no rates for.",
|
|
3691
|
-
currentValue: isShowCostEnabled() ? "on" : "off",
|
|
3692
|
-
values: ["on", "off"],
|
|
3693
|
-
},
|
|
3694
|
-
{
|
|
3695
|
-
id: "showModel",
|
|
3696
|
-
label: "Show model",
|
|
3697
|
-
description:
|
|
3698
|
-
"Name the model driving each agent, and the thinking level it is running at, on the widget's running rows. The Agent tool result and the conversation viewer show the pair either way — this adds it to the widget, where the row is already dense.",
|
|
3699
|
-
currentValue: isShowModelEnabled() ? "on" : "off",
|
|
3700
|
-
values: ["on", "off"],
|
|
3701
|
-
},
|
|
3702
|
-
{
|
|
3703
|
-
id: "viewerMarkdown",
|
|
3704
|
-
label: "Viewer markdown",
|
|
3705
|
-
description:
|
|
3706
|
-
"How much of the conversation viewer renders as Markdown. assistant = assistant text only (default); all = tool results too, for tools that emit Markdown — accepting that a Markdown pass over a diff or a log eats `#` comments, swallows a `---` line and re-fences indented output; off = everything verbatim. `m` in the viewer cycles the same setting (footer: raw / md / md+).",
|
|
3707
|
-
currentValue: getViewerMarkdown(),
|
|
3708
|
-
values: ["off", "assistant", "all"],
|
|
3709
|
-
},
|
|
3710
|
-
{
|
|
3711
|
-
id: "agentMentions",
|
|
3712
|
-
label: "Agent mentions",
|
|
3713
|
-
description: "Route `@handle message` at the prompt to that agent. model = an off-screen clone of this conversation calls the Agent tool, so the agent gets a context-written prompt, a transcript and per-tool detail, and the chat stays clean; direct = started here from your text, no model call. Messaging and resuming are direct either way.",
|
|
3714
|
-
currentValue: getAgentMentionMode(),
|
|
3715
|
-
values: ["model", "direct", "off"],
|
|
3716
|
-
},
|
|
3717
|
-
{
|
|
3718
|
-
id: "rememberAgents",
|
|
3719
|
-
label: "Remember agents",
|
|
3720
|
-
description: "Persist subagent sessions so `@handle` can resume one long after it finished (they also appear in /resume)",
|
|
3721
|
-
currentValue: getRememberAgents() ? "on" : "off",
|
|
3722
|
-
values: ["on", "off"],
|
|
3723
|
-
},
|
|
3724
2395
|
{
|
|
3725
2396
|
id: "widgetMode",
|
|
3726
2397
|
label: "Widget",
|
|
@@ -3745,15 +2416,6 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3745
2416
|
manager.setMaxConcurrent(n);
|
|
3746
2417
|
notifyApplied(ctx, `Max concurrency set to ${n}`);
|
|
3747
2418
|
}
|
|
3748
|
-
} else if (id === "maxConcurrentForeground") {
|
|
3749
|
-
// 0 is meaningful here, unlike maxConcurrent above: it means unlimited.
|
|
3750
|
-
const n = parseInt(value, 10);
|
|
3751
|
-
if (n >= 0) {
|
|
3752
|
-
manager.setMaxConcurrentForeground(n);
|
|
3753
|
-
notifyApplied(ctx, n === 0
|
|
3754
|
-
? "Max foreground concurrency set to unlimited"
|
|
3755
|
-
: `Max foreground concurrency set to ${n}`);
|
|
3756
|
-
}
|
|
3757
2419
|
} else if (id === "defaultMaxTurns") {
|
|
3758
2420
|
const n = parseInt(value, 10);
|
|
3759
2421
|
if (n === 0) {
|
|
@@ -3769,29 +2431,9 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3769
2431
|
setGraceTurns(n);
|
|
3770
2432
|
notifyApplied(ctx, `Grace turns set to ${n}`);
|
|
3771
2433
|
}
|
|
3772
|
-
} else if (id === "maxSubagentDepth") {
|
|
3773
|
-
const n = parseInt(value, 10);
|
|
3774
|
-
if (n >= 0) {
|
|
3775
|
-
setMaxSubagentDepth(n);
|
|
3776
|
-
notifyApplied(
|
|
3777
|
-
ctx,
|
|
3778
|
-
n <= 1
|
|
3779
|
-
? "Nested delegation disabled"
|
|
3780
|
-
: `Nested depth set to ${n}. Applies to agents started from now on.`,
|
|
3781
|
-
);
|
|
3782
|
-
}
|
|
3783
2434
|
} else if (id === "joinMode") {
|
|
3784
2435
|
setDefaultJoinMode(value as JoinMode);
|
|
3785
2436
|
notifyApplied(ctx, `Default join mode set to ${value}`);
|
|
3786
|
-
} else if (id === "backgroundByDefault") {
|
|
3787
|
-
const enabled = value === "on";
|
|
3788
|
-
setBackgroundByDefault(enabled);
|
|
3789
|
-
notifyApplied(
|
|
3790
|
-
ctx,
|
|
3791
|
-
enabled
|
|
3792
|
-
? "Agent calls run in the background unless they pass run_in_background: false"
|
|
3793
|
-
: "Agent calls block and return inline unless they pass run_in_background: true",
|
|
3794
|
-
);
|
|
3795
2437
|
} else if (id === "schedulingEnabled") {
|
|
3796
2438
|
const enabled = value === "on";
|
|
3797
2439
|
if (enabled === isSchedulingEnabled()) {
|
|
@@ -3804,91 +2446,21 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3804
2446
|
`Scheduling ${enabled ? "enabled" : "disabled"}. Tool spec change takes effect on next pi session.`,
|
|
3805
2447
|
);
|
|
3806
2448
|
}
|
|
3807
|
-
} else if (id === "workflowsEnabled") {
|
|
3808
|
-
const enabled = value === "on";
|
|
3809
|
-
if (enabled === isWorkflowsEnabled()) {
|
|
3810
|
-
ctx.ui.notify(`Workflows already ${enabled ? "enabled" : "disabled"}.`, "info");
|
|
3811
|
-
} else {
|
|
3812
|
-
setWorkflowsEnabled(enabled);
|
|
3813
|
-
// Runs already in flight keep going: the switch governs whether the
|
|
3814
|
-
// tool is offered, and killing live agents on a settings toggle would
|
|
3815
|
-
// lose work the user never asked to discard.
|
|
3816
|
-
notifyApplied(
|
|
3817
|
-
ctx,
|
|
3818
|
-
`Workflows ${enabled ? "enabled" : "disabled"}. Tool spec change takes effect on next pi session.`,
|
|
3819
|
-
);
|
|
3820
|
-
}
|
|
3821
2449
|
} else if (id === "scopeModels") {
|
|
3822
2450
|
const enabled = value === "on";
|
|
3823
2451
|
setScopeModelsEnabled(enabled);
|
|
3824
2452
|
notifyApplied(ctx, `Scope models ${enabled ? "enabled" : "disabled"}`);
|
|
3825
|
-
} else if (id === "strictAgentFiles") {
|
|
3826
|
-
const enabled = value === "on";
|
|
3827
|
-
strictAgentFiles = enabled;
|
|
3828
|
-
notifyApplied(ctx, `Strict agent files ${enabled ? "enabled" : "disabled"}. Takes effect on next pi session.`);
|
|
3829
2453
|
} else if (id === "disableDefaultAgents") {
|
|
3830
2454
|
const enabled = value === "on";
|
|
3831
2455
|
setDisableDefaultAgents(enabled);
|
|
3832
2456
|
notifyApplied(ctx, `Default agents ${enabled ? "disabled" : "enabled"}. Tool spec change takes effect on next pi session.`);
|
|
3833
|
-
} else if (id === "fallbackSubagent") {
|
|
3834
|
-
setFallbackSubagent(value);
|
|
3835
|
-
notifyApplied(
|
|
3836
|
-
ctx,
|
|
3837
|
-
value === NO_FALLBACK
|
|
3838
|
-
? "Unknown or disabled agent types will now be rejected"
|
|
3839
|
-
: `Unknown agent types will fall back to ${value}`,
|
|
3840
|
-
);
|
|
3841
2457
|
} else if (id === "outputTranscript") {
|
|
3842
2458
|
const enabled = value === "on";
|
|
3843
|
-
|
|
2459
|
+
setOutputTranscript(enabled);
|
|
3844
2460
|
notifyApplied(ctx, `Output transcript ${enabled ? "enabled" : "disabled"} by default`);
|
|
3845
|
-
} else if (id === "worktreeIsolation") {
|
|
3846
|
-
const enabled = value === "on";
|
|
3847
|
-
setWorktreeIsolationEnabled(enabled);
|
|
3848
|
-
// The refusal is live, but the tool schema is built at registration, so
|
|
3849
|
-
// the isolation parameter only appears/disappears next session.
|
|
3850
|
-
notifyApplied(
|
|
3851
|
-
ctx,
|
|
3852
|
-
`Worktree isolation ${enabled ? "enabled" : "disabled"}. Tool parameter updates on next pi session.`,
|
|
3853
|
-
);
|
|
3854
2461
|
} else if (id === "toolDescriptionMode") {
|
|
3855
2462
|
setToolDescriptionMode(value as ToolDescriptionMode);
|
|
3856
2463
|
notifyApplied(ctx, `Tool description set to ${value}. Takes effect on next pi session.`);
|
|
3857
|
-
} else if (id === "reportUsage") {
|
|
3858
|
-
const enabled = value === "on";
|
|
3859
|
-
setReportUsage(enabled);
|
|
3860
|
-
notifyApplied(
|
|
3861
|
-
ctx,
|
|
3862
|
-
enabled
|
|
3863
|
-
? "Subagent usage now counted in this session's totals"
|
|
3864
|
-
: "Subagent usage no longer counted in this session's totals",
|
|
3865
|
-
);
|
|
3866
|
-
} else if (id === "showCost") {
|
|
3867
|
-
const enabled = value === "on";
|
|
3868
|
-
setShowCost(enabled);
|
|
3869
|
-
notifyApplied(ctx, `Cost display ${enabled ? "enabled" : "disabled"}`);
|
|
3870
|
-
} else if (id === "showModel") {
|
|
3871
|
-
const enabled = value === "on";
|
|
3872
|
-
setShowModel(enabled);
|
|
3873
|
-
notifyApplied(ctx, `Model display ${enabled ? "enabled" : "disabled"}`);
|
|
3874
|
-
} else if (id === "viewerMarkdown") {
|
|
3875
|
-
setViewerMarkdown(value as ViewerMarkdownMode);
|
|
3876
|
-
notifyApplied(ctx, `Viewer markdown set to ${value}`);
|
|
3877
|
-
} else if (id === "agentMentions") {
|
|
3878
|
-
const mode = value as AgentMentionMode;
|
|
3879
|
-
setAgentMentionMode(mode);
|
|
3880
|
-
notifyApplied(
|
|
3881
|
-
ctx,
|
|
3882
|
-
mode === "off"
|
|
3883
|
-
? "Agent mentions disabled"
|
|
3884
|
-
: mode === "model"
|
|
3885
|
-
? "Agent mentions on — a conversation clone starts a mentioned agent off-screen"
|
|
3886
|
-
: "Agent mentions on — a mentioned agent starts here, with no model call",
|
|
3887
|
-
);
|
|
3888
|
-
} else if (id === "rememberAgents") {
|
|
3889
|
-
const enabled = value === "on";
|
|
3890
|
-
setRememberAgents(enabled);
|
|
3891
|
-
notifyApplied(ctx, `Remember agents ${enabled ? "enabled" : "disabled"}`);
|
|
3892
2464
|
} else if (id === "widgetMode") {
|
|
3893
2465
|
setWidgetMode(value as WidgetMode);
|
|
3894
2466
|
notifyApplied(ctx, `Widget set to ${value}`);
|
|
@@ -3943,23 +2515,15 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3943
2515
|
if (result && NUMERIC_IDS.has(result)) {
|
|
3944
2516
|
const current = result === "maxConcurrent"
|
|
3945
2517
|
? String(manager.getMaxConcurrent())
|
|
3946
|
-
: result === "
|
|
3947
|
-
? String(
|
|
3948
|
-
:
|
|
3949
|
-
? String(getDefaultMaxTurns() ?? 0)
|
|
3950
|
-
: result === "maxSubagentDepth"
|
|
3951
|
-
? String(getMaxSubagentDepth())
|
|
3952
|
-
: String(getGraceTurns());
|
|
2518
|
+
: result === "defaultMaxTurns"
|
|
2519
|
+
? String(getDefaultMaxTurns() ?? 0)
|
|
2520
|
+
: String(getGraceTurns());
|
|
3953
2521
|
|
|
3954
2522
|
const label = result === "maxConcurrent"
|
|
3955
2523
|
? "Max concurrency (1+)"
|
|
3956
|
-
: result === "
|
|
3957
|
-
? "
|
|
3958
|
-
:
|
|
3959
|
-
? "Default max turns (0 = unlimited)"
|
|
3960
|
-
: result === "maxSubagentDepth"
|
|
3961
|
-
? "Nested depth (0/1 = nesting off)"
|
|
3962
|
-
: "Grace turns (1+)";
|
|
2524
|
+
: result === "defaultMaxTurns"
|
|
2525
|
+
? "Default max turns (0 = unlimited)"
|
|
2526
|
+
: "Grace turns (1+)";
|
|
3963
2527
|
|
|
3964
2528
|
// Loop until user enters a valid integer or cancels (Esc / null).
|
|
3965
2529
|
// Silently trims whitespace; rejects non-numeric input by re-prompting.
|
|
@@ -3978,6 +2542,10 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3978
2542
|
}
|
|
3979
2543
|
}
|
|
3980
2544
|
|
|
2545
|
+
// Persist the current snapshot, emit `subagents:settings_changed`, and surface
|
|
2546
|
+
// the right toast. Successful saves show info; persistence failures downgrade
|
|
2547
|
+
// to warning so users aren't silently reverted on restart. Event fires regardless
|
|
2548
|
+
// of outcome so listeners see the in-memory change.
|
|
3981
2549
|
function notifyApplied(ctx: ExtensionCommandContext, successMsg: string) {
|
|
3982
2550
|
const { message, level } = saveAndEmitChanged(
|
|
3983
2551
|
snapshotSettings(),
|
|
@@ -3991,12 +2559,4 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
3991
2559
|
description: "Manage agents",
|
|
3992
2560
|
handler: async (_args, ctx) => { await showAgentsMenu(ctx); },
|
|
3993
2561
|
});
|
|
3994
|
-
|
|
3995
|
-
/** Dependencies shared by `/agents → Workflows` and its inspector. */
|
|
3996
|
-
const workflowMenuDeps: WorkflowMenuDeps = {
|
|
3997
|
-
tasks: workflowTasks,
|
|
3998
|
-
getRecord: id => manager.getRecord(id),
|
|
3999
|
-
viewAgentConversation,
|
|
4000
|
-
};
|
|
4001
|
-
|
|
4002
2562
|
}
|