@bastani/atomic 0.9.15 → 0.9.16-alpha.2

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.
Files changed (151) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +10 -0
  3. package/dist/builtin/intercom/README.md +8 -8
  4. package/dist/builtin/intercom/contact-supervisor-tool.ts +68 -32
  5. package/dist/builtin/intercom/index-heavy.ts +1 -0
  6. package/dist/builtin/intercom/index.ts +21 -15
  7. package/dist/builtin/intercom/intercom-tool.ts +66 -10
  8. package/dist/builtin/intercom/package.json +1 -1
  9. package/dist/builtin/intercom/parent-ask-handoff.ts +72 -0
  10. package/dist/builtin/mcp/package.json +1 -1
  11. package/dist/builtin/subagents/CHANGELOG.md +26 -0
  12. package/dist/builtin/subagents/README.md +37 -114
  13. package/dist/builtin/subagents/agents/worker.md +2 -2
  14. package/dist/builtin/subagents/package.json +1 -5
  15. package/dist/builtin/subagents/skills/subagent/SKILL.md +26 -103
  16. package/dist/builtin/subagents/src/extension/index.ts +5 -30
  17. package/dist/builtin/subagents/src/extension/notification-content.ts +1 -1
  18. package/dist/builtin/subagents/src/extension/schemas.ts +2 -14
  19. package/dist/builtin/subagents/src/extension/tool-description.ts +1 -4
  20. package/dist/builtin/subagents/src/extension/tool-rendering.ts +53 -0
  21. package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +1 -54
  22. package/dist/builtin/subagents/src/intercom/result-intercom.ts +4 -4
  23. package/dist/builtin/subagents/src/runs/foreground/execution-parent-ask-handoff.ts +78 -0
  24. package/dist/builtin/subagents/src/runs/foreground/inprocess-run-sync.ts +7 -5
  25. package/dist/builtin/subagents/src/runs/foreground/notify.ts +5 -6
  26. package/dist/builtin/subagents/src/runs/foreground/parent-ask-output.ts +48 -0
  27. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-burst-display.ts +46 -0
  28. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-burst.ts +403 -0
  29. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-context.ts +48 -9
  30. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-input.ts +3 -1
  31. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-live-update.ts +11 -0
  32. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +164 -93
  33. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel.ts +36 -29
  34. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parent-ask-projection.ts +11 -0
  35. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +42 -17
  36. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-status.ts +3 -121
  37. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-types.ts +7 -2
  38. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +30 -257
  39. package/dist/builtin/subagents/src/runs/inprocess/control-status.ts +0 -34
  40. package/dist/builtin/subagents/src/runs/inprocess/index.ts +0 -12
  41. package/dist/builtin/subagents/src/runs/inprocess/prompt-behavior.ts +1 -1
  42. package/dist/builtin/subagents/src/runs/inprocess/runner.ts +2 -141
  43. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +8 -0
  44. package/dist/builtin/subagents/src/shared/status-format.ts +3 -3
  45. package/dist/builtin/subagents/src/shared/types-config.ts +41 -0
  46. package/dist/builtin/subagents/src/shared/types-foreground-state.ts +7 -15
  47. package/dist/builtin/subagents/src/shared/types-results.ts +3 -1
  48. package/dist/builtin/subagents/src/shared/types-runtime.ts +1 -11
  49. package/dist/builtin/subagents/src/slash/slash-commands.ts +4 -468
  50. package/dist/builtin/subagents/src/tui/render-progress.ts +2 -2
  51. package/dist/builtin/subagents/src/tui/render-result-compact.ts +2 -2
  52. package/dist/builtin/subagents/src/tui/render-result.ts +38 -14
  53. package/dist/builtin/subagents/src/tui/render-status-progress.ts +1 -1
  54. package/dist/builtin/web-access/package.json +1 -1
  55. package/dist/builtin/workflows/CHANGELOG.md +16 -0
  56. package/dist/builtin/workflows/README.md +1 -1
  57. package/dist/builtin/workflows/package.json +1 -1
  58. package/dist/builtin/workflows/skills/bro/LICENSE.txt +21 -0
  59. package/dist/builtin/workflows/skills/bro/SKILL.md +14 -0
  60. package/dist/builtin/workflows/skills/how/LICENSE.txt +21 -0
  61. package/dist/builtin/workflows/skills/how/SKILL.md +162 -0
  62. package/dist/builtin/workflows/skills/how/references/critic-prompt.md +59 -0
  63. package/dist/builtin/workflows/skills/how/references/critique-rubric.md +58 -0
  64. package/dist/builtin/workflows/skills/how/references/explainer-prompt.md +55 -0
  65. package/dist/builtin/workflows/skills/how/references/explorer-prompt.md +52 -0
  66. package/dist/builtin/workflows/skills/teach/LICENSE.txt +21 -0
  67. package/dist/builtin/workflows/skills/teach/SKILL.md +28 -0
  68. package/dist/builtin/workflows/skills/unslop/LICENSE.txt +21 -0
  69. package/dist/builtin/workflows/skills/unslop/SKILL.md +87 -0
  70. package/dist/builtin/workflows/skills/why/LICENSE.txt +21 -0
  71. package/dist/builtin/workflows/skills/why/SKILL.md +267 -0
  72. package/dist/builtin/workflows/skills/why/references/epistemics.md +144 -0
  73. package/dist/builtin/workflows/skills/why/references/investigator-prompt.md +103 -0
  74. package/dist/builtin/workflows/skills/why/references/source-playbook.md +17 -0
  75. package/dist/builtin/workflows/skills/why/references/sources/code-archaeology.md +88 -0
  76. package/dist/builtin/workflows/skills/why/references/sources/databricks.md +70 -0
  77. package/dist/builtin/workflows/skills/why/references/sources/datadog.md +99 -0
  78. package/dist/builtin/workflows/skills/why/references/sources/incident-postmortem.md +15 -0
  79. package/dist/builtin/workflows/skills/why/references/sources/linear.md +48 -0
  80. package/dist/builtin/workflows/skills/why/references/sources/notion.md +55 -0
  81. package/dist/builtin/workflows/skills/why/references/sources/sentry.md +100 -0
  82. package/dist/builtin/workflows/skills/why/references/sources/slack.md +54 -0
  83. package/dist/builtin/workflows/skills/why/references/synthesizer-prompt.md +135 -0
  84. package/dist/builtin/workflows/src/durable/dbos-embedded-postgres-root.ts +548 -39
  85. package/dist/builtin/workflows/src/durable/dbos-embedded-postgres.ts +529 -65
  86. package/dist/builtin/workflows/src/durable/dbos-lifecycle.ts +30 -8
  87. package/dist/builtin/workflows/src/durable/dbos-local-postgres.ts +36 -3
  88. package/dist/builtin/workflows/src/durable/local-command.ts +68 -10
  89. package/dist/builtin/workflows/src/extension/companions.ts +1 -1
  90. package/dist/builtin/workflows/src/extension/dispatcher.ts +7 -1
  91. package/dist/builtin/workflows/src/extension/extension-factory.ts +2 -2
  92. package/dist/builtin/workflows/src/extension/index.bundle.mjs +1094 -298
  93. package/dist/builtin/workflows/src/extension/public-types.ts +2 -2
  94. package/dist/builtin/workflows/src/extension/render-result.ts +16 -2
  95. package/dist/builtin/workflows/src/extension/runtime.ts +8 -1
  96. package/dist/builtin/workflows/src/extension/workflow-request-abort.ts +15 -0
  97. package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +4 -3
  98. package/dist/builtin/workflows/src/extension/workflow-tool-registration.ts +107 -13
  99. package/dist/builtin/workflows/src/extension/workflow-tool.ts +22 -12
  100. package/dist/builtin/workflows/src/tui/widget.ts +25 -6
  101. package/dist/core/agent-session-auto-compaction.js +10 -10
  102. package/dist/core/agent-session-auto-compaction.js.map +1 -1
  103. package/dist/core/agent-session-events.d.ts.map +1 -1
  104. package/dist/core/agent-session-events.js +11 -13
  105. package/dist/core/agent-session-events.js.map +1 -1
  106. package/dist/core/agent-session-methods.d.ts +1 -1
  107. package/dist/core/agent-session-methods.d.ts.map +1 -1
  108. package/dist/core/agent-session-methods.js.map +1 -1
  109. package/dist/core/agent-session-models.d.ts.map +1 -1
  110. package/dist/core/agent-session-models.js +5 -7
  111. package/dist/core/agent-session-models.js.map +1 -1
  112. package/dist/core/agent-session-prompt.js +4 -4
  113. package/dist/core/agent-session-prompt.js.map +1 -1
  114. package/dist/core/agent-session-retry.d.ts +4 -4
  115. package/dist/core/agent-session-retry.d.ts.map +1 -1
  116. package/dist/core/agent-session-retry.js +8 -34
  117. package/dist/core/agent-session-retry.js.map +1 -1
  118. package/dist/core/agent-session-tool-hooks.js +4 -4
  119. package/dist/core/agent-session-tool-hooks.js.map +1 -1
  120. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  121. package/dist/core/atomic-guide-command.js +3 -10
  122. package/dist/core/atomic-guide-command.js.map +1 -1
  123. package/dist/core/event-bus.d.ts.map +1 -1
  124. package/dist/core/event-bus.js +19 -3
  125. package/dist/core/event-bus.js.map +1 -1
  126. package/dist/core/slash-commands.d.ts.map +1 -1
  127. package/dist/core/slash-commands.js +0 -3
  128. package/dist/core/slash-commands.js.map +1 -1
  129. package/dist/core/system-prompt.d.ts.map +1 -1
  130. package/dist/core/system-prompt.js +0 -12
  131. package/dist/core/system-prompt.js.map +1 -1
  132. package/dist/modes/interactive-engine/remote-command-catalog.d.ts +1 -1
  133. package/dist/modes/interactive-engine/remote-command-catalog.js +1 -1
  134. package/dist/modes/interactive-engine/remote-command-catalog.js.map +1 -1
  135. package/docs/intercom.md +29 -27
  136. package/docs/settings.md +2 -2
  137. package/docs/subagents.md +18 -15
  138. package/docs/usage.md +0 -9
  139. package/docs/workflows.md +27 -10
  140. package/npm-shrinkwrap.json +32 -32
  141. package/package.json +3 -3
  142. package/dist/builtin/subagents/prompts/gather-context-and-clarify.md +0 -27
  143. package/dist/builtin/subagents/prompts/parallel-cleanup.md +0 -61
  144. package/dist/builtin/subagents/prompts/parallel-context-build.md +0 -43
  145. package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +0 -47
  146. package/dist/builtin/subagents/prompts/parallel-research.md +0 -34
  147. package/dist/builtin/subagents/prompts/parallel-review.md +0 -47
  148. package/dist/builtin/subagents/prompts/review-loop.md +0 -39
  149. package/dist/builtin/subagents/src/extension/doctor.ts +0 -188
  150. package/dist/builtin/subagents/src/runs/inprocess/attempt-handles.ts +0 -104
  151. /package/dist/builtin/subagents/src/runs/foreground/{subagent-executor-resume.ts → subagent-executor-cwd.ts} +0 -0
