@wichayutdew/pi-workflows 0.3.0 → 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.
- package/README.md +53 -39
- package/dist/index.js +5754 -4794
- package/package.json +1 -1
- package/src/command-names.ts +7 -1
- package/src/commands.ts +126 -52
- package/src/config/catalog.ts +162 -0
- package/src/config/ceiling.ts +116 -116
- package/src/config/command-conflicts.ts +4 -4
- package/src/config/diagnostics.ts +28 -0
- package/src/config/load-settings.ts +37 -0
- package/src/config/load-types.ts +53 -0
- package/src/config/load-workflows.ts +135 -0
- package/src/config/load.ts +43 -311
- package/src/config/types.ts +137 -135
- package/src/config/validate.ts +12 -1261
- package/src/config/validation/permissions.ts +291 -0
- package/src/config/validation/prompt.ts +19 -0
- package/src/config/validation/settings.ts +122 -0
- package/src/config/validation/shared.ts +129 -0
- package/src/config/validation/shortcut.ts +118 -0
- package/src/config/validation/step.ts +228 -0
- package/src/config/validation/subagent.ts +288 -0
- package/src/config/validation/workflow.ts +156 -0
- package/src/config/yaml.ts +29 -0
- package/src/digest.ts +38 -6
- package/src/engine/checkpoint.ts +13 -9
- package/src/engine/create-run.ts +42 -0
- package/src/engine/gate-transitions.ts +184 -0
- package/src/engine/reconciliation-history.ts +88 -0
- package/src/engine/resume.ts +19 -7
- package/src/engine/run-advance.ts +122 -0
- package/src/engine/run-lifecycle.ts +124 -0
- package/src/engine/run-reconciliation.ts +116 -0
- package/src/engine/run-validation.ts +128 -0
- package/src/engine/state-types.ts +64 -0
- package/src/engine/state.ts +10 -193
- package/src/engine/transition-helpers.ts +28 -0
- package/src/engine/transition-types.ts +8 -0
- package/src/engine/transitions.ts +17 -471
- package/src/harness/action-context.ts +181 -0
- package/src/harness/catalog.ts +45 -0
- package/src/harness/context-idle.ts +21 -0
- package/src/harness/core-actions.ts +299 -0
- package/src/harness/delegation-control-actions.ts +250 -0
- package/src/harness/delegation-failure.ts +215 -0
- package/src/harness/delegation-plan.ts +219 -0
- package/src/harness/delegation-recovery-validation.ts +159 -0
- package/src/harness/delegation-response-actions.ts +295 -0
- package/src/harness/delegation-retry-policy.ts +118 -0
- package/src/harness/dependencies.ts +165 -0
- package/src/harness/gate-submission-action.ts +154 -0
- package/src/harness/lifecycle-actions.ts +154 -0
- package/src/harness/pause-actions.ts +120 -0
- package/src/harness/plannotator-result-actions.ts +123 -0
- package/src/harness/prompt-gate-actions.ts +270 -0
- package/src/harness/resume-action.ts +261 -0
- package/src/harness/start-actions.ts +183 -0
- package/src/harness/status-actions.ts +191 -0
- package/src/harness/step-execution-actions.ts +283 -0
- package/src/harness/types.ts +76 -0
- package/src/harness.ts +236 -2303
- package/src/index.ts +73 -11
- package/src/integrations/plannotator-requests.ts +127 -0
- package/src/integrations/plannotator-responses.ts +141 -0
- package/src/integrations/plannotator-types.ts +44 -0
- package/src/integrations/plannotator.ts +16 -235
- package/src/integrations/prompt-gate.ts +17 -10
- package/src/integrations/subagents/child-policy-envelope.ts +119 -0
- package/src/integrations/subagents/child-policy-paths.ts +66 -0
- package/src/integrations/subagents/child-policy-sections.ts +174 -0
- package/src/integrations/subagents/child-policy-types.ts +32 -0
- package/src/integrations/subagents/child-policy-validation.ts +163 -0
- package/src/integrations/subagents/child-runtime-completion.ts +56 -0
- package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
- package/src/integrations/subagents/child-runtime-files.ts +156 -0
- package/src/integrations/subagents/child-runtime-policy.ts +83 -0
- package/src/integrations/subagents/child-runtime-types.ts +30 -0
- package/src/integrations/subagents/child-runtime.ts +140 -301
- package/src/integrations/subagents/client-delegation.ts +181 -0
- package/src/integrations/subagents/client-messages.ts +66 -0
- package/src/integrations/subagents/client-types.ts +36 -0
- package/src/integrations/subagents/client.ts +127 -219
- package/src/integrations/subagents/delegated-result.ts +31 -0
- package/src/integrations/subagents/diagnostic-format.ts +40 -0
- package/src/integrations/subagents/diagnostic-text.ts +114 -0
- package/src/integrations/subagents/diagnostic-types.ts +77 -0
- package/src/integrations/subagents/diagnostics.ts +24 -977
- package/src/integrations/subagents/failure-correlation.ts +235 -0
- package/src/integrations/subagents/failure-transcript.ts +192 -0
- package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
- package/src/integrations/subagents/protocol-events.ts +27 -0
- package/src/integrations/subagents/protocol.ts +30 -375
- package/src/integrations/subagents/replay-audit.ts +147 -0
- package/src/integrations/subagents/replay-safety.ts +82 -0
- package/src/integrations/subagents/session-diagnostics.ts +258 -0
- package/src/policy/approved-command-extraction.ts +71 -0
- package/src/policy/approved-commands.ts +15 -432
- package/src/policy/bash-authorization.ts +86 -0
- package/src/policy/bash-read-only.ts +161 -0
- package/src/policy/bash-types.ts +20 -0
- package/src/policy/bash.ts +4 -346
- package/src/policy/completion-batch.ts +21 -16
- package/src/policy/immutable-input.ts +5 -2
- package/src/policy/mcp-authorization.ts +71 -0
- package/src/policy/restricted-command.ts +141 -0
- package/src/policy/restricted-git.ts +30 -0
- package/src/policy/reviewed-artifact.ts +97 -0
- package/src/policy/reviewed-command-safety.ts +150 -0
- package/src/policy/reviewed-command-shape.ts +49 -0
- package/src/policy/reviewed-repository-contract.ts +138 -0
- package/src/policy/reviewed-repository-cwd.ts +102 -0
- package/src/policy/tool-call-authorization.ts +58 -0
- package/src/policy/tool-selection.ts +72 -0
- package/src/policy/tool-types.ts +14 -0
- package/src/policy/tools.ts +11 -150
- package/src/preflight.ts +96 -61
- package/src/prompt/main-workflow-notice.ts +41 -0
- package/src/prompt/retry-task.ts +83 -0
- package/src/prompt/step-contract.ts +101 -0
- package/src/prompt/step-sections.ts +75 -0
- package/src/prompt/step-task.ts +156 -0
- package/src/prompt/template.ts +81 -0
- package/src/prompt.ts +10 -254
- package/src/runtime/completion-tool.ts +3 -0
- package/src/runtime/main-step-completion.ts +78 -0
- package/src/runtime/main-step-lifecycle.ts +37 -0
- package/src/runtime/main-step-policy.ts +84 -0
- package/src/runtime/main-step-runtime-types.ts +64 -0
- package/src/runtime/main-step-runtime.ts +129 -200
- package/src/runtime/main-step-state.ts +85 -0
- package/src/runtime/serial-task-queue.ts +48 -13
- package/src/runtime/step-result.ts +48 -27
- package/src/workflow-list.ts +15 -6
- package/src/workflow-status/format-status.ts +52 -0
- package/src/workflow-status/formatting.ts +178 -0
- package/src/workflow-status/layout.ts +109 -0
- package/src/workflow-status/render-board.ts +101 -0
- package/src/workflow-status/render-path.ts +123 -0
- package/src/workflow-status/render-summary.ts +144 -0
- package/src/workflow-status/types.ts +40 -0
- package/src/workflow-status/view.ts +273 -0
- 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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return
|
|
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
|
+
};
|