@wichayutdew/pi-workflows 0.2.3 → 1.0.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.
Files changed (142) hide show
  1. package/README.md +66 -38
  2. package/dist/index.js +5903 -4782
  3. package/package.json +1 -1
  4. package/src/command-names.ts +7 -1
  5. package/src/commands.ts +126 -52
  6. package/src/config/catalog.ts +162 -0
  7. package/src/config/ceiling.ts +116 -116
  8. package/src/config/command-conflicts.ts +4 -4
  9. package/src/config/diagnostics.ts +28 -0
  10. package/src/config/load-settings.ts +37 -0
  11. package/src/config/load-types.ts +53 -0
  12. package/src/config/load-workflows.ts +135 -0
  13. package/src/config/load.ts +43 -311
  14. package/src/config/types.ts +137 -135
  15. package/src/config/validate.ts +12 -1261
  16. package/src/config/validation/permissions.ts +291 -0
  17. package/src/config/validation/prompt.ts +19 -0
  18. package/src/config/validation/settings.ts +122 -0
  19. package/src/config/validation/shared.ts +129 -0
  20. package/src/config/validation/shortcut.ts +118 -0
  21. package/src/config/validation/step.ts +228 -0
  22. package/src/config/validation/subagent.ts +288 -0
  23. package/src/config/validation/workflow.ts +156 -0
  24. package/src/config/yaml.ts +29 -0
  25. package/src/digest.ts +38 -6
  26. package/src/engine/checkpoint.ts +13 -9
  27. package/src/engine/create-run.ts +42 -0
  28. package/src/engine/gate-transitions.ts +184 -0
  29. package/src/engine/reconciliation-history.ts +88 -0
  30. package/src/engine/resume.ts +19 -7
  31. package/src/engine/run-advance.ts +122 -0
  32. package/src/engine/run-lifecycle.ts +124 -0
  33. package/src/engine/run-reconciliation.ts +116 -0
  34. package/src/engine/run-validation.ts +128 -0
  35. package/src/engine/state-types.ts +64 -0
  36. package/src/engine/state.ts +10 -193
  37. package/src/engine/transition-helpers.ts +28 -0
  38. package/src/engine/transition-types.ts +8 -0
  39. package/src/engine/transitions.ts +17 -471
  40. package/src/harness/action-context.ts +181 -0
  41. package/src/harness/catalog.ts +45 -0
  42. package/src/harness/context-idle.ts +21 -0
  43. package/src/harness/core-actions.ts +299 -0
  44. package/src/harness/delegation-control-actions.ts +250 -0
  45. package/src/harness/delegation-failure.ts +215 -0
  46. package/src/harness/delegation-plan.ts +219 -0
  47. package/src/harness/delegation-recovery-validation.ts +159 -0
  48. package/src/harness/delegation-response-actions.ts +295 -0
  49. package/src/harness/delegation-retry-policy.ts +118 -0
  50. package/src/harness/dependencies.ts +165 -0
  51. package/src/harness/gate-submission-action.ts +154 -0
  52. package/src/harness/lifecycle-actions.ts +154 -0
  53. package/src/harness/pause-actions.ts +120 -0
  54. package/src/harness/plannotator-result-actions.ts +123 -0
  55. package/src/harness/prompt-gate-actions.ts +270 -0
  56. package/src/harness/resume-action.ts +261 -0
  57. package/src/harness/start-actions.ts +183 -0
  58. package/src/harness/status-actions.ts +191 -0
  59. package/src/harness/step-execution-actions.ts +283 -0
  60. package/src/harness/types.ts +76 -0
  61. package/src/harness.ts +237 -2201
  62. package/src/index.ts +73 -11
  63. package/src/integrations/plannotator-requests.ts +127 -0
  64. package/src/integrations/plannotator-responses.ts +141 -0
  65. package/src/integrations/plannotator-types.ts +44 -0
  66. package/src/integrations/plannotator.ts +16 -235
  67. package/src/integrations/prompt-gate.ts +17 -10
  68. package/src/integrations/subagents/child-policy-envelope.ts +119 -0
  69. package/src/integrations/subagents/child-policy-paths.ts +66 -0
  70. package/src/integrations/subagents/child-policy-sections.ts +174 -0
  71. package/src/integrations/subagents/child-policy-types.ts +32 -0
  72. package/src/integrations/subagents/child-policy-validation.ts +163 -0
  73. package/src/integrations/subagents/child-runtime-completion.ts +56 -0
  74. package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
  75. package/src/integrations/subagents/child-runtime-files.ts +156 -0
  76. package/src/integrations/subagents/child-runtime-policy.ts +83 -0
  77. package/src/integrations/subagents/child-runtime-types.ts +30 -0
  78. package/src/integrations/subagents/child-runtime.ts +140 -301
  79. package/src/integrations/subagents/client-delegation.ts +181 -0
  80. package/src/integrations/subagents/client-messages.ts +66 -0
  81. package/src/integrations/subagents/client-types.ts +36 -0
  82. package/src/integrations/subagents/client.ts +127 -219
  83. package/src/integrations/subagents/delegated-result.ts +31 -0
  84. package/src/integrations/subagents/diagnostic-format.ts +40 -0
  85. package/src/integrations/subagents/diagnostic-text.ts +114 -0
  86. package/src/integrations/subagents/diagnostic-types.ts +77 -0
  87. package/src/integrations/subagents/diagnostics.ts +24 -799
  88. package/src/integrations/subagents/failure-correlation.ts +235 -0
  89. package/src/integrations/subagents/failure-transcript.ts +192 -0
  90. package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
  91. package/src/integrations/subagents/protocol-events.ts +27 -0
  92. package/src/integrations/subagents/protocol.ts +30 -375
  93. package/src/integrations/subagents/replay-audit.ts +147 -0
  94. package/src/integrations/subagents/replay-safety.ts +82 -0
  95. package/src/integrations/subagents/session-diagnostics.ts +258 -0
  96. package/src/policy/approved-command-extraction.ts +71 -0
  97. package/src/policy/approved-commands.ts +15 -432
  98. package/src/policy/bash-authorization.ts +86 -0
  99. package/src/policy/bash-read-only.ts +161 -0
  100. package/src/policy/bash-types.ts +20 -0
  101. package/src/policy/bash.ts +4 -346
  102. package/src/policy/completion-batch.ts +21 -16
  103. package/src/policy/immutable-input.ts +5 -2
  104. package/src/policy/mcp-authorization.ts +71 -0
  105. package/src/policy/restricted-command.ts +141 -0
  106. package/src/policy/restricted-git.ts +30 -0
  107. package/src/policy/reviewed-artifact.ts +97 -0
  108. package/src/policy/reviewed-command-safety.ts +150 -0
  109. package/src/policy/reviewed-command-shape.ts +49 -0
  110. package/src/policy/reviewed-repository-contract.ts +138 -0
  111. package/src/policy/reviewed-repository-cwd.ts +102 -0
  112. package/src/policy/tool-call-authorization.ts +58 -0
  113. package/src/policy/tool-selection.ts +72 -0
  114. package/src/policy/tool-types.ts +14 -0
  115. package/src/policy/tools.ts +11 -150
  116. package/src/preflight.ts +96 -61
  117. package/src/prompt/main-workflow-notice.ts +41 -0
  118. package/src/prompt/retry-task.ts +83 -0
  119. package/src/prompt/step-contract.ts +101 -0
  120. package/src/prompt/step-sections.ts +75 -0
  121. package/src/prompt/step-task.ts +156 -0
  122. package/src/prompt/template.ts +81 -0
  123. package/src/prompt.ts +10 -245
  124. package/src/runtime/completion-tool.ts +3 -0
  125. package/src/runtime/main-step-completion.ts +78 -0
  126. package/src/runtime/main-step-lifecycle.ts +37 -0
  127. package/src/runtime/main-step-policy.ts +84 -0
  128. package/src/runtime/main-step-runtime-types.ts +64 -0
  129. package/src/runtime/main-step-runtime.ts +129 -200
  130. package/src/runtime/main-step-state.ts +85 -0
  131. package/src/runtime/serial-task-queue.ts +48 -13
  132. package/src/runtime/step-result.ts +48 -27
  133. package/src/workflow-list.ts +15 -6
  134. package/src/workflow-status/format-status.ts +52 -0
  135. package/src/workflow-status/formatting.ts +178 -0
  136. package/src/workflow-status/layout.ts +109 -0
  137. package/src/workflow-status/render-board.ts +101 -0
  138. package/src/workflow-status/render-path.ts +123 -0
  139. package/src/workflow-status/render-summary.ts +144 -0
  140. package/src/workflow-status/types.ts +40 -0
  141. package/src/workflow-status/view.ts +273 -0
  142. package/src/workflow-status.ts +20 -820