@@ -5,7 +5,7 @@ import type { SessionManager } from "../shared/persistence-restore.js";
5
5
  import type { RunStatus, StageStatus } from "../shared/store-types.js";
6
6
  import type { WorkflowBudget, WorkflowInputValues } from "../shared/types.js";
7
7
  import type { WidgetFactory } from "../tui/store-widget-installer.js";
8
- import type { RenderResultOpts, WorkflowToolResult } from "./render-result.js";
8
+ import type { RenderResultOpts, WorkflowRegisteredToolResult } from "./render-result.js";
9
9
  import type { PiUISurface } from "./wiring.js";
10
10
 
11
11
  export type PiTheme = Record<string, string>;
@@ -231,4 +231,4 @@ export interface WorkflowToolArgs {
231
231
  reason?: string;
232
232
  }
233
233
 
234
- export type WorkflowExecuteToolResult = PiAgentToolResult<WorkflowToolResult>;
234
+ export type WorkflowExecuteToolResult = PiAgentToolResult<WorkflowRegisteredToolResult>;
@@ -197,6 +197,15 @@ export interface ModelCatalogEntry {
197
197
  }
198
198
  type ModelsResult = { action: "models"; models: ModelCatalogEntry[] };
199
199
 
200
+ export type WorkflowTimeoutResult = {
201
+ action: NonNullable<import("./public-types.js").WorkflowToolArgs["action"]>;
202
+ status: "failed";
203
+ code: "WORKFLOW_TIMEOUT";
204
+ timeoutMs: number;
205
+ runId?: string;
206
+ error: string;
207
+ };
208
+
200
209
  export type WorkflowToolResult =
