@bastani/atomic 0.9.15 → 0.9.16-alpha.1

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 (82) hide show
  1. package/CHANGELOG.md +7 -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 +13 -0
  12. package/dist/builtin/subagents/README.md +23 -12
  13. package/dist/builtin/subagents/agents/worker.md +2 -2
  14. package/dist/builtin/subagents/package.json +1 -1
  15. package/dist/builtin/subagents/skills/subagent/SKILL.md +10 -17
  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 -2
  20. package/dist/builtin/subagents/src/extension/tool-rendering.ts +53 -0
  21. package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +1 -1
  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 -220
  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/tui/render-progress.ts +2 -2
  50. package/dist/builtin/subagents/src/tui/render-result-compact.ts +2 -2
  51. package/dist/builtin/subagents/src/tui/render-result.ts +38 -14
  52. package/dist/builtin/subagents/src/tui/render-status-progress.ts +1 -1
  53. package/dist/builtin/web-access/package.json +1 -1
  54. package/dist/builtin/workflows/CHANGELOG.md +10 -0
  55. package/dist/builtin/workflows/README.md +1 -1
  56. package/dist/builtin/workflows/package.json +1 -1
  57. package/dist/builtin/workflows/src/durable/dbos-embedded-postgres-root.ts +548 -39
  58. package/dist/builtin/workflows/src/durable/dbos-embedded-postgres.ts +529 -65
  59. package/dist/builtin/workflows/src/durable/dbos-lifecycle.ts +30 -8
  60. package/dist/builtin/workflows/src/durable/dbos-local-postgres.ts +36 -3
  61. package/dist/builtin/workflows/src/durable/local-command.ts +68 -10
  62. package/dist/builtin/workflows/src/extension/dispatcher.ts +7 -1
  63. package/dist/builtin/workflows/src/extension/extension-factory.ts +2 -2
  64. package/dist/builtin/workflows/src/extension/index.bundle.mjs +1094 -298
  65. package/dist/builtin/workflows/src/extension/public-types.ts +2 -2
  66. package/dist/builtin/workflows/src/extension/render-result.ts +16 -2
  67. package/dist/builtin/workflows/src/extension/runtime.ts +8 -1
  68. package/dist/builtin/workflows/src/extension/workflow-request-abort.ts +15 -0
  69. package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +4 -3
  70. package/dist/builtin/workflows/src/extension/workflow-tool-registration.ts +107 -13
  71. package/dist/builtin/workflows/src/extension/workflow-tool.ts +22 -12
  72. package/dist/builtin/workflows/src/tui/widget.ts +25 -6
  73. package/dist/core/event-bus.d.ts.map +1 -1
  74. package/dist/core/event-bus.js +19 -3
  75. package/dist/core/event-bus.js.map +1 -1
  76. package/docs/intercom.md +28 -26
  77. package/docs/subagents.md +15 -11
  78. package/docs/workflows.md +27 -10
  79. package/npm-shrinkwrap.json +32 -32
  80. package/package.json +3 -3
  81. package/dist/builtin/subagents/src/runs/inprocess/attempt-handles.ts +0 -104
  82. /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;
@@ -1 +1 @@
1
- {"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/core/event-bus.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,QAAQ;IACxB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3C,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAClE;AAED,MAAM,WAAW,kBAAmB,SAAQ,QAAQ;IACnD,KAAK,IAAI,IAAI,CAAC;CACd;AAED,wBAAgB,cAAc,IAAI,kBAAkB,CAqBnD;AASD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAE3E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D"}
1
+ {"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/core/event-bus.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,QAAQ;IACxB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3C,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAClE;AAED,MAAM,WAAW,kBAAmB,SAAQ,QAAQ;IACnD,KAAK,IAAI,IAAI,CAAC;CACd;AAED,wBAAgB,cAAc,IAAI,kBAAkB,CAqBnD;AA4BD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAE3E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D"}
@@ -22,18 +22,34 @@ export function createEventBus() {
22
22
  },
23
23
  };
24
24
  }
25
+ /**
26
+ * Shared by duplicate host-module instances in packaged/split-loader builds.
27
+ * The `@1` suffix versions the stored WeakMap shape.
28
+ */
29
+ const CANONICAL_EVENT_BUSES_KEY = Symbol.for("atomic-coding-agent/canonical-event-buses@1");
30
+ function canonicalEventBusBag() {
31
+ return globalThis;
32
+ }
25
33
  /**
26
34
  * Per-extension `events` facades mapped back to the canonical EventBus each
27
35
  * forwards to. Weak so neither facades nor buses are retained: an entry lives
28
36
  * exactly as long as the facade it keys on.
29
37
  */