@@ -0,0 +1,181 @@
1
+ import {
2
+ parseDelegationResponse,
3
+ parseDelegationUpdate,
4
+ requestIdOf,
5
+ } from './client-messages.ts';
6
+ import type {
7
+ ActiveDelegation,
8
+ DelegateOptions,
9
+ DelegationTimer,
10
+ SubagentDelegationClientDependencies,
11
+ SubagentEventBus,
12
+ } from './client-types.ts';
13
+ import {
14
+ SUBAGENT_DELEGATION_CANCEL_EVENT,
15
+ SUBAGENT_DELEGATION_PROTOCOL_VERSION,
16
+ SUBAGENT_DELEGATION_REQUEST_EVENT,
17
+ SUBAGENT_DELEGATION_RESPONSE_EVENT,
18
+ SUBAGENT_DELEGATION_STARTED_EVENT,
19
+ SUBAGENT_DELEGATION_UPDATE_EVENT,
20
+ } from './protocol-events.ts';
21
+ import type {
22
+ SubagentDelegationRequest,
23
+ SubagentDelegationResponse,
24
+ } from './protocol-events.ts';
25
+
26
+ export const DEFAULT_CLIENT_DEPENDENCIES = {
27
+ scheduleTimeout: (callback: () => void, timeoutMs: number) =>
28
+ setTimeout(callback, timeoutMs),
29
+ cancelTimeout: (timer: DelegationTimer) => {
30
+ clearTimeout(timer);
31
+ },
32
+ } as const satisfies SubagentDelegationClientDependencies;
33
+
34
+ type CreatedDelegation = {
35
+ readonly active: ActiveDelegation;
36
+ readonly promise: Promise<SubagentDelegationResponse>;
37
+ readonly start: () => void;
38
+ };
39
+
40
+ type CreateDelegationOptions = {
41
+ readonly events: SubagentEventBus;
42
+ readonly request: SubagentDelegationRequest;
43
+ readonly options: DelegateOptions;
44
+ readonly dependencies: SubagentDelegationClientDependencies;
45
+ readonly releaseActive: (requestId: string) => void;
46
+ };
47
+
48
+ function isUnsubscribe(value: unknown): value is () => void {
49
+ return typeof value === 'function';
50
+ }
51
+
52
+ /**
53
+ * Creates one delegation lifecycle without retaining client-level state.
54
+ */
55
+ export const createDelegation = ({
56
+ events,
57
+ request,
58
+ options,
59
+ dependencies,
60
+ releaseActive,
61
+ }: CreateDelegationOptions): CreatedDelegation => {
62
+ let requestCancellation = (): void => undefined;
63
+ let resolveTerminal = (): void => undefined;
64
+ const terminal = new Promise<void>((resolve) => {
65
+ resolveTerminal = resolve;
66
+ });
67
+
68
+ const promise = new Promise<SubagentDelegationResponse>((resolve, reject) => {
69
+ let isSettled = false;
70
+ let isCancellationRequested = false;
71
+ const subscriptions: Array<() => void> = [];
72
+
73
+ const subscribe = (
74
+ event: string,
75
+ handler: (data: unknown) => void,
76
+ ): void => {
77
+ const unsubscribe = events.on(event, handler);
78
+ if (isUnsubscribe(unsubscribe)) {
79
+ subscriptions.push(() => {
80
+ unsubscribe();
81
+ });
82
+ }
83
+ };
84
+ const stopLocalWatchers = (): void => {
85
+ dependencies.cancelTimeout(startTimer);
86
+ dependencies.cancelTimeout(overallTimer);
87
+ options.signal?.removeEventListener('abort', abort);
88
+ };
89
+ const cleanup = (): void => {
90
+ stopLocalWatchers();
91
+ for (const unsubscribe of subscriptions) unsubscribe();
92
+ releaseActive(request.requestId);
93
+ };
94
+ const finish = (
95
+ result:
96
+ | { readonly response: SubagentDelegationResponse }
97
+ | { readonly error: Error },
98
+ ): void => {
99
+ if (isSettled) return;
100
+ isSettled = true;
101
+ cleanup();
102
+ if ('response' in result) resolve(result.response);
103
+ else reject(result.error);
104
+ };
105
+ const emitCancel = (): void => {
106
+ if (isCancellationRequested || isSettled) return;
107
+ isCancellationRequested = true;
108
+ events.emit(SUBAGENT_DELEGATION_CANCEL_EVENT, {
109
+ version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
110
+ requestId: request.requestId,
111
+ });
112
+ };
113
+ const failAndCancel = (reason: string): void => {
114
+ if (isSettled) return;
115
+ emitCancel();
116
+ // A local timeout is not proof that the child process terminated.
117
+ // Retain correlation listeners and ownership until the terminal event.
118
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- emitting cancellation can synchronously settle through the event bus
119
+ if (isSettled) return;
120
+ isSettled = true;
121
+ stopLocalWatchers();
122
+ reject(new Error(reason));
123
+ };
124
+ const abort = (): void => {
125
+ failAndCancel('subagent delegation was cancelled');
126
+ };
127
+ requestCancellation = emitCancel;
128
+
129
+ subscribe(SUBAGENT_DELEGATION_STARTED_EVENT, (data) => {
130
+ if (requestIdOf(data) !== request.requestId) return;
131
+ dependencies.cancelTimeout(startTimer);
132
+ });
133
+ subscribe(SUBAGENT_DELEGATION_UPDATE_EVENT, (data) => {
134
+ const update = parseDelegationUpdate(data);
135
+ if (!update || update.requestId !== request.requestId) return;
136
+ options.onUpdate?.(update);
137
+ });
138
+ subscribe(SUBAGENT_DELEGATION_RESPONSE_EVENT, (data) => {
139
+ const response = parseDelegationResponse(data);
140
+ if (!response || response.requestId !== request.requestId) return;
141
+ resolveTerminal();
142
+ if (isSettled) {
143
+ cleanup();
144
+ options.onLateTerminal?.(response);
145
+ return;
146
+ }
147
+ finish({ response });
148
+ });
149
+
150
+ const startTimer = dependencies.scheduleTimeout(() => {
151
+ failAndCancel(
152
+ 'pi-subagents did not accept the delegation request; verify it is installed and loaded',
153
+ );
154
+ }, options.startTimeoutMs ?? 3_000);
155
+ const overallTimer = dependencies.scheduleTimeout(
156
+ () => {
157
+ failAndCancel(
158
+ 'pi-subagents did not settle the delegation request before its deadline',
159
+ );
160
+ },
161
+ (request.timeoutMs ?? 900_000) + 5_000,
162
+ );
163
+ startTimer.unref();
164
+ overallTimer.unref();
165
+ options.signal?.addEventListener('abort', abort, { once: true });
166
+ });
167
+
168
+ return {
169
+ active: {
170
+ requestId: request.requestId,
171
+ requestCancellation: () => {
172
+ requestCancellation();
173
+ },
174
+ terminal,
175
+ },
176
+ promise,
177
+ start: () => {
178
+ events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, request);
179
+ },
180
+ };
181
+ };
@@ -0,0 +1,66 @@
1
+ import { SUBAGENT_DELEGATION_PROTOCOL_VERSION } from './protocol-events.ts';
2
+ import type {
3
+ SubagentDelegationResponse,
4
+ SubagentDelegationStatus,
5
+ SubagentDelegationUpdate,
6
+ } from './protocol-events.ts';
7
+
8
+ const DELEGATION_STATUSES: ReadonlySet<string> = new Set([
9
+ 'completed',
10
+ 'failed',
11
+ 'timed_out',
12
+ 'cancelled',
13
+ 'interrupted',
14
+ 'turn_budget_exhausted',
15
+ 'tool_budget_exhausted',
16
+ 'structured_output_failed',
17
+ 'acceptance_failed',
18
+ 'invalid_request',
19
+ 'unavailable_context',
20
+ ]);
21
+
22
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
23
+ value !== null && typeof value === 'object' && !Array.isArray(value);
24
+
25
+ const isDelegationStatus = (value: string): value is SubagentDelegationStatus =>
26
+ DELEGATION_STATUSES.has(value);
27
+
28
+ const isDelegationResponse = (
29
+ value: unknown,
30
+ ): value is SubagentDelegationResponse =>
31
+ isRecord(value) &&
32
+ value.version === SUBAGENT_DELEGATION_PROTOCOL_VERSION &&
33
+ typeof value.requestId === 'string' &&
34
+ typeof value.status === 'string' &&
35
+ isDelegationStatus(value.status);
36
+
37
+ const isDelegationUpdate = (
38
+ value: unknown,
39
+ ): value is SubagentDelegationUpdate =>
40
+ isRecord(value) &&
41
+ value.version === SUBAGENT_DELEGATION_PROTOCOL_VERSION &&
42
+ typeof value.requestId === 'string';
43
+
44
+ /**
45
+ * Reads a request identifier from an untrusted event payload.
46
+ */
47
+ export const requestIdOf = (value: unknown): string | undefined =>
48
+ isRecord(value) && typeof value.requestId === 'string'
49
+ ? value.requestId
50
+ : undefined;
51
+
52
+ /**
53
+ * Validates an untrusted terminal delegation response.
54
+ */
55
+ export const parseDelegationResponse = (
56
+ value: unknown,
57
+ ): SubagentDelegationResponse | undefined =>
58
+ isDelegationResponse(value) ? value : undefined;
59
+
60
+ /**
61
+ * Validates an untrusted delegation progress update.
62
+ */
63
+ export const parseDelegationUpdate = (
64
+ value: unknown,
65
+ ): SubagentDelegationUpdate | undefined =>
66
+ isDelegationUpdate(value) ? value : undefined;
@@ -0,0 +1,36 @@
1
+ import type {
2
+ SubagentDelegationResponse,
3
+ SubagentDelegationUpdate,
4
+ } from './protocol-events.ts';
5
+
6
+ export type SubagentEventBus = {
7
+ readonly on: (event: string, handler: (data: unknown) => void) => unknown;
8
+ readonly emit: (event: string, data: unknown) => void;
9
+ };
10
+
11
+ export type DelegateOptions = {
12
+ readonly signal?: AbortSignal;
13
+ readonly startTimeoutMs?: number;
14
+ readonly onUpdate?: (update: SubagentDelegationUpdate) => void;
15
+ /**
16
+ * Called when a terminal response arrives after the delegate promise already
17
+ * rejected locally. The child was still potentially alive until this event.
18
+ */
19
+ readonly onLateTerminal?: (response: SubagentDelegationResponse) => void;
20
+ };
21
+
22
+ export type DelegationTimer = ReturnType<typeof setTimeout>;
23
+
24
+ export type SubagentDelegationClientDependencies = {
25
+ readonly scheduleTimeout: (
26
+ callback: () => void,
27
+ timeoutMs: number,
28
+ ) => DelegationTimer;
29
+ readonly cancelTimeout: (timer: DelegationTimer) => void;
30
+ };
31
+
32
+ export type ActiveDelegation = {
33
+ readonly requestId: string;
34
+ readonly requestCancellation: () => void;
35
+ readonly terminal: Promise<void>;
36
+ };
@@ -1,240 +1,148 @@
1
1
  import {
2
- SUBAGENT_DELEGATION_CANCEL_EVENT,
3
- SUBAGENT_DELEGATION_PROTOCOL_VERSION,
4
- SUBAGENT_DELEGATION_REQUEST_EVENT,
5
- SUBAGENT_DELEGATION_RESPONSE_EVENT,
6
- SUBAGENT_DELEGATION_STARTED_EVENT,
7
- SUBAGENT_DELEGATION_UPDATE_EVENT,
8
- type SubagentDelegationRequest,
9
- type SubagentDelegationResponse,
10
- type SubagentDelegationStatus,
11
- type SubagentDelegationUpdate,
12
- } from './protocol.ts';
2
+ createDelegation,
3
+ DEFAULT_CLIENT_DEPENDENCIES,
4
+ } from './client-delegation.ts';
5
+ import type {
6
+ ActiveDelegation,
7
+ DelegateOptions,
8
+ SubagentDelegationClientDependencies,
9
+ SubagentEventBus,
10
+ } from './client-types.ts';
11
+ import type {
12
+ SubagentDelegationRequest,
13
+ SubagentDelegationResponse,
14
+ } from './protocol-events.ts';
15
+
16
+ export type {
17
+ DelegateOptions,
18
+ SubagentDelegationClientDependencies,
19
+ SubagentEventBus,
20
+ } from './client-types.ts';
21
+
22
+ /**
23
+ * Functional surface used by workflow orchestration.
24
+ */
25
+ export type SubagentDelegationClientController = {
26
+ /** Request currently owned by this client, if any. */
27
+ readonly activeRequestId: string | undefined;
28
+ /** Starts one correlated delegation. */
29
+ readonly delegate: (
30
+ request: SubagentDelegationRequest,
31
+ options?: DelegateOptions,
32
+ ) => Promise<SubagentDelegationResponse>;
33
+ /** Cancels the active delegation and waits for terminal confirmation. */
34
+ readonly cancelActiveAndWait: (waitMs?: number) => Promise<boolean>;
35
+ };
36
+
37
+ /**
38
+ * Creates an isolated subagent delegation controller.
39
+ *
40
+ * @param events - Event transport shared with the subagent runtime.
41
+ * @param dependencies - Timer effects used for cancellation and timeouts.
42
+ * @returns A controller that owns at most one active delegation.
43
+ */
44
+ export function createSubagentDelegationClient(
45
+ events: SubagentEventBus,
46
+ dependencies: SubagentDelegationClientDependencies = DEFAULT_CLIENT_DEPENDENCIES,
47
+ ): SubagentDelegationClientController {
48
+ let active: ActiveDelegation | undefined;
49
+
50
+ const delegate = (
51
+ request: SubagentDelegationRequest,
52
+ options: DelegateOptions = {},
53
+ ): Promise<SubagentDelegationResponse> => {
54
+ if (active) {
55
+ return Promise.reject(
56
+ new Error(`subagent request "${active.requestId}" is still active`),
57
+ );
58
+ }
59
+ if (options.signal?.aborted) {
60
+ return Promise.reject(new Error('subagent delegation was cancelled'));
61
+ }
13
62
 
14
- export interface SubagentEventBus {
15
- on(event: string, handler: (data: unknown) => void): (() => void) | void;
16
- emit(event: string, data: unknown): void;
17
- }
63
+ const delegation = createDelegation({
64
+ events,
65
+ request,
66
+ options,
67
+ dependencies,
68
+ releaseActive: (requestId) => {
69
+ if (active?.requestId === requestId) active = undefined;
70
+ },
71
+ });
72
+ active = delegation.active;
73
+ delegation.start();
74
+ return delegation.promise;
75
+ };
18
76
 
19
- export interface DelegateOptions {
20
- signal?: AbortSignal;
21
- startTimeoutMs?: number;
22
- onUpdate?: (update: SubagentDelegationUpdate) => void;
23
- /**
24
- * Called when a terminal response arrives after the delegate promise already
25
- * rejected locally. The child was still potentially alive until this event.
26
- */
27
- onLateTerminal?: (response: SubagentDelegationResponse) => void;
28
- }
77
+ const cancelActiveAndWait = (waitMs = 5_000): Promise<boolean> => {
78
+ const current = active;
79
+ if (!current) return Promise.resolve(true);
29
80
 
30
- interface ActiveDelegation {
31
- requestId: string;
32
- requestCancellation: () => void;
33
- terminal: Promise<void>;
81
+ current.requestCancellation();
82
+ return new Promise<boolean>((resolve) => {
83
+ let isFinished = false;
84
+ const finish = (isConfirmed: boolean): void => {
85
+ if (isFinished) return;
86
+ isFinished = true;
87
+ dependencies.cancelTimeout(timer);
88
+ resolve(isConfirmed);
89
+ };
90
+ const timer = dependencies.scheduleTimeout(() => {
91
+ finish(false);
92
+ }, waitMs);
93
+ void current.terminal.then(() => {
94
+ finish(true);
95
+ });
96
+ });
97
+ };
98
+
99
+ return {
100
+ get activeRequestId() {
101
+ return active?.requestId;
102
+ },
103
+ delegate,
104
+ cancelActiveAndWait,
105
+ };
34
106
  }
35
107
 
36
- const DELEGATION_STATUSES = new Set<SubagentDelegationStatus>([
37
- 'completed',
38
- 'failed',
39
- 'timed_out',
40
- 'cancelled',
41
- 'interrupted',
42
- 'turn_budget_exhausted',
43
- 'tool_budget_exhausted',
44
- 'structured_output_failed',
45
- 'acceptance_failed',
46
- 'invalid_request',
47
- 'unavailable_context',
48
- ]);
49
-
50
- function requestIdOf(value: unknown): string | undefined {
51
- if (value === null || typeof value !== 'object') return undefined;
52
- const requestId = (value as { requestId?: unknown }).requestId;
53
- return typeof requestId === 'string' ? requestId : undefined;
54
- }
108
+ /**
109
+ * Coordinates one foreground pi-subagents request at a time.
110
+ *
111
+ * Event and timer boundaries are constructor-injected; production defaults
112
+ * preserve the original runtime behavior.
113
+ */
114
+ export class SubagentDelegationClient implements SubagentDelegationClientController {
115
+ readonly #controller: SubagentDelegationClientController;
55
116
 
56
- function parseResponse(value: unknown): SubagentDelegationResponse | undefined {
57
- if (value === null || typeof value !== 'object') return undefined;
58
- const response = value as Partial<SubagentDelegationResponse>;
59
- if (
60
- response.version !== SUBAGENT_DELEGATION_PROTOCOL_VERSION ||
61
- typeof response.requestId !== 'string' ||
62
- typeof response.status !== 'string' ||
63
- !DELEGATION_STATUSES.has(response.status as SubagentDelegationStatus)
117
+ /**
118
+ * Creates a client over the supplied event and timer boundaries.
119
+ */
120
+ constructor(
121
+ events: SubagentEventBus,
122
+ dependencies: SubagentDelegationClientDependencies = DEFAULT_CLIENT_DEPENDENCIES,
64
123
  ) {
65
- return undefined;
66
- }
67
- return response as SubagentDelegationResponse;
68
- }
69
-
70
- function parseUpdate(value: unknown): SubagentDelegationUpdate | undefined {
71
- if (value === null || typeof value !== 'object') return undefined;
72
- const update = value as Partial<SubagentDelegationUpdate>;
73
- if (
74
- update.version !== SUBAGENT_DELEGATION_PROTOCOL_VERSION ||
75
- typeof update.requestId !== 'string'
76
- ) {
77
- return undefined;
78
- }
79
- return update as SubagentDelegationUpdate;
80
- }
81
-
82
- export class SubagentDelegationClient {
83
- private readonly events: SubagentEventBus;
84
- private active: ActiveDelegation | undefined;
85
-
86
- constructor(events: SubagentEventBus) {
87
- this.events = events;
124
+ this.#controller = createSubagentDelegationClient(events, dependencies);
88
125
  }
89
126
 
127
+ /** Returns the request currently owned by this client, if any. */
90
128
  get activeRequestId(): string | undefined {
91
- return this.active?.requestId;
129
+ return this.#controller.activeRequestId;
92
130
  }
93
131
 
132
+ /**
133
+ * Starts a correlated delegation and resolves on its terminal event.
134
+ */
94
135
  delegate(
95
136
  request: SubagentDelegationRequest,
96
137
  options: DelegateOptions = {},
97
138
  ): Promise<SubagentDelegationResponse> {
98
- if (this.active) {
99
- return Promise.reject(
100
- new Error(
101
- `subagent request "${this.active.requestId}" is still active`,
102
- ),
103
- );
104
- }
105
- if (options.signal?.aborted) {
106
- return Promise.reject(new Error('subagent delegation was cancelled'));
107
- }
108
-
109
- let start!: () => void;
110
- let requestCancellation!: () => void;
111
- let resolveTerminal!: () => void;
112
- const terminal = new Promise<void>((resolve) => {
113
- resolveTerminal = resolve;
114
- });
115
- const delegation = new Promise<SubagentDelegationResponse>(
116
- (resolve, reject) => {
117
- let settled = false;
118
- let cancellationRequested = false;
119
- const subscriptions: Array<() => void> = [];
120
- const startTimeoutMs = options.startTimeoutMs ?? 3_000;
121
- const overallTimeoutMs = (request.timeoutMs ?? 900_000) + 5_000;
122
-
123
- const subscribe = (
124
- event: string,
125
- handler: (data: unknown) => void,
126
- ): void => {
127
- const unsubscribe = this.events.on(event, handler);
128
- if (typeof unsubscribe === 'function')
129
- subscriptions.push(unsubscribe);
130
- };
131
- const stopLocalWatchers = (): void => {
132
- clearTimeout(startTimer);
133
- clearTimeout(overallTimer);
134
- options.signal?.removeEventListener('abort', abort);
135
- };
136
- const cleanup = (): void => {
137
- stopLocalWatchers();
138
- for (const unsubscribe of subscriptions) unsubscribe();
139
- if (this.active?.requestId === request.requestId)
140
- this.active = undefined;
141
- };
142
- const finish = (
143
- result: { response: SubagentDelegationResponse } | { error: Error },
144
- ): void => {
145
- if (settled) return;
146
- settled = true;
147
- cleanup();
148
- if ('response' in result) resolve(result.response);
149
- else reject(result.error);
150
- };
151
- const emitCancel = (): void => {
152
- if (cancellationRequested || settled) return;
153
- cancellationRequested = true;
154
- this.events.emit(SUBAGENT_DELEGATION_CANCEL_EVENT, {
155
- version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
156
- requestId: request.requestId,
157
- });
158
- };
159
- const failAndCancel = (reason: string): void => {
160
- if (settled) return;
161
- emitCancel();
162
- // A local timeout is not proof that the child process terminated. Reject
163
- // the caller, but retain correlation listeners and active ownership
164
- // until pi-subagents emits the terminal response.
165
- if (settled) return;
166
- settled = true;
167
- stopLocalWatchers();
168
- reject(new Error(reason));
169
- };
170
- const abort = (): void => {
171
- failAndCancel('subagent delegation was cancelled');
172
- };
173
- requestCancellation = emitCancel;
174
-
175
- subscribe(SUBAGENT_DELEGATION_STARTED_EVENT, (data) => {
176
- if (requestIdOf(data) !== request.requestId) return;
177
- clearTimeout(startTimer);
178
- });
179
- subscribe(SUBAGENT_DELEGATION_UPDATE_EVENT, (data) => {
180
- const update = parseUpdate(data);
181
- if (!update || update.requestId !== request.requestId) return;
182
- options.onUpdate?.(update);
183
- });
184
- subscribe(SUBAGENT_DELEGATION_RESPONSE_EVENT, (data) => {
185
- const response = parseResponse(data);
186
- if (!response || response.requestId !== request.requestId) return;
187
- resolveTerminal();
188
- if (settled) {
189
- cleanup();
190
- options.onLateTerminal?.(response);
191
- return;
192
- }
193
- finish({ response });
194
- });
195
-
196
- const startTimer = setTimeout(() => {
197
- failAndCancel(
198
- 'pi-subagents did not accept the delegation request; verify it is installed and loaded',
199
- );
200
- }, startTimeoutMs);
201
- const overallTimer = setTimeout(() => {
202
- failAndCancel(
203
- 'pi-subagents did not settle the delegation request before its deadline',
204
- );
205
- }, overallTimeoutMs);
206
- startTimer.unref?.();
207
- overallTimer.unref?.();
208
-
209
- options.signal?.addEventListener('abort', abort, { once: true });
210
- start = () =>
211
- this.events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, request);
212
- },
213
- );
214
-
215
- this.active = {
216
- requestId: request.requestId,
217
- requestCancellation,
218
- terminal,
219
- };
220
- start();
221
- return delegation;
139
+ return this.#controller.delegate(request, options);
222
140
  }
223
141
 
224
- async cancelActiveAndWait(waitMs = 5_000): Promise<boolean> {
225
- const active = this.active;
226
- if (!active) return true;
227
- active.requestCancellation();
228
- return new Promise<boolean>((resolve) => {
229
- let finished = false;
230
- const finish = (confirmed: boolean): void => {
231
- if (finished) return;
232
- finished = true;
233
- clearTimeout(timer);
234
- resolve(confirmed);
235
- };
236
- const timer = setTimeout(() => finish(false), waitMs);
237
- void active.terminal.then(() => finish(true));
238
- });
142
+ /**
143
+ * Requests cancellation and waits briefly for terminal confirmation.
144
+ */
145
+ cancelActiveAndWait(waitMs = 5_000): Promise<boolean> {
146
+ return this.#controller.cancelActiveAndWait(waitMs);
239
147
  }
240
148
  }
@@ -0,0 +1,31 @@
1
+ import { parseWorkflowStepResult } from '../../runtime/step-result.ts';
2
+ import type { WorkflowStepResult } from '../../runtime/step-result.ts';
3
+ import type { ChildStepPolicy } from './child-policy-types.ts';
4
+
5
+ export type DelegatedStepResult = WorkflowStepResult;
6
+
7
+ /**
8
+ * Validates a child completion value against its delegated step policy.
9
+ *
10
+ * @throws When the completion value violates the delegated step contract.
11
+ */
12
+ export const parseDelegatedStepResult = (
13
+ value: unknown,
14
+ policy: ChildStepPolicy,
15
+ ): DelegatedStepResult => {
16
+ try {
17
+ return parseWorkflowStepResult(value, {
18
+ policyDigest: policy.policyDigest,
19
+ outcomes: [...policy.outcomes],
20
+ summaryMaxChars: policy.summaryMaxChars,
21
+ ...(policy.gateSubmitOutcome
22
+ ? { gateSubmitOutcome: policy.gateSubmitOutcome }
23
+ : {}),
24
+ });
25
+ } catch (error) {
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ throw new Error(message.replaceAll('workflow step', 'delegated step'), {
28
+ cause: error,
29
+ });
30
+ }
31
+ };