201
210
  | ListResult
202
211
  | StatusResult
@@ -216,6 +225,8 @@ export type WorkflowToolResult =
216
225
  | ToolNodeControlResult
217
226
  | ModelsResult;
218
227
 
228
+ export type WorkflowRegisteredToolResult = WorkflowToolResult | WorkflowTimeoutResult;
229
+
219
230
  export interface RenderResultOpts {
220
231
  isPartial?: boolean;
221
232
  /**
@@ -310,7 +321,7 @@ function transcriptNoticeText(result: TranscriptResult): string {
310
321
  return entriesText;
311
322
  }
312
323
 
313
- export function renderResult(result: WorkflowToolResult, opts?: RenderResultOpts): string {
324
+ export function renderResult(result: WorkflowRegisteredToolResult | null | undefined, opts?: RenderResultOpts): string {
314
325
  const partial = opts?.isPartial === true;
315
326
  const themed = opts?.plain !== true;
316
327
 
@@ -319,9 +330,12 @@ export function renderResult(result: WorkflowToolResult, opts?: RenderResultOpts
319
330
  // paths that return content without a structured payload. Dereferencing a
320
331
  // missing `action` here previously threw and crashed the TUI render loop, so
321
332
  // degrade gracefully instead.
322
- if (result === null || typeof result !== "object" || typeof (result as { action?: unknown }).action !== "string") {
333
+ if (result === null || result === undefined || typeof result.action !== "string") {
323
334
  return partial ? "" : renderNotice("WORKFLOW", "no result", opts, themed);
324
335
  }
336
+ if ("code" in result && result.code === "WORKFLOW_TIMEOUT") {
337
+ return renderNotice("WORKFLOW TIMEOUT", result.error, opts, themed);
338
+ }
325
339
 
326
340
  switch (result.action) {
327
341
  case "list": {
@@ -40,6 +40,7 @@ import {
40
40
  releaseActiveBlockedClaim,
41
41
  } from "./runtime-active-block-claim.js";
42
42
  import { createDurableResumeRuntime, type DurableResumeRuntime } from "./runtime-durable-resume.js";
43
+ import { raceWorkflowRequestAbort } from "./workflow-request-abort.js";
43
44
 
44
45
  // ---------------------------------------------------------------------------
45
46
  // Options
@@ -122,6 +123,10 @@ export interface RuntimeDispatchOptions {
122
123
  readonly actor?: WorkflowActor;
123
124
  /** Run-level budget override used when a continuation is launched. */
124
125
  readonly budget?: WorkflowBudget;
126
+ /** Cancels only the public request/admission wait, never detached execution after acknowledgement. */
127
+ readonly signal?: AbortSignal;
128
+ /** Reports the exact detached identity before startup admission is awaited. */
129
+ readonly onRunAccepted?: (runId: string) => void;
125
130
  }
