one-code-extension 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -202
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -12,20 +12,47 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Orchestration is opt-in, like Claude Code: the tool description gates it,
|
|
14
14
|
* and the literal keyword "ultracode" in a user message arms the turn via a
|
|
15
|
-
* system reminder
|
|
15
|
+
* system reminder (skipped while `/effort ultracode` has the standing block
|
|
16
|
+
* on — see `effort/`). The keyword is read by pi's `input` event, which only
|
|
17
|
+
* a prompt sent to an idle session fires; a message queued mid-turn does not
|
|
18
|
+
* arm (docs/upstream_prs.md #14).
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
22
|
import os from "node:os";
|
|
20
23
|
import { join } from "node:path";
|
|
21
24
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
22
25
|
import { Type } from "typebox";
|
|
26
|
+
import { recordUsage } from "../lib/usage-bus.ts";
|
|
27
|
+
import { MODEL_UNUSABLE_CHANNEL, type ModelUnusableEvent } from "../lib/model-unusable.ts";
|
|
28
|
+
import { whenAborted } from "../lib/abort.ts";
|
|
29
|
+
import { createTaskNotifier, oneShotNote, sessionOutlivesTurn } from "../lib/notifications.ts";
|
|
23
30
|
import { REMINDER_CHANNEL } from "../lib/reminders.ts";
|
|
31
|
+
import { ULTRACODE_MODE_CHANNEL } from "../effort/slider.ts";
|
|
32
|
+
import { PERMISSION_STATUS_CHANNEL } from "../permissions/modes.ts";
|
|
33
|
+
import { watchPermissionBridge } from "../permissions/subagent-gate.ts";
|
|
34
|
+
import { watchHookBridge } from "../hooks/subagent-bridge.ts";
|
|
24
35
|
import { applicableSubagentDefault, loadSubagentDefault } from "../subagents/default-model.ts";
|
|
25
|
-
import { discoverSavedWorkflows, findSavedWorkflow } from "./saved-workflows.ts";
|
|
36
|
+
import { discoverSavedWorkflows, findSavedWorkflow, workflowDirs } from "./saved-workflows.ts";
|
|
26
37
|
import { buildRunReport, WorkflowRunManager } from "./run-manager.ts";
|
|
27
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
ccToolRenderers,
|
|
40
|
+
customMessageText,
|
|
41
|
+
notificationComponent,
|
|
42
|
+
safeThemeBold,
|
|
43
|
+
safeThemePaint,
|
|
44
|
+
truncateLine,
|
|
45
|
+
} from "../lib/tui-render.ts";
|
|
28
46
|
import { WorkflowScriptError } from "./types.ts";
|
|
47
|
+
import {
|
|
48
|
+
clampViewerState,
|
|
49
|
+
decodeStatusKey,
|
|
50
|
+
decodeViewerKey,
|
|
51
|
+
initialViewerState,
|
|
52
|
+
planSave,
|
|
53
|
+
renderViewer,
|
|
54
|
+
type ViewerRunSnapshot,
|
|
55
|
+
} from "./viewer.ts";
|
|
29
56
|
import { WorkflowWidget } from "./widget.ts";
|
|
30
57
|
|
|
31
58
|
/**
|
|
@@ -39,7 +66,7 @@ const ULTRACODE_REMINDER =
|
|
|
39
66
|
/**
|
|
40
67
|
* Ported from Claude Code's Workflow tool description, adapted to this harness
|
|
41
68
|
* (runId + follow-up message instead of a task id + task-notification,
|
|
42
|
-
* `/workflows stop` instead of TaskStop, `
|
|
69
|
+
* `/workflows stop` instead of TaskStop, `Agent` as the single-agent
|
|
43
70
|
* alternative). The authoring guidance is the substantive part: it is what
|
|
44
71
|
* makes the model reach for pipeline() over barriers and pick sane fan-out
|
|
45
72
|
* sizes, so it is kept rather than trimmed.
|
|
@@ -50,10 +77,12 @@ A workflow structures work across many agents — to be comprehensive (decompose
|
|
|
50
77
|
|
|
51
78
|
ONLY call this tool when the user has explicitly opted into multi-agent orchestration. Workflows can spawn dozens of agents and consume a large amount of tokens; the user must request that scale, not have it inferred. Explicit opt-in means one of:
|
|
52
79
|
- The user included the keyword "ultracode" in their prompt (you'll see a system-reminder confirming it).
|
|
80
|
+
- Ultracode is on for the session (a system-reminder confirms it), which makes the opt-in standing for every turn until it is turned off.
|
|
53
81
|
- The user directly asked you to run a workflow or use multi-agent orchestration in their own words ("use a workflow", "run a workflow", "fan out agents", "orchestrate this with subagents"). The ask must be in the user's words — a task that would merely benefit from a workflow does not count.
|
|
82
|
+
- The user invoked a skill or slash command whose instructions tell you to call Workflow.
|
|
54
83
|
- The user asked you to run a specific named or saved workflow.
|
|
55
84
|
|
|
56
|
-
For any other task — even one that would clearly benefit from parallelism — do NOT call this tool. Use the \`
|
|
85
|
+
For any other task — even one that would clearly benefit from parallelism — do NOT call this tool. Use the \`Agent\` tool for individual subagents, or briefly describe what a multi-agent workflow could do and how much it would roughly cost, and ask the user whether to run it.
|
|
57
86
|
|
|
58
87
|
When you do call it, the right move is often **hybrid**: scout inline first (list the files, find the targets, scope the diff) to discover the work-list, then call workflow to pipeline over it. You don't need to know the shape before the *task* — only before the *orchestration step*.
|
|
59
88
|
|
|
@@ -89,7 +118,7 @@ Script body hooks:
|
|
|
89
118
|
- log(message: string): void — emit a progress message to the user.
|
|
90
119
|
- phase(title: string): void — start a new phase; subsequent agent() calls are grouped under it.
|
|
91
120
|
- args: any — the value passed as this tool's \`args\` input, verbatim. Pass arrays/objects as actual JSON values, NOT as a JSON-encoded string.
|
|
92
|
-
- budget: {total: number|null, spent(): number, remaining(): number} — the run's token target from \`tokenBudget
|
|
121
|
+
- budget: {total: number|null, spent(): number, remaining(): number} — the run's output-token target from \`tokenBudget\`, shared with nested workflows. \`total\` is null if none was set. Once spent() reaches total, new and queued agent() calls throw before starting. Already-running agents may finish above the target; this is not a hard cap on provider usage. Use for dynamic loops: \`while (budget.total && budget.remaining() > 50_000) { ... }\`.
|
|
93
122
|
- workflow(nameOrRef: string | {scriptPath: string}, args?: any): Promise<any> — run another workflow inline as a sub-step. Nesting is one level only.
|
|
94
123
|
|
|
95
124
|
DEFAULT TO pipeline(). Only reach for a barrier (parallel between stages) when you genuinely need ALL prior-stage results together — dedup/merge across the full result set, early-exit on a zero count, or a prompt that references "the other findings". A barrier is NOT justified by "I need to flatten/map/filter first" (do it inside a pipeline stage) or "the stages are conceptually separate".
|
|
@@ -165,7 +194,7 @@ const WorkflowParams = Type.Object({
|
|
|
165
194
|
tokenBudget: Type.Optional(
|
|
166
195
|
Type.Integer({
|
|
167
196
|
minimum: 1000,
|
|
168
|
-
description: "
|
|
197
|
+
description: "Output-token target shared by the run and nested workflows. Stops new and queued agents once reached; already-running agents may exceed it. Exposed as budget {total, spent(), remaining()}",
|
|
169
198
|
}),
|
|
170
199
|
),
|
|
171
200
|
sync: Type.Optional(
|
|
@@ -174,35 +203,59 @@ const WorkflowParams = Type.Object({
|
|
|
174
203
|
});
|
|
175
204
|
|
|
176
205
|
export default function workflowExtension(pi: ExtensionAPI) {
|
|
206
|
+
// Models the account refused this session (classifier/subagent reports, lib/model-unusable.ts):
|
|
207
|
+
// workflow agents share the subagent selector, so they resolve around them too.
|
|
208
|
+
const unusableModels = new Set<string>();
|
|
209
|
+
pi.events.on(MODEL_UNUSABLE_CHANNEL, (data) => {
|
|
210
|
+
unusableModels.add((data as ModelUnusableEvent).model);
|
|
211
|
+
});
|
|
212
|
+
const notifyTask = createTaskNotifier(pi);
|
|
177
213
|
const manager = new WorkflowRunManager();
|
|
178
214
|
let lastCtx: ExtensionContext | undefined;
|
|
179
215
|
const widget = new WorkflowWidget(manager, () => lastCtx);
|
|
180
216
|
const deliveredRuns = new Set<string>();
|
|
217
|
+
let viewerOpen = false;
|
|
218
|
+
|
|
219
|
+
// Parent permission bridge for workflow agents' gates — same bridge the
|
|
220
|
+
// subagent runner uses; see AgentRunnerOptions.getPermissionBridge.
|
|
221
|
+
const getPermissionBridge = watchPermissionBridge(pi);
|
|
222
|
+
const getHookBridge = watchHookBridge(pi);
|
|
223
|
+
|
|
224
|
+
const openViewer = async (ctx: ExtensionContext, opts?: { height?: "full" | "half"; runIndex?: number }) => {
|
|
225
|
+
if (viewerOpen) return;
|
|
226
|
+
viewerOpen = true;
|
|
227
|
+
try {
|
|
228
|
+
await openWorkflowViewer(ctx, manager, opts);
|
|
229
|
+
} finally {
|
|
230
|
+
viewerOpen = false;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
181
233
|
|
|
182
234
|
const deliverResult = (runId: string) => {
|
|
183
235
|
const handle = manager.get(runId);
|
|
184
236
|
if (!handle || deliveredRuns.has(runId)) return;
|
|
185
237
|
deliveredRuns.add(runId);
|
|
186
|
-
|
|
187
|
-
{
|
|
188
|
-
customType: "workflow-result",
|
|
189
|
-
content: [{ type: "text", text: buildRunReport(handle) }],
|
|
190
|
-
display: true,
|
|
191
|
-
details: { runId: handle.runId, name: handle.meta.name, status: handle.status },
|
|
192
|
-
},
|
|
193
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
194
|
-
);
|
|
238
|
+
notifyTask("one-code:workflow-result", buildRunReport(handle), { runId: handle.runId, name: handle.meta.name, status: handle.status });
|
|
195
239
|
};
|
|
196
240
|
|
|
197
|
-
pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) =>
|
|
241
|
+
pi.registerMessageRenderer("one-code:workflow-result", (message, { expanded }, theme) =>
|
|
198
242
|
notificationComponent(theme, customMessageText(message.content), expanded),
|
|
199
243
|
);
|
|
200
244
|
|
|
201
245
|
pi.registerTool({
|
|
202
246
|
name: "workflow",
|
|
203
247
|
label: "Workflow",
|
|
204
|
-
...ccToolRenderers<
|
|
248
|
+
...ccToolRenderers<
|
|
249
|
+
{ name?: string; scriptPath?: string; script?: string; resumeFromRunId?: string },
|
|
250
|
+
{ runId?: string; background?: boolean }
|
|
251
|
+
>("Workflow", {
|
|
205
252
|
title: (a) => a?.name ?? a?.scriptPath ?? (a?.resumeFromRunId ? `resume ${a.resumeFromRunId}` : a?.script ? "inline script" : undefined),
|
|
253
|
+
// The model-facing start text is instructions, not information — show
|
|
254
|
+
// the user Claude Code's hint line instead.
|
|
255
|
+
result: (result, _args, isError) =>
|
|
256
|
+
!isError && result.details?.background
|
|
257
|
+
? `Running in background · /workflows to monitor and save · ${result.details.runId ?? ""}`.trimEnd()
|
|
258
|
+
: undefined,
|
|
206
259
|
}),
|
|
207
260
|
description: WORKFLOW_TOOL_DESCRIPTION,
|
|
208
261
|
promptSnippet: "Run a script that orchestrates many subagents (opt-in ultracode mode)",
|
|
@@ -247,12 +300,28 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
247
300
|
cwd: ctx.cwd,
|
|
248
301
|
sessionDir,
|
|
249
302
|
defaultModel: ctx.model,
|
|
250
|
-
|
|
303
|
+
configuredDefault,
|
|
251
304
|
defaultEffort: ctx.thinkingLevel,
|
|
305
|
+
getPermissionBridge,
|
|
306
|
+
getHookBridge,
|
|
307
|
+
// Workflow agents run in their own sessions; their spend reaches the footer only through the bus.
|
|
308
|
+
onUsage: (cost) => recordUsage(pi, "subagent", { cost: { total: cost } }),
|
|
309
|
+
unusableModels: () => unusableModels,
|
|
310
|
+
onModelUnusable: (model, reason) => {
|
|
311
|
+
if (unusableModels.has(model)) return;
|
|
312
|
+
unusableModels.add(model);
|
|
313
|
+
pi.events.emit(MODEL_UNUSABLE_CHANNEL, { model, reason } satisfies ModelUnusableEvent);
|
|
314
|
+
},
|
|
252
315
|
});
|
|
253
316
|
widget.attach(handle);
|
|
254
317
|
|
|
255
|
-
|
|
318
|
+
// One-shot modes (`-p` / `--mode json`) exit when the turn settles:
|
|
319
|
+
// session_shutdown aborts every run, so a backgrounded workflow never
|
|
320
|
+
// finished and its promised follow-up never came (LIFECYCLE-REVIEW
|
|
321
|
+
// M1, measured: no result, no journal). Run it to completion there,
|
|
322
|
+
// the rule bash and Agent apply, and return the report in the result.
|
|
323
|
+
const forcedSync = !params.sync && !sessionOutlivesTurn(ctx.mode);
|
|
324
|
+
if (params.sync || forcedSync) {
|
|
256
325
|
const onProgress = () => {
|
|
257
326
|
onUpdate?.({
|
|
258
327
|
content: [{ type: "text", text: handle.recentEvents.slice(-8).join("\n") || "starting…" }],
|
|
@@ -260,17 +329,17 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
260
329
|
});
|
|
261
330
|
};
|
|
262
331
|
handle.on("progress", onProgress);
|
|
263
|
-
const
|
|
264
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
332
|
+
const unhookAbort = whenAborted(signal, () => handle.abort("tool call aborted"));
|
|
265
333
|
try {
|
|
266
334
|
await handle.finished;
|
|
267
335
|
} finally {
|
|
268
|
-
|
|
336
|
+
unhookAbort();
|
|
269
337
|
handle.removeListener("progress", onProgress);
|
|
270
338
|
}
|
|
271
339
|
deliveredRuns.add(handle.runId); // sync result goes in the tool result, not a followUp
|
|
340
|
+
const oneShotPrefix = forcedSync ? `${oneShotNote("workflow")}\n\n` : "";
|
|
272
341
|
return {
|
|
273
|
-
content: [{ type: "text", text: buildRunReport(handle) }],
|
|
342
|
+
content: [{ type: "text", text: `${oneShotPrefix}${buildRunReport(handle)}` }],
|
|
274
343
|
details: { runId: handle.runId, status: handle.status, scriptPath: handle.scriptPath },
|
|
275
344
|
isError: handle.status !== "completed",
|
|
276
345
|
};
|
|
@@ -302,14 +371,18 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
302
371
|
});
|
|
303
372
|
|
|
304
373
|
pi.registerCommand("workflows", {
|
|
305
|
-
description: "
|
|
374
|
+
description: "Open the workflow viewer; list, stop, or inspect runs",
|
|
306
375
|
getArgumentCompletions: (prefix) => {
|
|
307
|
-
const items = ["stop ", "log "].filter((c) => c.startsWith(prefix));
|
|
376
|
+
const items = ["stop ", "log ", "list"].filter((c) => c.startsWith(prefix));
|
|
308
377
|
return items.length ? items.map((c) => ({ value: c, label: c.trim() })) : null;
|
|
309
378
|
},
|
|
310
379
|
handler: async (args, ctx) => {
|
|
311
380
|
lastCtx = ctx;
|
|
312
381
|
const [action, runId] = args.trim().split(/\s+/);
|
|
382
|
+
if (!action && ctx.hasUI) {
|
|
383
|
+
await openViewer(ctx, { height: "full" });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
313
386
|
if (action === "stop" && runId) {
|
|
314
387
|
const stopped = manager.abort(runId, "stopped via /workflows");
|
|
315
388
|
ctx.ui.notify(stopped ? `Stopping ${runId}…` : `No running workflow ${runId}`, stopped ? "info" : "error");
|
|
@@ -347,18 +420,260 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
347
420
|
},
|
|
348
421
|
});
|
|
349
422
|
|
|
423
|
+
// While ultracode MODE is on (/effort ultracode) the standing block already
|
|
424
|
+
// carries the opt-in, so the keyword's single-turn one-shot is skipped: one
|
|
425
|
+
// instruction for one fact. `input` fires for prompt() only — a message the
|
|
426
|
+
// user queues while a turn is running (pi's steer()/followUp()) is expanded
|
|
427
|
+
// without it and does not arm the turn (upstream ask #14; the mode covers it).
|
|
428
|
+
let ultracodeMode = false;
|
|
429
|
+
pi.events.on(ULTRACODE_MODE_CHANNEL, (data) => {
|
|
430
|
+
ultracodeMode = (data as { active?: boolean })?.active === true;
|
|
431
|
+
});
|
|
350
432
|
pi.on("input", (event) => {
|
|
351
|
-
if (/\bultracode\b/i.test(event.text)) {
|
|
433
|
+
if (!ultracodeMode && /\bultracode\b/i.test(event.text)) {
|
|
352
434
|
pi.events.emit(REMINDER_CHANNEL, { text: ULTRACODE_REMINDER, scope: "next-turn" });
|
|
353
435
|
}
|
|
354
436
|
return undefined;
|
|
355
437
|
});
|
|
356
438
|
|
|
439
|
+
// Down-arrow soft focus for the below-editor status strip (Claude Code's
|
|
440
|
+
// bottom workflow entry): with the editor focused and empty, ↓ highlights
|
|
441
|
+
// the newest run's row; ↑/↓ move, enter opens the half-screen viewer, x
|
|
442
|
+
// stops the run, esc — or typing anything — returns to the editor. The
|
|
443
|
+
// listener runs before the focused component sees the byte, so every
|
|
444
|
+
// consume is guarded by "the core editor really has focus" (identity
|
|
445
|
+
// against the captured baseline) to never steal keys from dialogs.
|
|
446
|
+
let inputHookRegistered = false;
|
|
447
|
+
const DOWN_KEYS = new Set(["\x1b[B", "\x1bOB"]);
|
|
448
|
+
const registerInputHook = (registerCtx: ExtensionContext) => {
|
|
449
|
+
if (inputHookRegistered || !registerCtx.hasUI) return;
|
|
450
|
+
inputHookRegistered = true;
|
|
451
|
+
const leave = () => widget.setFocus(undefined);
|
|
452
|
+
try {
|
|
453
|
+
registerCtx.ui.onTerminalInput((data) => {
|
|
454
|
+
// Session switches replace the ExtensionContext; the hook registers
|
|
455
|
+
// once, so it must act through the freshest ctx, not its closure.
|
|
456
|
+
const ctx = lastCtx ?? registerCtx;
|
|
457
|
+
if (viewerOpen || !widget.rowCount()) {
|
|
458
|
+
if (widget.focusIndex !== undefined) leave();
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
if (widget.focusIndex === undefined) {
|
|
462
|
+
if (!DOWN_KEYS.has(data) || !widget.editorFocusedAndIdle(ctx)) return undefined;
|
|
463
|
+
widget.setFocus(0);
|
|
464
|
+
return { consume: true };
|
|
465
|
+
}
|
|
466
|
+
// Focus moved to a dialog/overlay since the strip took soft focus —
|
|
467
|
+
// drop it and let the dialog have the key.
|
|
468
|
+
if (!widget.editorFocused()) {
|
|
469
|
+
leave();
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
const key = decodeStatusKey(data);
|
|
473
|
+
if (!key) {
|
|
474
|
+
leave();
|
|
475
|
+
return undefined; // typing resumes in the editor, byte included
|
|
476
|
+
}
|
|
477
|
+
switch (key) {
|
|
478
|
+
case "up":
|
|
479
|
+
if (widget.focusIndex === 0) leave();
|
|
480
|
+
else widget.setFocus(widget.focusIndex - 1);
|
|
481
|
+
return { consume: true };
|
|
482
|
+
case "down":
|
|
483
|
+
widget.setFocus(Math.min(widget.rowCount() - 1, widget.focusIndex + 1));
|
|
484
|
+
return { consume: true };
|
|
485
|
+
case "leave":
|
|
486
|
+
leave();
|
|
487
|
+
// While the model streams, esc must still interrupt it — drop the
|
|
488
|
+
// soft focus and let the byte through; consumed only when idle.
|
|
489
|
+
return ctx.isIdle() ? { consume: true } : undefined;
|
|
490
|
+
case "stop": {
|
|
491
|
+
const run = widget.selectedRun();
|
|
492
|
+
if (run) manager.abort(run.runId, "stopped from status strip");
|
|
493
|
+
return { consume: true };
|
|
494
|
+
}
|
|
495
|
+
case "open": {
|
|
496
|
+
const runIndex = widget.focusIndex;
|
|
497
|
+
leave();
|
|
498
|
+
void openViewer(ctx, { height: "half", runIndex });
|
|
499
|
+
return { consume: true };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
} catch {
|
|
504
|
+
// Mode without raw terminal input (print/RPC) — the strip is view-only.
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
357
508
|
pi.on("session_start", (_event, ctx) => {
|
|
358
509
|
lastCtx = ctx;
|
|
510
|
+
registerInputHook(ctx);
|
|
359
511
|
});
|
|
360
512
|
|
|
513
|
+
// The permission-mode badge is a below-editor widget too, re-set on every
|
|
514
|
+
// mode change; setWidget order is last-write order, so re-set the strip
|
|
515
|
+
// whenever the badge changes to keep it beneath the mode line (CC's order).
|
|
516
|
+
pi.events.on(PERMISSION_STATUS_CHANNEL, () => widget.refresh());
|
|
517
|
+
|
|
361
518
|
pi.on("session_shutdown", () => {
|
|
519
|
+
// Fires on /clear, /new and /resume too (findings §8): after this the ctx
|
|
520
|
+
// is invalid, so nothing below may paint on it.
|
|
362
521
|
manager.abortAll("session ended");
|
|
522
|
+
widget.dispose();
|
|
523
|
+
lastCtx = undefined;
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* The interactive workflow viewer (Claude Code's /workflows screen). All
|
|
529
|
+
* layout/key logic is pure in viewer.ts; this component owns only mutable
|
|
530
|
+
* state, the repaint ticker, and the save-to-.claude/workflows fs work.
|
|
531
|
+
*
|
|
532
|
+
* Liveness comes from a 500ms ticker (invalidate + requestRender) instead of
|
|
533
|
+
* per-RunHandle subscriptions — it also picks up runs that start while the
|
|
534
|
+
* viewer is open, and the memoized render keeps the per-frame cost at a map
|
|
535
|
+
* lookup (findings §15).
|
|
536
|
+
*/
|
|
537
|
+
async function openWorkflowViewer(
|
|
538
|
+
ctx: ExtensionContext,
|
|
539
|
+
manager: WorkflowRunManager,
|
|
540
|
+
opts?: { height?: "full" | "half"; runIndex?: number },
|
|
541
|
+
): Promise<void> {
|
|
542
|
+
await ctx.ui.custom<null>((tui, theme, _keybindings, done) => {
|
|
543
|
+
const paint = { fg: safeThemePaint(theme), bold: safeThemeBold(theme) };
|
|
544
|
+
|
|
545
|
+
// Newest run first, so the viewer opens on the latest.
|
|
546
|
+
const snapshots = (): ViewerRunSnapshot[] => manager.snapshots();
|
|
547
|
+
|
|
548
|
+
// Full (the /workflows tab) fills the terminal; half (opened from the
|
|
549
|
+
// status strip) leaves the transcript visible above, like Claude Code.
|
|
550
|
+
const viewerHeight = (rows: number): number =>
|
|
551
|
+
opts?.height === "half" ? Math.max(12, Math.min(Math.floor(rows / 2), 30)) : Math.max(12, rows - 2);
|
|
552
|
+
|
|
553
|
+
const state = initialViewerState(opts?.runIndex ?? 0);
|
|
554
|
+
let cache: { width: number; lines: string[] } | undefined;
|
|
555
|
+
const repaint = () => {
|
|
556
|
+
cache = undefined;
|
|
557
|
+
tui.requestRender();
|
|
558
|
+
};
|
|
559
|
+
const ticker = setInterval(repaint, 500);
|
|
560
|
+
ticker.unref?.();
|
|
561
|
+
|
|
562
|
+
const save = (runs: ViewerRunSnapshot[]) => {
|
|
563
|
+
const run = runs[state.runIndex];
|
|
564
|
+
const handle = run && manager.get(run.runId);
|
|
565
|
+
if (!handle) return;
|
|
566
|
+
try {
|
|
567
|
+
const source = readFileSync(handle.scriptPath, "utf8");
|
|
568
|
+
const dir = workflowDirs(ctx.cwd, os.homedir()).project;
|
|
569
|
+
const safeName = run.name.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
570
|
+
const target = join(dir, `${safeName}.js`);
|
|
571
|
+
const exists = existsSync(target);
|
|
572
|
+
const sameContent = exists && readFileSync(target, "utf8") === source;
|
|
573
|
+
const plan = planSave({ name: safeName, exists, sameContent, confirmed: state.confirmOverwrite });
|
|
574
|
+
if (plan.action === "write") {
|
|
575
|
+
mkdirSync(dir, { recursive: true });
|
|
576
|
+
writeFileSync(target, source, "utf8");
|
|
577
|
+
state.confirmOverwrite = false;
|
|
578
|
+
} else if (plan.action === "confirm") {
|
|
579
|
+
state.confirmOverwrite = true;
|
|
580
|
+
}
|
|
581
|
+
state.notice = plan.notice;
|
|
582
|
+
} catch (error) {
|
|
583
|
+
state.notice = `save failed: ${(error as Error).message}`;
|
|
584
|
+
state.confirmOverwrite = false;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
render: (width: number) => {
|
|
590
|
+
if (cache?.width === width) return cache.lines;
|
|
591
|
+
const height = viewerHeight(tui.terminal.rows);
|
|
592
|
+
const lines = renderViewer({ runs: snapshots(), state, width, height, now: Date.now() }, paint).map(
|
|
593
|
+
(line) => truncateLine(line, width),
|
|
594
|
+
);
|
|
595
|
+
cache = { width, lines };
|
|
596
|
+
return lines;
|
|
597
|
+
},
|
|
598
|
+
handleInput: (data: string) => {
|
|
599
|
+
const key = decodeViewerKey(data);
|
|
600
|
+
if (!key) return;
|
|
601
|
+
const close = () => {
|
|
602
|
+
clearInterval(ticker);
|
|
603
|
+
done(null);
|
|
604
|
+
};
|
|
605
|
+
if (key.kind === "close") return close();
|
|
606
|
+
if (key.kind !== "save") {
|
|
607
|
+
state.notice = undefined;
|
|
608
|
+
state.confirmOverwrite = false;
|
|
609
|
+
}
|
|
610
|
+
switch (key.kind) {
|
|
611
|
+
case "up":
|
|
612
|
+
case "down": {
|
|
613
|
+
const delta = key.kind === "up" ? -1 : 1;
|
|
614
|
+
if (state.level === "phases") state.phaseCursor += delta;
|
|
615
|
+
else state.agentCursor += delta;
|
|
616
|
+
clampViewerState(state, snapshots());
|
|
617
|
+
state.detailScroll = 0;
|
|
618
|
+
state.promptExpanded = false;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
case "enter":
|
|
622
|
+
// Phases level drills into the phase; agents level toggles the prompt.
|
|
623
|
+
if (state.level === "phases") {
|
|
624
|
+
state.level = "agents";
|
|
625
|
+
state.agentCursor = 0;
|
|
626
|
+
state.detailScroll = 0;
|
|
627
|
+
state.promptExpanded = false;
|
|
628
|
+
clampViewerState(state, snapshots()); // falls back if the phase has no agents
|
|
629
|
+
} else {
|
|
630
|
+
state.promptExpanded = !state.promptExpanded;
|
|
631
|
+
}
|
|
632
|
+
break;
|
|
633
|
+
case "back":
|
|
634
|
+
if (state.level === "agents") {
|
|
635
|
+
state.level = "phases";
|
|
636
|
+
state.detailScroll = 0;
|
|
637
|
+
state.promptExpanded = false;
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
return close();
|
|
641
|
+
case "pageUp":
|
|
642
|
+
case "pageDown":
|
|
643
|
+
// Clamped against the detail's real length at render time.
|
|
644
|
+
state.detailScroll = Math.max(0, state.detailScroll + (key.kind === "pageUp" ? -8 : 8));
|
|
645
|
+
break;
|
|
646
|
+
case "nextRun": {
|
|
647
|
+
const count = manager.list().length;
|
|
648
|
+
state.runIndex = count ? (state.runIndex + 1) % count : 0;
|
|
649
|
+
state.level = "phases";
|
|
650
|
+
state.phaseCursor = 0;
|
|
651
|
+
state.agentCursor = 0;
|
|
652
|
+
state.detailScroll = 0;
|
|
653
|
+
state.promptExpanded = false;
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
case "stop": {
|
|
657
|
+
const run = snapshots()[state.runIndex];
|
|
658
|
+
const stopped = run && manager.abort(run.runId, "stopped from viewer");
|
|
659
|
+
state.notice = stopped ? `stopping ${run.runId}…` : "workflow is not running";
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
case "save":
|
|
663
|
+
save(snapshots());
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
repaint();
|
|
667
|
+
},
|
|
668
|
+
invalidate: () => {
|
|
669
|
+
cache = undefined;
|
|
670
|
+
},
|
|
671
|
+
// Covers every dismissal path that isn't the user pressing esc while
|
|
672
|
+
// focused (session end, another overlay taking over) — without it the
|
|
673
|
+
// ticker outlives the component.
|
|
674
|
+
dispose: () => {
|
|
675
|
+
clearInterval(ticker);
|
|
676
|
+
},
|
|
677
|
+
};
|
|
363
678
|
});
|
|
364
679
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-agent run records (pure): folds the RunProgressEvent stream into
|
|
3
|
+
* AgentRecord[] so the viewer can show each agent's prompt, model, activity,
|
|
4
|
+
* and outcome. RunHandle owns one store and applies every recorded event;
|
|
5
|
+
* timestamps are stamped here at apply time (host side — the vm's Date block
|
|
6
|
+
* does not apply).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AgentRecord, RunProgressEvent } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
/** The per-agent members of the event union (the ones records care about). */
|
|
12
|
+
type AgentEvent = Extract<RunProgressEvent, { callIndex: number }>;
|
|
13
|
+
|
|
14
|
+
function recordKey(callIndex: number, source?: string): string {
|
|
15
|
+
return `${source ?? ""}#${callIndex}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ACTIVITY_CAP = 100;
|
|
19
|
+
export const PREVIEW_CAP = 2000;
|
|
20
|
+
|
|
21
|
+
/** Human-readable preview of an agent's returned value, hard-capped. */
|
|
22
|
+
export function previewValue(value: unknown, cap = PREVIEW_CAP, truncationMarker = "…"): string {
|
|
23
|
+
let text: string;
|
|
24
|
+
if (typeof value === "string") {
|
|
25
|
+
text = value;
|
|
26
|
+
} else if (value === undefined) {
|
|
27
|
+
text = "(no value)";
|
|
28
|
+
} else {
|
|
29
|
+
try {
|
|
30
|
+
text = JSON.stringify(value, null, 2);
|
|
31
|
+
} catch {
|
|
32
|
+
text = String(value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return text.length > cap ? `${text.slice(0, cap)}${truncationMarker}` : text;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class AgentRecordStore {
|
|
39
|
+
// Keyed by (source, callIndex): a nested workflow() child restarts its own
|
|
40
|
+
// callIndex at 0, so callIndex alone would merge its agents into the parent's.
|
|
41
|
+
private byKey = new Map<string, AgentRecord>();
|
|
42
|
+
private order: string[] = [];
|
|
43
|
+
|
|
44
|
+
constructor(private readonly now: () => number = () => Date.now()) {}
|
|
45
|
+
|
|
46
|
+
/** Records in first-seen order (callIndex order for sequential scripts). */
|
|
47
|
+
list(): AgentRecord[] {
|
|
48
|
+
return this.order.map((key) => this.byKey.get(key) as AgentRecord);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get(callIndex: number, source?: string): AgentRecord | undefined {
|
|
52
|
+
return this.byKey.get(recordKey(callIndex, source));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
apply(event: RunProgressEvent): void {
|
|
56
|
+
switch (event.type) {
|
|
57
|
+
case "agentStart": {
|
|
58
|
+
const record = this.ensure(event);
|
|
59
|
+
record.status = "running";
|
|
60
|
+
record.startedAt ??= this.now();
|
|
61
|
+
record.prompt = event.prompt;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case "agentUpdate": {
|
|
65
|
+
const record = this.ensure(event);
|
|
66
|
+
if (event.model) record.model = event.model;
|
|
67
|
+
if (event.tool) {
|
|
68
|
+
record.activity.push(event.tool);
|
|
69
|
+
record.toolCalls++;
|
|
70
|
+
if (record.activity.length > ACTIVITY_CAP) record.activity.shift();
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "agentEnd": {
|
|
75
|
+
const record = this.ensure(event);
|
|
76
|
+
record.finishedAt = this.now();
|
|
77
|
+
if (event.prompt !== undefined) record.prompt ??= event.prompt;
|
|
78
|
+
if (event.tokens) record.tokens = event.tokens;
|
|
79
|
+
if (event.cost !== undefined) record.cost = event.cost;
|
|
80
|
+
if (event.preview !== undefined) record.outcome = event.preview;
|
|
81
|
+
if (event.replayed) {
|
|
82
|
+
record.status = "replayed";
|
|
83
|
+
} else if (event.text) {
|
|
84
|
+
record.status = "failed";
|
|
85
|
+
record.error = event.text;
|
|
86
|
+
} else {
|
|
87
|
+
record.status = "done";
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private ensure(event: AgentEvent): AgentRecord {
|
|
97
|
+
const key = recordKey(event.callIndex, event.source);
|
|
98
|
+
const record = this.byKey.get(key);
|
|
99
|
+
if (!record) {
|
|
100
|
+
const created: AgentRecord = {
|
|
101
|
+
callIndex: event.callIndex,
|
|
102
|
+
source: event.source,
|
|
103
|
+
label: event.label ?? `agent ${event.callIndex + 1}`,
|
|
104
|
+
phase: event.phase,
|
|
105
|
+
status: "running",
|
|
106
|
+
activity: [],
|
|
107
|
+
toolCalls: 0,
|
|
108
|
+
};
|
|
109
|
+
this.byKey.set(key, created);
|
|
110
|
+
this.order.push(key);
|
|
111
|
+
return created;
|
|
112
|
+
}
|
|
113
|
+
// Later events may carry the label/phase a bare agentUpdate lacked.
|
|
114
|
+
if (event.label) record.label = event.label;
|
|
115
|
+
if (event.phase && !record.phase) record.phase = event.phase;
|
|
116
|
+
return record;
|
|
117
|
+
}
|
|
118
|
+
}
|