30
- const canonicalEventBuses = new WeakMap();
38
+ function getCanonicalEventBuses() {
39
+ const bag = canonicalEventBusBag();
40
+ const existing = bag[CANONICAL_EVENT_BUSES_KEY];
41
+ if (existing !== undefined)
42
+ return existing;
43
+ const created = new WeakMap();
44
+ bag[CANONICAL_EVENT_BUSES_KEY] = created;
45
+ return created;
46
+ }
31
47
  /**
32
48
  * Register `facade` as forwarding to `bus`, so `canonicalEventBusFor(facade)`
33
49
  * resolves to `bus`.
34
50
  */
35
51
  export function registerCanonicalEventBus(facade, bus) {
36
- canonicalEventBuses.set(facade, bus);
52
+ getCanonicalEventBuses().set(facade, bus);
37
53
  }
38
54
  /**
39
55
  * Resolve a scope object to the canonical EventBus it forwards to. A
@@ -42,6 +58,6 @@ export function registerCanonicalEventBus(facade, bus) {
42
58
  * so canonical buses are fixed points.
43
59
  */
44
60
  export function canonicalEventBusFor(scope) {
45
- return canonicalEventBuses.get(scope) ?? scope;
61
+ return getCanonicalEventBuses().get(scope) ?? scope;
46
62
  }
47
63
  //# sourceMappingURL=event-bus.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"event-bus.js","sourceRoot":"","sources":["../../src/core/event-bus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAW3C,MAAM,UAAU,cAAc;IAC7B,MAAM,OAAO,GAAG,IAAI,YAAY,EAAE,CAAC;IACnC,OAAO;QACN,IAAI,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE;YACvB,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC7B,CAAC;QACD,EAAE,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE;YACxB,MAAM,WAAW,GAAG,KAAK,EAAE,IAAa,EAAE,EAAE;gBAC3C,IAAI,CAAC;oBACJ,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;gBACrB,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACd,OAAO,CAAC,KAAK,CAAC,wBAAwB,OAAO,IAAI,EAAE,GAAG,CAAC,CAAC;gBACzD,CAAC;YACF,CAAC,CAAC;YACF,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;YACjC,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;QAChD,CAAC;QACD,KAAK,EAAE,GAAG,EAAE;YACX,OAAO,CAAC,kBAAkB,EAAE,CAAC;QAC9B,CAAC;KACD,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,IAAI,OAAO,EAAkB,CAAC;AAE1D;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,MAAc,EAAE,GAAW;IACpE,mBAAmB,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACtC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAa;IACjD,OAAO,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC;AAChD,CAAC","sourcesContent":["import { EventEmitter } from \"node:events\";\n\nexport interface EventBus {\n\temit(channel: string, data: unknown): void;\n\ton(channel: string, handler: (data: unknown) => void): () => void;\n}\n\nexport interface EventBusController extends EventBus {\n\tclear(): void;\n}\n\nexport function createEventBus(): EventBusController {\n\tconst emitter = new EventEmitter();\n\treturn {\n\t\temit: (channel, data) => {\n\t\t\temitter.emit(channel, data);\n\t\t},\n\t\ton: (channel, handler) => {\n\t\t\tconst safeHandler = async (data: unknown) => {\n\t\t\t\ttry {\n\t\t\t\t\tawait handler(data);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(`Event handler error (${channel}):`, err);\n\t\t\t\t}\n\t\t\t};\n\t\t\temitter.on(channel, safeHandler);\n\t\t\treturn () => emitter.off(channel, safeHandler);\n\t\t},\n\t\tclear: () => {\n\t\t\temitter.removeAllListeners();\n\t\t},\n\t};\n}\n\n/**\n * Per-extension `events` facades mapped back to the canonical EventBus each\n * forwards to. Weak so neither facades nor buses are retained: an entry lives\n * exactly as long as the facade it keys on.\n */\nconst canonicalEventBuses = new WeakMap<object, object>();\n\n/**\n * Register `facade` as forwarding to `bus`, so `canonicalEventBusFor(facade)`\n * resolves to `bus`.\n */\nexport function registerCanonicalEventBus(facade: object, bus: object): void {\n\tcanonicalEventBuses.set(facade, bus);\n}\n\n/**\n * Resolve a scope object to the canonical EventBus it forwards to. A\n * registered per-extension `events` facade yields the shared bus behind it;\n * anything else — including a canonical bus itself — is returned unchanged,\n * so canonical buses are fixed points.\n */\nexport function canonicalEventBusFor(scope: object): object {\n\treturn canonicalEventBuses.get(scope) ?? scope;\n}\n"]}