126
131
  // ---------------------------------------------------------------------------
127
132
  // Factory
@@ -397,7 +402,7 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
397
402
  },
398
403
 
399
404
  async dispatch(args: WorkflowToolArgs, options?: RuntimeDispatchOptions): Promise<WorkflowToolResult> {
400
- await ensureDbosReady();
405
+ await raceWorkflowRequestAbort(ensureDbosReady(), options?.signal);
401
406
  const defaultSessionDir = resolveDefaultStageSessionDir?.();
402
407
  return dispatch(args, {
403
408
  registry,
@@ -411,6 +416,8 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
411
416
  models,
412
417
  policy: options?.policy,
413
418
  ...(options?.origin === undefined ? {} : { origin: options.origin }),
419
+ ...(options?.signal === undefined ? {} : { signal: options.signal }),
420
+ ...(options?.onRunAccepted === undefined ? {} : { onRunAccepted: options.onRunAccepted }),
414
421
  cwd: runtimeCwd,
415
422
  ...(defaultSessionDir !== undefined ? { defaultSessionDir } : {}),
416
423
  });
@@ -0,0 +1,15 @@
1
+ export function raceWorkflowRequestAbort<T>(operation: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
2
+ if (signal === undefined) return operation;
3
+ if (signal.aborted) {
4
+ void operation.catch(() => {});
5
+ return Promise.reject(signal.reason ?? new DOMException("Workflow request aborted", "AbortError"));
6
+ }
7
+ const abort = Promise.withResolvers<never>();
8
+ const onAbort = (): void => {
9
+ abort.reject(signal.reason ?? new DOMException("Workflow request aborted", "AbortError"));
10
+ };
11
+ signal.addEventListener("abort", onAbort, { once: true });
12
+ return Promise.race([operation, abort.promise]).finally(() => {
13
+ signal.removeEventListener("abort", onAbort);
14
+ });
15
+ }
@@ -4,12 +4,12 @@ import type { RunSnapshot } from "../shared/store-types.js";
4
4
  import type { WorkflowSerializableValue } from "../shared/types.js";
5
5
  import { fmtDuration, statusIcon } from "../tui/status-helpers.js";
6
6
  import type { WorkflowToolArgs } from "./public-types.js";
7
- import type { WorkflowToolResult } from "./render-result.js";
7
+ import type { WorkflowRegisteredToolResult, WorkflowToolResult } from "./render-result.js";
8
8
  import type { ExtensionRuntime } from "./runtime.js";
9
9
  import type { WorkflowRunStatusSummary, WorkflowStatusAwaitingInput } from "./workflow-status-summary.js";
10
10
  import { getWorkflowStatusRenderRuns } from "./workflow-status-summary.js";
11
11
 
12
- function stringifyWorkflowToolResult(result: WorkflowToolResult): string {
12
+ function stringifyWorkflowToolResult(result: WorkflowRegisteredToolResult): string {
13
13
  return JSON.stringify(result, null, 2);
14
14
  }
15
15
 
@@ -212,8 +212,9 @@ function renderStageToolContent(result: Extract<WorkflowToolResult, { action: "s
212
212
  return lines.join("\n");
213
213
  }
214
214
 
215
- export function renderWorkflowToolContent(result: WorkflowToolResult, args: WorkflowToolArgs): string {
215
+ export function renderWorkflowToolContent(result: WorkflowRegisteredToolResult, args: WorkflowToolArgs): string {
216
216
  if (args.format === "json") return stringifyWorkflowToolResult(result);
217
+ if ("code" in result && result.code === "WORKFLOW_TIMEOUT") return stringifyWorkflowToolResult(result);
217
218
  switch (result.action) {
218
219
  case "transcript":
219
220
  return renderTranscriptToolContent(result);
@@ -1,35 +1,127 @@
1
- import type { ExtensionAPI, PiExecuteContext, WorkflowToolArgs } from "./public-types.js";
1
+ import type { ExtensionAPI, PiExecuteContext, PiToolOpts, WorkflowToolArgs } from "./public-types.js";
2
2
  import { renderCall } from "./render-call.js";
3
3
  import { dynamicTextRenderComponent } from "./render-component.js";
4
- import type { WorkflowToolResult } from "./render-result.js";
4
+ import type { WorkflowRegisteredToolResult, WorkflowTimeoutResult, WorkflowToolResult } from "./render-result.js";
5
5
  import { renderResult } from "./render-result.js";
6
6
  import { workflowPolicyFromContext } from "./workflow-policy.js";
7
7
  import { DEFAULT_PROMPT_GUIDANCE, WORKFLOW_TOOL_DESCRIPTION } from "./workflow-prompts.js";
8
+ import { raceWorkflowRequestAbort } from "./workflow-request-abort.js";
8
9
  import { WorkflowParametersSchema } from "./workflow-schema.js";
9
10
  import { renderWorkflowToolContent } from "./workflow-tool-content.js";
10
11
 
12
+ interface WorkflowToolRegistrationOptions {
13
+ /** Internal fixture seam; production registrations always use the fixed default. */
14
+ readonly requestTimeoutMs?: number;
15
+ }
16
+
17
+ export type WorkflowToolRegistrar = Pick<ExtensionAPI, "registerTool">;
18
+
19
+ export const WORKFLOW_TOOL_REQUEST_TIMEOUT_MS = 30_000;
20
+
21
+ type WorkflowToolExecutor = (
22
+ args: WorkflowToolArgs,
23
+ ctx: PiExecuteContext,
24
+ signal?: AbortSignal,
25
+ onRunAccepted?: (runId: string) => void,
26
+ ) => Promise<WorkflowToolResult>;
27
+
28
+ const MUTATING_WORKFLOW_ACTIONS = new Set<NonNullable<WorkflowToolArgs["action"]>>([
29
+ "reload",
30
+ "run",
31
+ "send",
32
+ "pause",
33
+ "resume",
34
+ "interrupt",
35
+ "quit",
36
+ ]);
37
+
38
+ function workflowToolTimeoutResult(
39
+ args: WorkflowToolArgs,
40
+ timeoutMs: number,
41
+ runId: string | undefined,
42
+ ): WorkflowTimeoutResult {
43
+ const action = args.action ?? "run";
44
+ const prefix = `Workflow ${action} request timed out after ${timeoutMs}ms.`;
45
+ return {
46
+ action,
47
+ status: "failed",
48
+ code: "WORKFLOW_TIMEOUT",
49
+ timeoutMs,
50
+ ...(runId === undefined ? {} : { runId }),
51
+ error: MUTATING_WORKFLOW_ACTIONS.has(action)
52
+ ? `${prefix} The outcome is unknown. Inspect workflow status before retrying.`
53
+ : prefix,
54
+ };
55
+ }
56
+
57
+ async function executeWithWorkflowToolDeadline(
58
+ executeWorkflowTool: WorkflowToolExecutor,
59
+ params: WorkflowToolArgs,
60
+ ctx: PiExecuteContext,
61
+ callerSignal: AbortSignal | undefined,
62
+ timeoutMs: number,
63
+ ): Promise<WorkflowRegisteredToolResult> {
64
+ callerSignal?.throwIfAborted();
65
+ const deadlineController = new AbortController();
66
+ const operationSignal =
67
+ callerSignal === undefined
68
+ ? deadlineController.signal
69
+ : AbortSignal.any([callerSignal, deadlineController.signal]);
70
+ let timer: ReturnType<typeof setTimeout> | undefined;
71
+ let acceptedRunId: string | undefined;
72
+ const timeout = new Promise<WorkflowRegisteredToolResult>((resolve) => {
73
+ timer = setTimeout(() => {
74
+ const result = workflowToolTimeoutResult(params, timeoutMs, acceptedRunId);
75
+ resolve(result);
76
+ deadlineController.abort(new Error(result.error));
77
+ }, timeoutMs);
78
+ });
79
+ try {
80
+ return await raceWorkflowRequestAbort(
81
+ Promise.race([
82
+ executeWorkflowTool(params, ctx, operationSignal, (runId) => {
83
+ acceptedRunId = runId;
84
+ }),
85
+ timeout,
86
+ ]),
87
+ callerSignal,
88
+ );
89
+ } finally {
90
+ if (timer !== undefined) clearTimeout(timer);
91
+ }
92
+ }
93
+
11
94
  export function registerWorkflowTool(
12
- pi: ExtensionAPI,
13
- executeWorkflowTool: (args: WorkflowToolArgs, ctx: PiExecuteContext) => Promise<WorkflowToolResult>,
95
+ pi: WorkflowToolRegistrar,
96
+ executeWorkflowTool: WorkflowToolExecutor,
14
97
  runWithLifecycleSuppressedForPolicy: <T>(
15
98
  policy: ReturnType<typeof workflowPolicyFromContext>,
16
99
  fn: () => Promise<T>,
17
100
  ) => Promise<T>,
18
- ): void {
19
- if (typeof pi.registerTool !== "function") return;
20
- pi.registerTool<WorkflowToolArgs, WorkflowToolResult>({
101
+ options: WorkflowToolRegistrationOptions = {},
102
+ ): PiToolOpts<WorkflowToolArgs, WorkflowRegisteredToolResult> | undefined {
103
+ if (typeof pi.registerTool !== "function") return undefined;
104
+ const tool: PiToolOpts<WorkflowToolArgs, WorkflowRegisteredToolResult> = {
21
105
  name: "workflow",
22
106
  label: "workflow",
23
107
  description: WORKFLOW_TOOL_DESCRIPTION,
24
108
  parameters: WorkflowParametersSchema,
25
109
  promptGuidelines: DEFAULT_PROMPT_GUIDANCE,
26
110
  renderShell: "self",
27
- execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
111
+ execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
28
112
  const policy = workflowPolicyFromContext(ctx);
29
- const details =
30
- (params.action ?? "run") === "run"
31
- ? await runWithLifecycleSuppressedForPolicy(policy, () => executeWorkflowTool(params, ctx))
32
- : await executeWorkflowTool(params, ctx);
113
+ const details = await executeWithWorkflowToolDeadline(
114
+ (actionParams, actionContext, operationSignal, onRunAccepted) =>
115
+ (actionParams.action ?? "run") === "run"
116
+ ? runWithLifecycleSuppressedForPolicy(policy, () =>
117
+ executeWorkflowTool(actionParams, actionContext, operationSignal, onRunAccepted),
118
+ )
119
+ : executeWorkflowTool(actionParams, actionContext, operationSignal, onRunAccepted),
120
+ params,
121
+ ctx,
122
+ signal,
123
+ options.requestTimeoutMs ?? WORKFLOW_TOOL_REQUEST_TIMEOUT_MS,
124
+ );
33
125
  return {
34
126
  content: [{ type: "text", text: renderWorkflowToolContent(details, params) }],
35
127
  details,
@@ -47,5 +139,7 @@ export function registerWorkflowTool(
47
139
  }),
48
140
  );
49
141
  },
50
- });
142
+ };
143
+ pi.registerTool(tool);
144
+ return tool;
51
145
  }
@@ -8,6 +8,7 @@ import type { ExtensionRuntime } from "./runtime.js";
8
8
  import { formatWorkflowResourceLoadWarning } from "./workflow-command-surfaces.js";
9
9
  import { workflowPolicyFromContext } from "./workflow-policy.js";
10
10
  import type { WorkflowReloadReport } from "./workflow-reload-report.js";
11
+ import { raceWorkflowRequestAbort } from "./workflow-request-abort.js";
11
12
  import { buildWorkflowStatusListing, setWorkflowStatusRenderRuns } from "./workflow-status-summary.js";
12
13
  import { isWorkflowStageToolContext, resolveRunId, topLevelExpandedSnapshots } from "./workflow-targets.js";
13
14
  import { workflowGetResult } from "./workflow-tool-content.js";
@@ -66,10 +67,17 @@ export function makeExecuteWorkflowTool(
66
67
  reloadWorkflowResources: () => Promise<WorkflowReloadReport | undefined> | undefined,
67
68
  ensureWorkflowResourcesLoaded: () => Promise<void> | void = () => {},
68
69
  sendDeps: WorkflowSendDeps = {},
69
- ): (args: WorkflowToolArgs, ctx: PiExecuteContext) => Promise<WorkflowToolResult> {
70
+ ): (
71
+ args: WorkflowToolArgs,
72
+ ctx: PiExecuteContext,
73
+ signal?: AbortSignal,
74
+ onRunAccepted?: (runId: string) => void,
75
+ ) => Promise<WorkflowToolResult> {
70
76
  return async function executeWorkflowTool(
71
77
  args: WorkflowToolArgs,
72
78
  ctx: PiExecuteContext,
79
+ signal?: AbortSignal,
80
+ onRunAccepted?: (runId: string) => void,
73
81
  ): Promise<WorkflowToolResult> {
74
82
  const action = args.action ?? "run";
75
83
  const runId = args.runId ?? "";
@@ -84,10 +92,12 @@ export function makeExecuteWorkflowTool(
84
92
  }
85
93
  const policy: WorkflowExecutionPolicy = workflowPolicyFromContext(ctx);
86
94
  const getRuntime = (): ExtensionRuntime => (typeof runtime === "function" ? runtime(ctx) : runtime);
95
+ const awaitRequest = <T>(operation: Promise<T>): Promise<T> => raceWorkflowRequestAbort(operation, signal);
87
96
  const ensureWorkflowResourcesVisible = async (): Promise<void> => {
88
97
  try {
89
- await ensureWorkflowResourcesLoaded();
98
+ await awaitRequest(Promise.resolve(ensureWorkflowResourcesLoaded()));
90
99
  } catch (error) {
100
+ if (signal?.aborted === true) throw signal.reason ?? error;
91
101
  ctx.ui?.notify?.(formatWorkflowResourceLoadWarning(error), "warning");
92
102
  }
93
103
  };
@@ -111,13 +121,13 @@ export function makeExecuteWorkflowTool(
111
121
  case "list":
112
122
  case "inputs": {
113
123
  await ensureWorkflowResourcesVisible();
114
- return getRuntime().dispatch(args, { policy });
124
+ return awaitRequest(getRuntime().dispatch(args, { policy, signal }));
115
125
  }
116
126
  case "run": {
117
127
  await ensureWorkflowResourcesVisible();
118
128
  // A tool launch is the agent's own action: it is attributed as such and
119
129
  // the tool result already reports the run, so it raises no chat notice.
120
- return getRuntime().dispatch(args, { policy, origin: "agent" });
130
+ return awaitRequest(getRuntime().dispatch(args, { policy, origin: "agent", signal, onRunAccepted }));
121
131
  }
122
132
  case "status": {
123
133
  const target = args.runId;
@@ -127,7 +137,7 @@ export function makeExecuteWorkflowTool(
127
137
  return { action: "statusDetail", runId: target, error: resolved.message };
128
138
  }
129
139
  if (resolved.kind === "not_found") {
130
- const durable = await getRuntime().inspectDurableWorkflow(target);
140
+ const durable = await awaitRequest(getRuntime().inspectDurableWorkflow(target));
131
141
  return durable.kind === "found"
132
142
  ? { action: "statusDetail", runId: target, detail: durable.detail }
133
143
  : { action: "statusDetail", runId: target, error: durable.message };
@@ -150,7 +160,7 @@ export function makeExecuteWorkflowTool(
150
160
  case "stages":
151
161
  case "stage":
152
162
  case "transcript": {
153
- const resolved = await resolveDurableInspectionSource(args, getRuntime());
163
+ const resolved = await awaitRequest(resolveDurableInspectionSource(args, getRuntime()));
154
164
  if (resolved.kind === "error") return durableInspectionError(action, args.runId ?? "", resolved.message);
155
165
  const source = resolved.kind === "durable" ? resolved.source : undefined;
156
166
  if (action === "stages") return workflowStagesResult(args, source);
@@ -158,17 +168,17 @@ export function makeExecuteWorkflowTool(
158
168
  return workflowTranscriptResult(args, source);
159
169
  }
160
170
  case "send":
161
- return workflowSendAction(args, sendDeps);
171
+ return awaitRequest(workflowSendAction(args, sendDeps));
162
172
  case "pause":
163
- return workflowPauseAction(args);
173
+ return awaitRequest(workflowPauseAction(args));
164
174
  case "reload":
165
- return workflowReloadAction(args, { reloadWorkflowResources });
175
+ return awaitRequest(workflowReloadAction(args, { reloadWorkflowResources }));
166
176
  case "quit":
167
- return workflowQuitAction(args);
177
+ return awaitRequest(workflowQuitAction(args));
168
178
  case "interrupt":
169
- return workflowInterruptAction(args);
179
+ return awaitRequest(workflowInterruptAction(args));
170
180
  case "resume":
171
- return workflowResumeAction(args, { getRuntime, policy, ensureWorkflowResourcesLoaded });
181
+ return awaitRequest(workflowResumeAction(args, { getRuntime, policy, ensureWorkflowResourcesLoaded }));
172
182
  default: {
173
183
  const _exhaustive: never = action;
174
184
  throw new Error(`Workflow extension: unknown action "${_exhaustive}"`);
@@ -6,7 +6,7 @@
6
6
  * badges (`✓ n ● n ○ n ✗ n`) in the title.
7
7
  * - One compact rounded card per run:
8
8
  * title: `<status glyph> <full id>`
9
- * row 1: `<name> · <dim mode · progress · duration>`
9
+ * row 1: `<name> · <dim mode · progress · live tool nodes · duration>`
10
10
  * - Collapsed single-line form below 80 cells:
11
11
  * `▾ N background · X ●` in dim+warning.
12
12
  *
@@ -215,6 +215,17 @@ function elapsedLabel(run: RunSnapshot, now: number): string {
215
215
  return "";
216
216
  }
217
217
 
218
+ function activeToolNodes(run: RunSnapshot): NonNullable<RunSnapshot["toolNodes"]> {
219
+ return (run.toolNodes ?? []).filter((node) => node.status === "pending" || node.status === "running");
220
+ }
221
+
222
+ function activeToolLabel(run: RunSnapshot): string | undefined {
223
+ const nodes = activeToolNodes(run);
224
+ if (nodes.length === 0) return undefined;
225
+ const details = nodes.map((node) => `${node.name} · ${node.status}`).join(", ");
226
+ return nodes.length === 1 ? details : `${nodes.length} tools · ${details}`;
227
+ }
228
+
218
229
  function metaLine(run: RunSnapshot, now: number): string {
219
230
  if (run.endedAt !== undefined) {
220
231
  return elapsedLabel(run, now);
@@ -224,6 +235,8 @@ function metaLine(run: RunSnapshot, now: number): string {
224
235
  const parts: string[] = [modeLabel(run)];
225
236
  const prog = progressLabel(run);
226
237
  if (prog) parts.push(prog);
238
+ const tools = activeToolLabel(run);
239
+ if (tools) parts.push(tools);
227
240
  const elapsed = elapsedLabel(run, now);
228
241
  if (elapsed) parts.push(elapsed);
229
242
  return parts.join(" · ");
@@ -306,7 +319,7 @@ function plainRunLines(run: RunSnapshot, now: number, allRuns: readonly RunSnaps
306
319
  // Collapsed (< 80 cell) form
307
320
  // ---------------------------------------------------------------------------
308
321
 
309
- function themedCollapsed(counts: RunCounts, theme: GraphTheme): string {
322
+ function themedCollapsed(counts: RunCounts, activeTools: number, theme: GraphTheme): string {
310
323
  const mauve = hexToAnsi(theme.mauve);
311
324
  const dim = hexToAnsi(theme.dim);
312
325
  const muted = hexToAnsi(theme.textMuted);
@@ -316,15 +329,18 @@ function themedCollapsed(counts: RunCounts, theme: GraphTheme): string {
316
329
  const paused = counts.paused > 0 ? `${dim} · ${RESET}${warning}${counts.paused} ❚❚${RESET}` : "";
317
330
  const quit = counts.quit > 0 ? `${dim} · ${RESET}${warning}${counts.quit} quit${RESET}` : "";
318
331
  const blocked = counts.blocked > 0 ? `${dim} · ${RESET}${warning}${counts.blocked} ↑${RESET}` : "";
319
- return ` ${mauve}▾${RESET} ${muted}${total} background${RESET}${dim} · ${RESET}${warning}${active} ●${RESET}${paused}${quit}${blocked}`;
332
+ const tools =
333
+ activeTools > 0 ? `${dim} · ${RESET}${warning}${activeTools} tool${activeTools === 1 ? "" : "s"}${RESET}` : "";
334
+ return ` ${mauve}▾${RESET} ${muted}${total} background${RESET}${dim} · ${RESET}${warning}${active} ●${RESET}${paused}${quit}${blocked}${tools}`;
320
335
  }
321
336
 
322
- function plainCollapsed(counts: RunCounts): string {
337
+ function plainCollapsed(counts: RunCounts, activeTools: number): string {
323
338
  const total = counts.active + counts.paused + counts.quit + counts.done + counts.blocked + counts.failed;
324
339
  const paused = counts.paused > 0 ? ` · ${counts.paused} ❚❚` : "";
325
340
  const quit = counts.quit > 0 ? ` · ${counts.quit} quit` : "";
326
341
  const blocked = counts.blocked > 0 ? ` · ${counts.blocked} ↑` : "";
327
- return ` ▾ ${total} background · ${counts.active} ●${paused}${quit}${blocked}`;
342
+ const tools = activeTools > 0 ? ` · ${activeTools} tool${activeTools === 1 ? "" : "s"}` : "";
343
+ return ` ▾ ${total} background · ${counts.active} ●${paused}${quit}${blocked}${tools}`;
328
344
  }
329
345
 
330
346
  // ---------------------------------------------------------------------------
@@ -369,10 +385,13 @@ export function buildThemedWidgetLines(
369
385
 
370
386
  const themed = piTheme !== undefined;
371
387
  const graphTheme = deriveGraphTheme({});
388
+ const activeTools = display.filter(isActive).reduce((total, run) => total + activeToolNodes(run).length, 0);
372
389
 
373
390
  // Collapsed single-line form for narrow terminals.
374
391
  if (width < COLLAPSED_BREAKPOINT_COLS) {
375
- return [themed ? themedCollapsed(visibleCounts, graphTheme) : plainCollapsed(visibleCounts)];
392
+ return [
393
+ themed ? themedCollapsed(visibleCounts, activeTools, graphTheme) : plainCollapsed(visibleCounts, activeTools),
394
+ ];
376
395
  }
377
396
 
378
397
  const total = display.length;
@@ -287,17 +287,17 @@ export function _schedulePostAutoCompactionContinuationProbe(_reason, willRetry)
287
287
  pending = new Promise((resolve) => {
288
288
  setTimeout(() => {
289
289
  void (async () => {
290
- const restoreIfOwned = async () => {
290
+ const settleIfOwned = async () => {
291
291
  if (fallbackScopeGeneration === undefined ||
292
292
  this._fallbackOriginGeneration !== fallbackScopeGeneration ||
293
- typeof this._restoreFallbackModel !== "function")
293
+ typeof this._settleFallbackModelScope !== "function")
294
294
  return;
295
295
  try {
296
- await this._restoreFallbackModel();
296
+ await this._settleFallbackModelScope();
297
297
  }
298
298
  catch {
299
- // A listener must not strand the continuation waiter. The model
300
- // state was already restored before lifecycle notifications ran.
299
+ // A listener must not strand the continuation waiter. The fallback
300
+ // state was already settled before lifecycle notifications ran.
301
301
  }
302
302
  };
303
303
  try {
@@ -314,12 +314,12 @@ export function _schedulePostAutoCompactionContinuationProbe(_reason, willRetry)
314
314
  if (this.isCompacting || this.isStreaming)
315
315
  return;
316
316
  if (!this.agent.hasQueuedMessages()) {
317
- await restoreIfOwned();
317
+ await settleIfOwned();
318
318
  return;
319
319
  }
320
- // A queued message starts the next user turn. Restore before
321
- // Agent snapshots the next request's model.
322
- await restoreIfOwned();
320
+ // A queued message starts the next user turn. Settle before Agent
321
+ // snapshots the next request's model.
322
+ await settleIfOwned();
323
323
  }
324
324
  if (this._pendingPostCompactionContinuation !== pending)
325
325
  return;
@@ -329,7 +329,7 @@ export function _schedulePostAutoCompactionContinuationProbe(_reason, willRetry)
329
329
  this._pendingPostCompactionContinuation = undefined;
330
330
  await this._resumeAfterAutoCompaction();
331
331
  if (willRetry && this._pendingPostCompactionContinuation === undefined) {
332
- await restoreIfOwned();
332
+ await settleIfOwned();
333
333
  }
334
334
  }
335
335
  finally {