1
+ {"version":3,"file":"event-bus.js","sourceRoot":"","sources":["../../src/core/event-bus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAW3C,MAAM,UAAU,cAAc;IAC7B,MAAM,OAAO,GAAG,IAAI,YAAY,EAAE,CAAC;IACnC,OAAO;QACN,IAAI,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE;YACvB,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC7B,CAAC;QACD,EAAE,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE;YACxB,MAAM,WAAW,GAAG,KAAK,EAAE,IAAa,EAAE,EAAE;gBAC3C,IAAI,CAAC;oBACJ,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;gBACrB,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACd,OAAO,CAAC,KAAK,CAAC,wBAAwB,OAAO,IAAI,EAAE,GAAG,CAAC,CAAC;gBACzD,CAAC;YACF,CAAC,CAAC;YACF,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;YACjC,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;QAChD,CAAC;QACD,KAAK,EAAE,GAAG,EAAE;YACX,OAAO,CAAC,kBAAkB,EAAE,CAAC;QAC9B,CAAC;KACD,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,yBAAyB,GAAG,MAAM,CAAC,GAAG,CAAC,6CAA6C,CAAC,CAAC;AAI5F,SAAS,oBAAoB;IAC5B,OAAO,UAAiF,CAAC;AAC1F,CAAC;AAED;;;;GAIG;AACH,SAAS,sBAAsB;IAC9B,MAAM,GAAG,GAAG,oBAAoB,EAAE,CAAC;IACnC,MAAM,QAAQ,GAAG,GAAG,CAAC,yBAAyB,CAAC,CAAC;IAChD,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IAC5C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAkB,CAAC;IAC9C,GAAG,CAAC,yBAAyB,CAAC,GAAG,OAAO,CAAC;IACzC,OAAO,OAAO,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,MAAc,EAAE,GAAW;IACpE,sBAAsB,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAa;IACjD,OAAO,sBAAsB,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC;AACrD,CAAC","sourcesContent":["import { EventEmitter } from \"node:events\";\n\nexport interface EventBus {\n\temit(channel: string, data: unknown): void;\n\ton(channel: string, handler: (data: unknown) => void): () => void;\n}\n\nexport interface EventBusController extends EventBus {\n\tclear(): void;\n}\n\nexport function createEventBus(): EventBusController {\n\tconst emitter = new EventEmitter();\n\treturn {\n\t\temit: (channel, data) => {\n\t\t\temitter.emit(channel, data);\n\t\t},\n\t\ton: (channel, handler) => {\n\t\t\tconst safeHandler = async (data: unknown) => {\n\t\t\t\ttry {\n\t\t\t\t\tawait handler(data);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(`Event handler error (${channel}):`, err);\n\t\t\t\t}\n\t\t\t};\n\t\t\temitter.on(channel, safeHandler);\n\t\t\treturn () => emitter.off(channel, safeHandler);\n\t\t},\n\t\tclear: () => {\n\t\t\temitter.removeAllListeners();\n\t\t},\n\t};\n}\n\n/**\n * Shared by duplicate host-module instances in packaged/split-loader builds.\n * The `@1` suffix versions the stored WeakMap shape.\n */\nconst CANONICAL_EVENT_BUSES_KEY = Symbol.for(\"atomic-coding-agent/canonical-event-buses@1\");\n\ntype CanonicalEventBuses = WeakMap<object, object>;\n\nfunction canonicalEventBusBag(): Record<symbol, CanonicalEventBuses | undefined> {\n\treturn globalThis as typeof globalThis & Record<symbol, CanonicalEventBuses | undefined>;\n}\n\n/**\n * Per-extension `events` facades mapped back to the canonical EventBus each\n * forwards to. Weak so neither facades nor buses are retained: an entry lives\n * exactly as long as the facade it keys on.\n */\nfunction getCanonicalEventBuses(): CanonicalEventBuses {\n\tconst bag = canonicalEventBusBag();\n\tconst existing = bag[CANONICAL_EVENT_BUSES_KEY];\n\tif (existing !== undefined) return existing;\n\tconst created = new WeakMap<object, object>();\n\tbag[CANONICAL_EVENT_BUSES_KEY] = created;\n\treturn created;\n}\n\n/**\n * Register `facade` as forwarding to `bus`, so `canonicalEventBusFor(facade)`\n * resolves to `bus`.\n */\nexport function registerCanonicalEventBus(facade: object, bus: object): void {\n\tgetCanonicalEventBuses().set(facade, bus);\n}\n\n/**\n * Resolve a scope object to the canonical EventBus it forwards to. A\n * registered per-extension `events` facade yields the shared bus behind it;\n * anything else — including a canonical bus itself — is returned unchanged,\n * so canonical buses are fixed points.\n */\nexport function canonicalEventBusFor(scope: object): object {\n\treturn getCanonicalEventBuses().get(scope) ?? scope;\n}\n"]}