@wichayutdew/pi-workflows 2.5.0 → 2.6.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 +4 -1
- package/dist/index.js +1133 -2409
- package/examples/starter-kit/agents/planner.md +7 -0
- package/examples/starter-kit/agents/reviewer.md +6 -0
- package/examples/starter-kit/agents/scout.md +6 -0
- package/examples/starter-kit/agents/worker.md +7 -0
- package/examples/starter-kit/agents/workspace-preparer.md +6 -0
- package/examples/starter-kit/investigate.workflow.yaml +114 -0
- package/examples/starter-kit/mr-comment.workflow.yaml +114 -54
- package/examples/starter-kit/mr-review.workflow.yaml +77 -56
- package/examples/starter-kit/settings.yaml +3 -0
- package/examples/starter-kit/steps/investigate/investigate.md +77 -0
- package/examples/starter-kit/steps/investigate/retrieve.md +63 -0
- package/examples/starter-kit/steps/investigate/validate.md +46 -0
- package/examples/starter-kit/steps/mr-comment/checkout-source.md +67 -0
- package/examples/starter-kit/steps/mr-comment/fetch.md +34 -26
- package/examples/starter-kit/steps/mr-comment/implement.md +34 -27
- package/examples/starter-kit/steps/mr-comment/plan.md +54 -40
- package/examples/starter-kit/steps/mr-comment/publish.md +28 -27
- package/examples/starter-kit/steps/mr-comment/verify.md +42 -35
- package/examples/starter-kit/steps/mr-review/fetch.md +52 -22
- package/examples/starter-kit/steps/mr-review/publish-approved.md +49 -0
- package/examples/starter-kit/steps/mr-review/review-for-approval.md +142 -0
- package/examples/starter-kit/steps/mr-review/verify-published.md +37 -0
- package/examples/starter-kit/steps/shared/prepare-workspace.md +91 -73
- package/examples/starter-kit/steps/shared/publish-remote.md +45 -0
- package/examples/starter-kit/steps/ticket/implement.md +59 -25
- package/examples/starter-kit/steps/ticket/plan.md +145 -72
- package/examples/starter-kit/steps/ticket/verify.md +80 -45
- package/examples/starter-kit/steps/work/implement.md +60 -24
- package/examples/starter-kit/steps/work/plan.md +129 -57
- package/examples/starter-kit/steps/work/verify.md +58 -22
- package/examples/starter-kit/ticket.workflow.yaml +75 -36
- package/examples/starter-kit/work.workflow.yaml +63 -33
- package/package.json +3 -16
- package/schemas/workflow.schema.json +2 -15
- package/src/agents/profile.ts +98 -0
- package/src/config/ceiling.ts +0 -82
- package/src/config/types.ts +6 -53
- package/src/config/validation/settings.ts +2 -14
- package/src/config/validation/step.ts +12 -8
- package/src/config/validation/workflow.ts +1 -10
- package/src/engine/run-workflow-validation.ts +0 -3
- package/src/engine/state-types.ts +1 -1
- package/src/harness/action-context.ts +1 -13
- package/src/harness/delegation-control-actions.ts +4 -63
- package/src/harness/delegation-plan.ts +26 -80
- package/src/harness/delegation-response-actions.ts +22 -135
- package/src/harness/dependencies.ts +1 -12
- package/src/harness/status-actions.ts +1 -0
- package/src/harness/step-execution-actions.ts +2 -9
- package/src/harness/types.ts +2 -40
- package/src/harness.ts +2 -17
- package/src/index.ts +4 -9
- package/src/integrations/subagents/child-policy-validation.ts +6 -7
- package/src/integrations/subagents/child-runtime-dependencies.ts +1 -1
- package/src/integrations/subagents/child-runtime-policy.ts +1 -7
- package/src/integrations/subagents/child-runtime.ts +18 -9
- package/src/integrations/subagents/client.ts +240 -98
- package/src/integrations/subagents/protocol-events.ts +32 -15
- package/src/integrations/subagents/protocol.ts +1 -1
- package/src/preflight.ts +0 -8
- package/src/prompt/main-workflow-notice.ts +8 -15
- package/src/prompt/step-task.ts +7 -6
- package/src/workflow-status/format-status.ts +5 -1
- package/src/workflow-status/render-step-detail.ts +94 -0
- package/src/workflow-status/types.ts +1 -0
- package/src/workflow-status/view.ts +49 -14
- package/agents/step.md +0 -32
- package/examples/mr-comments.workflow.yaml +0 -125
- package/examples/prompts/mr-comments/implement.md +0 -17
- package/examples/prompts/mr-comments/inspect.md +0 -5
- package/examples/prompts/mr-comments/plan.md +0 -54
- package/examples/prompts/mr-comments/verify.md +0 -9
- package/examples/settings.yaml +0 -27
- package/examples/starter-kit/steps/mr-review/publish.md +0 -48
- package/examples/starter-kit/steps/mr-review/review.md +0 -76
- package/examples/starter-kit/steps/mr-review/verify.md +0 -35
- package/src/config/validation/subagent.ts +0 -288
- package/src/harness/delegation-failure.ts +0 -248
- package/src/harness/delegation-recovery-validation.ts +0 -161
- package/src/harness/delegation-retry-policy.ts +0 -120
- package/src/integrations/subagents/client-delegation.ts +0 -181
- package/src/integrations/subagents/client-messages.ts +0 -66
- package/src/integrations/subagents/client-types.ts +0 -36
- package/src/integrations/subagents/diagnostic-format.ts +0 -45
- package/src/integrations/subagents/diagnostic-text.ts +0 -114
- package/src/integrations/subagents/diagnostic-types.ts +0 -83
- package/src/integrations/subagents/diagnostics.ts +0 -26
- package/src/integrations/subagents/failure-correlation.ts +0 -285
- package/src/integrations/subagents/failure-transcript.ts +0 -251
- package/src/integrations/subagents/hidden-bash-failure.ts +0 -98
- package/src/integrations/subagents/replay-audit.ts +0 -146
- package/src/integrations/subagents/replay-safety.ts +0 -67
- package/src/integrations/subagents/session-diagnostics.ts +0 -357
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { AGENT_PROFILE_NAME_PATTERN } from '../../config/types.ts';
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_CHILD_POLICY_ENVIRONMENT,
|
|
5
5
|
isSafeStepCapabilityPath,
|
|
@@ -61,11 +61,10 @@ const requiredString = (
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Returns whether a name follows the runtime naming contract used by
|
|
64
|
-
*
|
|
64
|
+
* workflow agent profiles.
|
|
65
65
|
*/
|
|
66
|
-
export const
|
|
67
|
-
name
|
|
68
|
-
): name is string => Boolean(name && SUBAGENT_RUNTIME_NAME_PATTERN.test(name));
|
|
66
|
+
export const isAgentProfileName = (name: string | undefined): name is string =>
|
|
67
|
+
Boolean(name && AGENT_PROFILE_NAME_PATTERN.test(name));
|
|
69
68
|
|
|
70
69
|
const rejectUnknownProperties = (
|
|
71
70
|
value: Readonly<Record<string, unknown>>,
|
|
@@ -115,8 +114,8 @@ const parseIdentityAndPaths = (
|
|
|
115
114
|
if (!POLICY_DIGEST_PATTERN.test(policyDigest)) {
|
|
116
115
|
throw new Error('child policy digest is invalid');
|
|
117
116
|
}
|
|
118
|
-
if (!
|
|
119
|
-
throw new Error('child policy agent is not a valid
|
|
117
|
+
if (!isAgentProfileName(agent)) {
|
|
118
|
+
throw new Error('child policy agent is not a valid agent profile name');
|
|
120
119
|
}
|
|
121
120
|
if (!CAPABILITY_TOKEN_PATTERN.test(capabilityToken)) {
|
|
122
121
|
throw new Error('child policy capability token is invalid');
|
|
@@ -41,7 +41,7 @@ export const DEFAULT_CHILD_RUNTIME_DEPENDENCIES = {
|
|
|
41
41
|
createUniqueId: randomUUID,
|
|
42
42
|
currentWorkingDirectory: () => process.cwd(),
|
|
43
43
|
environmentChildAgent: () =>
|
|
44
|
-
process.env.
|
|
44
|
+
process.env.PI_WORKFLOWS_CHILD_AGENT?.trim() || undefined,
|
|
45
45
|
temporaryDirectory: tmpdir,
|
|
46
46
|
tokensAreEqual,
|
|
47
47
|
} as const satisfies SubagentChildRuntimeDependencies;
|
|
@@ -8,13 +8,7 @@ import type { ChildStepPolicy } from './child-policy-types.ts';
|
|
|
8
8
|
export const childPolicyStep = (policy: ChildStepPolicy): WorkflowStep => ({
|
|
9
9
|
title: policy.stepTitle,
|
|
10
10
|
prompt: { inline: 'Delegated workflow step' },
|
|
11
|
-
|
|
12
|
-
agent: policy.agent,
|
|
13
|
-
context: 'fresh',
|
|
14
|
-
timeoutMs: 900_000,
|
|
15
|
-
artifacts: false,
|
|
16
|
-
retryToolFailures: false,
|
|
17
|
-
},
|
|
11
|
+
agent: { name: policy.agent },
|
|
18
12
|
permissions: policy.permissions,
|
|
19
13
|
requires: { tools: [], extensions: [], skills: [] },
|
|
20
14
|
transitions: {},
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
InputEvent,
|
|
4
4
|
InputEventResult,
|
|
5
5
|
} from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import { Type } from 'typebox';
|
|
6
7
|
import { invalidCompletionCallIds } from '../../policy/completion-batch.ts';
|
|
7
8
|
import { freezeToolInput } from '../../policy/immutable-input.ts';
|
|
8
9
|
import { authorizeToolCall, resolveActiveTools } from '../../policy/tools.ts';
|
|
@@ -24,7 +25,6 @@ import type {
|
|
|
24
25
|
} from './child-runtime-types.ts';
|
|
25
26
|
import { extractChildPolicy } from './child-policy-envelope.ts';
|
|
26
27
|
import type { ChildStepPolicy } from './child-policy-types.ts';
|
|
27
|
-
import { isSubagentRuntimeName } from './child-policy-validation.ts';
|
|
28
28
|
|
|
29
29
|
export { CHILD_COMPLETION_TOOL } from './child-runtime-completion.ts';
|
|
30
30
|
export type {
|
|
@@ -85,6 +85,21 @@ export const registerSubagentChildRuntime = (
|
|
|
85
85
|
const childAgent = resolveChildAgent(options, dependencies);
|
|
86
86
|
let state = INITIAL_STATE;
|
|
87
87
|
|
|
88
|
+
pi.registerTool({
|
|
89
|
+
name: CHILD_COMPLETION_TOOL,
|
|
90
|
+
label: 'Complete Workflow Step',
|
|
91
|
+
description: 'Return the one structured result for this workflow step',
|
|
92
|
+
parameters: Type.Object({ value: Type.Any() }),
|
|
93
|
+
executionMode: 'sequential',
|
|
94
|
+
execute: async () => ({
|
|
95
|
+
content: [
|
|
96
|
+
{ type: 'text' as const, text: 'Captured workflow step result.' },
|
|
97
|
+
],
|
|
98
|
+
details: {},
|
|
99
|
+
terminate: true,
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
|
|
88
103
|
pi.on('input', (event) => {
|
|
89
104
|
let extracted;
|
|
90
105
|
try {
|
|
@@ -104,11 +119,7 @@ export const registerSubagentChildRuntime = (
|
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
try {
|
|
107
|
-
if (!
|
|
108
|
-
throw new Error(
|
|
109
|
-
'child agent does not match the delegated workflow policy',
|
|
110
|
-
);
|
|
111
|
-
}
|
|
122
|
+
if (!childAgent) throw new Error('workflow worker agent is unavailable');
|
|
112
123
|
verifyChildWorkingDirectory(extracted.policy, dependencies);
|
|
113
124
|
verifyChildCapability({
|
|
114
125
|
policy: extracted.policy,
|
|
@@ -117,9 +128,7 @@ export const registerSubagentChildRuntime = (
|
|
|
117
128
|
});
|
|
118
129
|
const profileTools = new Set(pi.getActiveTools());
|
|
119
130
|
if (!profileTools.has(CHILD_COMPLETION_TOOL)) {
|
|
120
|
-
throw new Error(
|
|
121
|
-
'pi-subagents structured_output completion is unavailable',
|
|
122
|
-
);
|
|
131
|
+
throw new Error('workflow worker completion tool is unavailable');
|
|
123
132
|
}
|
|
124
133
|
const effectiveTools = new Set(
|
|
125
134
|
resolveActiveTools(
|
|
@@ -1,51 +1,188 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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';
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
11
3
|
import type {
|
|
12
4
|
SubagentDelegationRequest,
|
|
13
5
|
SubagentDelegationResponse,
|
|
6
|
+
SubagentDelegationUpdate,
|
|
14
7
|
} from './protocol-events.ts';
|
|
15
8
|
|
|
16
|
-
export type {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
9
|
+
export type DelegateOptions = {
|
|
10
|
+
readonly signal?: AbortSignal;
|
|
11
|
+
readonly onUpdate?: (update: { readonly requestId: string }) => void;
|
|
12
|
+
readonly onLateTerminal?: (response: SubagentDelegationResponse) => void;
|
|
13
|
+
};
|
|
21
14
|
|
|
22
|
-
/**
|
|
23
|
-
* Functional surface used by workflow orchestration.
|
|
24
|
-
*/
|
|
15
|
+
/** A pi-workflows-owned foreground Pi process. */
|
|
25
16
|
export type SubagentDelegationClientController = {
|
|
26
|
-
/** Request currently owned by this client, if any. */
|
|
27
17
|
readonly activeRequestId: string | undefined;
|
|
28
|
-
/** Starts one correlated delegation. */
|
|
29
18
|
readonly delegate: (
|
|
30
19
|
request: SubagentDelegationRequest,
|
|
31
20
|
options?: DelegateOptions,
|
|
32
21
|
) => Promise<SubagentDelegationResponse>;
|
|
33
|
-
/** Cancels the active delegation and waits for terminal confirmation. */
|
|
34
22
|
readonly cancelActiveAndWait: (waitMs?: number) => Promise<boolean>;
|
|
35
23
|
};
|
|
36
24
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
25
|
+
export type DirectWorkerSpawn = (
|
|
26
|
+
command: string,
|
|
27
|
+
args: Array<string>,
|
|
28
|
+
options: {
|
|
29
|
+
readonly cwd: string;
|
|
30
|
+
readonly env: NodeJS.ProcessEnv;
|
|
31
|
+
readonly stdio: ['ignore', 'pipe', 'pipe'];
|
|
32
|
+
},
|
|
33
|
+
) => ChildProcess & {
|
|
34
|
+
readonly stdout: NonNullable<ChildProcess['stdout']>;
|
|
35
|
+
readonly stderr: NonNullable<ChildProcess['stderr']>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const directWorkerCommand = (
|
|
39
|
+
request: SubagentDelegationRequest,
|
|
40
|
+
): ReadonlyArray<string> => [
|
|
41
|
+
'--no-session',
|
|
42
|
+
'--mode',
|
|
43
|
+
'json',
|
|
44
|
+
...(request.model ? ['--model', request.model] : []),
|
|
45
|
+
...(request.thinking ? ['--thinking', request.thinking] : []),
|
|
46
|
+
'--print',
|
|
47
|
+
request.task,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export function directWorkerResponse(
|
|
51
|
+
request: SubagentDelegationRequest,
|
|
52
|
+
code: number | null,
|
|
53
|
+
signal: NodeJS.Signals | null,
|
|
54
|
+
stderr: string,
|
|
55
|
+
): SubagentDelegationResponse {
|
|
56
|
+
const status = code === 0 ? 'completed' : signal ? 'cancelled' : 'failed';
|
|
57
|
+
return {
|
|
58
|
+
requestId: request.requestId,
|
|
59
|
+
agent: request.agent,
|
|
60
|
+
status,
|
|
61
|
+
...(code === null ? {} : { exitCode: code }),
|
|
62
|
+
...(status !== 'completed' && stderr.trim()
|
|
63
|
+
? { error: stderr.trim().slice(-4_000) }
|
|
64
|
+
: {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type WorkerJsonEvent = {
|
|
69
|
+
readonly type?: unknown;
|
|
70
|
+
readonly toolName?: unknown;
|
|
71
|
+
readonly args?: unknown;
|
|
72
|
+
readonly message?: { readonly role?: unknown };
|
|
73
|
+
readonly assistantMessageEvent?: {
|
|
74
|
+
readonly type?: unknown;
|
|
75
|
+
readonly delta?: unknown;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type WorkerProgress = {
|
|
80
|
+
readonly toolCount: number;
|
|
81
|
+
readonly responseText: string;
|
|
82
|
+
readonly update?: SubagentDelegationUpdate;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const MAX_PROGRESS_DETAIL_CHARS = 480;
|
|
86
|
+
const SECRET_KEY = /authorization|cookie|password|secret|token|api[-_]?key/i;
|
|
87
|
+
|
|
88
|
+
function redactProgressValue(value: unknown, key = ''): unknown {
|
|
89
|
+
if (SECRET_KEY.test(key)) return '[redacted]';
|
|
90
|
+
if (typeof value === 'string') {
|
|
91
|
+
return value.length > MAX_PROGRESS_DETAIL_CHARS
|
|
92
|
+
? `${value.slice(0, MAX_PROGRESS_DETAIL_CHARS - 1)}…`
|
|
93
|
+
: value;
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(value))
|
|
96
|
+
return value.map((item) => redactProgressValue(item));
|
|
97
|
+
if (value && typeof value === 'object') {
|
|
98
|
+
return Object.fromEntries(
|
|
99
|
+
Object.entries(value).map(([entryKey, entryValue]) => [
|
|
100
|
+
entryKey,
|
|
101
|
+
redactProgressValue(entryValue, entryKey),
|
|
102
|
+
]),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatToolCall(toolName: string, args: unknown): string {
|
|
109
|
+
const rendered = JSON.stringify(redactProgressValue(args));
|
|
110
|
+
return `call ${toolName} ${rendered}`.slice(0, MAX_PROGRESS_DETAIL_CHARS);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Converts one Pi JSONL event into safe, operator-visible worker progress. */
|
|
114
|
+
export function workerProgressFromJsonLine(
|
|
115
|
+
line: string,
|
|
116
|
+
requestId: string,
|
|
117
|
+
toolCount: number,
|
|
118
|
+
responseText = '',
|
|
119
|
+
): WorkerProgress {
|
|
120
|
+
let event: WorkerJsonEvent;
|
|
121
|
+
try {
|
|
122
|
+
const parsed: unknown = JSON.parse(line);
|
|
123
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
124
|
+
return { toolCount, responseText };
|
|
125
|
+
event = parsed;
|
|
126
|
+
} catch {
|
|
127
|
+
return { toolCount, responseText };
|
|
128
|
+
}
|
|
129
|
+
if (event.type === 'agent_start') {
|
|
130
|
+
return {
|
|
131
|
+
toolCount,
|
|
132
|
+
responseText,
|
|
133
|
+
update: { requestId, activity: 'thinking', toolCount },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (event.type === 'message_start' && event.message?.role === 'assistant') {
|
|
137
|
+
return { toolCount, responseText: '' };
|
|
138
|
+
}
|
|
139
|
+
if (
|
|
140
|
+
event.type === 'message_update' &&
|
|
141
|
+
event.assistantMessageEvent?.type === 'text_delta' &&
|
|
142
|
+
typeof event.assistantMessageEvent.delta === 'string'
|
|
143
|
+
) {
|
|
144
|
+
const nextResponseText =
|
|
145
|
+
`${responseText}${event.assistantMessageEvent.delta}`.slice(
|
|
146
|
+
-MAX_PROGRESS_DETAIL_CHARS,
|
|
147
|
+
);
|
|
148
|
+
return {
|
|
149
|
+
toolCount,
|
|
150
|
+
responseText: nextResponseText,
|
|
151
|
+
update: {
|
|
152
|
+
requestId,
|
|
153
|
+
activity: 'responding',
|
|
154
|
+
detail: `response: ${nextResponseText}`,
|
|
155
|
+
toolCount,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (
|
|
160
|
+
(event.type === 'tool_execution_start' ||
|
|
161
|
+
event.type === 'tool_execution_update') &&
|
|
162
|
+
typeof event.toolName === 'string'
|
|
163
|
+
) {
|
|
164
|
+
const nextToolCount =
|
|
165
|
+
event.type === 'tool_execution_start' ? toolCount + 1 : toolCount;
|
|
166
|
+
return {
|
|
167
|
+
toolCount: nextToolCount,
|
|
168
|
+
responseText,
|
|
169
|
+
update: {
|
|
170
|
+
requestId,
|
|
171
|
+
currentTool: event.toolName,
|
|
172
|
+
...(event.type === 'tool_execution_start'
|
|
173
|
+
? { detail: formatToolCall(event.toolName, event.args) }
|
|
174
|
+
: {}),
|
|
175
|
+
toolCount: nextToolCount,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return { toolCount, responseText };
|
|
180
|
+
}
|
|
181
|
+
|
|
44
182
|
export function createSubagentDelegationClient(
|
|
45
|
-
|
|
46
|
-
dependencies: SubagentDelegationClientDependencies = DEFAULT_CLIENT_DEPENDENCIES,
|
|
183
|
+
spawnWorker: DirectWorkerSpawn = spawn,
|
|
47
184
|
): SubagentDelegationClientController {
|
|
48
|
-
let active:
|
|
185
|
+
let active: { requestId: string; process: ChildProcess } | undefined;
|
|
49
186
|
|
|
50
187
|
const delegate = (
|
|
51
188
|
request: SubagentDelegationRequest,
|
|
@@ -53,45 +190,67 @@ export function createSubagentDelegationClient(
|
|
|
53
190
|
): Promise<SubagentDelegationResponse> => {
|
|
54
191
|
if (active) {
|
|
55
192
|
return Promise.reject(
|
|
56
|
-
new Error(`
|
|
193
|
+
new Error(`workflow worker "${active.requestId}" is still active`),
|
|
57
194
|
);
|
|
58
195
|
}
|
|
59
196
|
if (options.signal?.aborted) {
|
|
60
|
-
return Promise.reject(new Error('
|
|
197
|
+
return Promise.reject(new Error('workflow worker was cancelled'));
|
|
61
198
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
const child = spawnWorker('pi', [...directWorkerCommand(request)], {
|
|
201
|
+
cwd: request.cwd,
|
|
202
|
+
env: {
|
|
203
|
+
...process.env,
|
|
204
|
+
PI_WORKFLOWS_CHILD: '1',
|
|
205
|
+
PI_WORKFLOWS_CHILD_AGENT: request.agent,
|
|
206
|
+
},
|
|
207
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
208
|
+
});
|
|
209
|
+
active = { requestId: request.requestId, process: child };
|
|
210
|
+
let stderr = '';
|
|
211
|
+
let stdoutBuffer = '';
|
|
212
|
+
let toolCount = 0;
|
|
213
|
+
let responseText = '';
|
|
214
|
+
const stdoutDecoder = new StringDecoder('utf8');
|
|
215
|
+
const consumeWorkerLines = (): void => {
|
|
216
|
+
while (true) {
|
|
217
|
+
const newline = stdoutBuffer.indexOf('\n');
|
|
218
|
+
if (newline === -1) return;
|
|
219
|
+
const line = stdoutBuffer.slice(0, newline);
|
|
220
|
+
stdoutBuffer = stdoutBuffer.slice(newline + 1);
|
|
221
|
+
const progress = workerProgressFromJsonLine(
|
|
222
|
+
line,
|
|
223
|
+
request.requestId,
|
|
224
|
+
toolCount,
|
|
225
|
+
responseText,
|
|
226
|
+
);
|
|
227
|
+
toolCount = progress.toolCount;
|
|
228
|
+
responseText = progress.responseText;
|
|
229
|
+
if (progress.update) options.onUpdate?.(progress.update);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
const consumeWorkerOutput = (chunk: Buffer): void => {
|
|
233
|
+
stdoutBuffer += stdoutDecoder.write(chunk);
|
|
234
|
+
consumeWorkerLines();
|
|
89
235
|
};
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
236
|
+
child.stdout.on('data', consumeWorkerOutput);
|
|
237
|
+
child.stderr.on('data', (chunk: Buffer) => {
|
|
238
|
+
stderr += chunk.toString('utf8');
|
|
239
|
+
});
|
|
240
|
+
const abort = (): void => {
|
|
241
|
+
child.kill('SIGTERM');
|
|
242
|
+
};
|
|
243
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
244
|
+
child.once('error', (error) => {
|
|
245
|
+
if (active?.process === child) active = undefined;
|
|
246
|
+
reject(error);
|
|
247
|
+
});
|
|
248
|
+
child.once('close', (code, signal) => {
|
|
249
|
+
stdoutBuffer += stdoutDecoder.end();
|
|
250
|
+
consumeWorkerLines();
|
|
251
|
+
if (active?.process === child) active = undefined;
|
|
252
|
+
options.signal?.removeEventListener('abort', abort);
|
|
253
|
+
resolve(directWorkerResponse(request, code, signal, stderr));
|
|
95
254
|
});
|
|
96
255
|
});
|
|
97
256
|
};
|
|
@@ -101,48 +260,31 @@ export function createSubagentDelegationClient(
|
|
|
101
260
|
return active?.requestId;
|
|
102
261
|
},
|
|
103
262
|
delegate,
|
|
104
|
-
cancelActiveAndWait
|
|
263
|
+
async cancelActiveAndWait(): Promise<boolean> {
|
|
264
|
+
const current = active;
|
|
265
|
+
if (!current) return true;
|
|
266
|
+
current.process.kill('SIGTERM');
|
|
267
|
+
return new Promise((resolve) => {
|
|
268
|
+
current.process.once('close', () => {
|
|
269
|
+
resolve(true);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
},
|
|
105
273
|
};
|
|
106
274
|
}
|
|
107
275
|
|
|
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
276
|
export class SubagentDelegationClient implements SubagentDelegationClientController {
|
|
115
|
-
readonly #
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Creates a client over the supplied event and timer boundaries.
|
|
119
|
-
*/
|
|
120
|
-
constructor(
|
|
121
|
-
events: SubagentEventBus,
|
|
122
|
-
dependencies: SubagentDelegationClientDependencies = DEFAULT_CLIENT_DEPENDENCIES,
|
|
123
|
-
) {
|
|
124
|
-
this.#controller = createSubagentDelegationClient(events, dependencies);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Returns the request currently owned by this client, if any. */
|
|
277
|
+
readonly #client = createSubagentDelegationClient();
|
|
128
278
|
get activeRequestId(): string | undefined {
|
|
129
|
-
return this.#
|
|
279
|
+
return this.#client.activeRequestId;
|
|
130
280
|
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Starts a correlated delegation and resolves on its terminal event.
|
|
134
|
-
*/
|
|
135
281
|
delegate(
|
|
136
282
|
request: SubagentDelegationRequest,
|
|
137
|
-
options
|
|
283
|
+
options?: DelegateOptions,
|
|
138
284
|
): Promise<SubagentDelegationResponse> {
|
|
139
|
-
return this.#
|
|
285
|
+
return this.#client.delegate(request, options);
|
|
140
286
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
* Requests cancellation and waits briefly for terminal confirmation.
|
|
144
|
-
*/
|
|
145
|
-
cancelActiveAndWait(waitMs = 5_000): Promise<boolean> {
|
|
146
|
-
return this.#controller.cancelActiveAndWait(waitMs);
|
|
287
|
+
cancelActiveAndWait(waitMs?: number): Promise<boolean> {
|
|
288
|
+
return this.#client.cancelActiveAndWait(waitMs);
|
|
147
289
|
}
|
|
148
290
|
}
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
SubagentDelegationRequest as UpstreamDelegationRequest,
|
|
3
|
-
SubagentDelegationResponse as UpstreamDelegationResponse,
|
|
4
|
-
SubagentDelegationStatus as UpstreamDelegationStatus,
|
|
5
|
-
SubagentDelegationUpdate as UpstreamDelegationUpdate,
|
|
6
|
-
} from 'pi-subagents/delegation';
|
|
7
|
-
|
|
8
|
-
// These released v1 transport values are duplicated as literals because
|
|
9
|
-
// pi-subagents 0.36.0 exports TypeScript source. Node's native type stripping
|
|
10
|
-
// cannot execute TypeScript below node_modules; the public types above remain
|
|
11
|
-
// the compile-time compatibility check.
|
|
12
1
|
export const SUBAGENT_DELEGATION_PROTOCOL_VERSION = 1 as const;
|
|
13
2
|
export const SUBAGENT_DELEGATION_REQUEST_EVENT =
|
|
14
3
|
'prompt-template:subagent:request';
|
|
@@ -21,7 +10,35 @@ export const SUBAGENT_DELEGATION_RESPONSE_EVENT =
|
|
|
21
10
|
export const SUBAGENT_DELEGATION_CANCEL_EVENT =
|
|
22
11
|
'prompt-template:subagent:cancel';
|
|
23
12
|
|
|
24
|
-
export type SubagentDelegationRequest =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
13
|
+
export type SubagentDelegationRequest = {
|
|
14
|
+
readonly version: 1;
|
|
15
|
+
readonly requestId: string;
|
|
16
|
+
readonly agent: string;
|
|
17
|
+
readonly task: string;
|
|
18
|
+
readonly cwd: string;
|
|
19
|
+
readonly timeoutMs?: number;
|
|
20
|
+
readonly model?: string;
|
|
21
|
+
readonly thinking?:
|
|
22
|
+
'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
23
|
+
readonly [key: string]: unknown;
|
|
24
|
+
};
|
|
25
|
+
export type SubagentDelegationUpdate = {
|
|
26
|
+
readonly requestId: string;
|
|
27
|
+
readonly activity?: string;
|
|
28
|
+
/** Operator-visible detail; never used as workflow instruction or input. */
|
|
29
|
+
readonly detail?: string;
|
|
30
|
+
readonly currentTool?: string;
|
|
31
|
+
readonly toolCount?: number;
|
|
32
|
+
readonly tokens?: number;
|
|
33
|
+
};
|
|
34
|
+
export type SubagentDelegationStatus = 'completed' | 'failed' | 'cancelled';
|
|
35
|
+
|
|
36
|
+
export type SubagentDelegationResponse = {
|
|
37
|
+
readonly version?: number;
|
|
38
|
+
readonly requestId: string;
|
|
39
|
+
readonly agent?: string;
|
|
40
|
+
readonly status: SubagentDelegationStatus;
|
|
41
|
+
readonly error?: string;
|
|
42
|
+
readonly exitCode?: number;
|
|
43
|
+
readonly warnings?: ReadonlyArray<string>;
|
|
44
|
+
};
|
|
@@ -11,7 +11,7 @@ export type {
|
|
|
11
11
|
ChildStepPolicy,
|
|
12
12
|
ExtractedChildPolicy,
|
|
13
13
|
} from './child-policy-types.ts';
|
|
14
|
-
export {
|
|
14
|
+
export { isAgentProfileName } from './child-policy-validation.ts';
|
|
15
15
|
export { parseDelegatedStepResult } from './delegated-result.ts';
|
|
16
16
|
export type { DelegatedStepResult } from './delegated-result.ts';
|
|
17
17
|
export {
|
package/src/preflight.ts
CHANGED
|
@@ -69,19 +69,11 @@ export function preflightStep(
|
|
|
69
69
|
const extensionResources = [...inventory.tools, ...inventory.commands];
|
|
70
70
|
const hasExtension = (extension: string): boolean =>
|
|
71
71
|
extensionResources.some((resource) => sourceMatches(resource, extension));
|
|
72
|
-
const hasSubagentTool = inventory.tools.some(
|
|
73
|
-
(tool) => tool.name === 'subagent' && sourceMatches(tool, 'pi-subagents'),
|
|
74
|
-
);
|
|
75
72
|
const isPlannotatorRequired =
|
|
76
73
|
step.gate?.provider === 'plannotator' &&
|
|
77
74
|
!step.requires.extensions.includes('plannotator');
|
|
78
75
|
|
|
79
76
|
return [
|
|
80
|
-
...(step.subagent && !hasSubagentTool
|
|
81
|
-
? [
|
|
82
|
-
'pi-subagents is required, but its "subagent" tool is not installed or detectable',
|
|
83
|
-
]
|
|
84
|
-
: []),
|
|
85
77
|
...missingRequiredResources({
|
|
86
78
|
requiredNames: step.requires.tools,
|
|
87
79
|
hasResource: (toolName) => toolNames.has(toolName),
|
|
@@ -20,22 +20,15 @@ export function buildMainWorkflowNotice(
|
|
|
20
20
|
throw new Error(`unknown workflow step "${run.currentStepId}"`);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
if (!step.subagent) {
|
|
24
|
-
return [
|
|
25
|
-
'# Active main-agent workflow',
|
|
26
|
-
'',
|
|
27
|
-
`Workflow "${workflow.definition.id}" is running step "${run.currentStepId}" (${step.title}) in this session.`,
|
|
28
|
-
'Perform only the active workflow step with its allowed resources.',
|
|
29
|
-
'Call `workflow_complete_step` exactly once when finished.',
|
|
30
|
-
'Use `/workflow-pause` to halt and repair the workflow before resuming.',
|
|
31
|
-
].join('\n');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
23
|
return [
|
|
35
|
-
'# Active
|
|
24
|
+
'# Active workflow',
|
|
36
25
|
'',
|
|
37
|
-
`Workflow "${workflow.definition.id}" is running step "${run.currentStepId}" (${step.title}) in
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
`Workflow "${workflow.definition.id}" is running step "${run.currentStepId}" (${step.title}) in this session.`,
|
|
27
|
+
...(step.agent
|
|
28
|
+
? [`Apply the "${step.agent.name}" workflow role prompt.`]
|
|
29
|
+
: []),
|
|
30
|
+
'Perform only the active workflow step with its allowed resources.',
|
|
31
|
+
'Call `workflow_complete_step` exactly once when finished.',
|
|
32
|
+
`Use \`${statusShortcutLabel}\` or \`/workflow-status\` to inspect status, or \`/workflow-pause\` to halt and repair the workflow before resuming.`,
|
|
40
33
|
].join('\n');
|
|
41
34
|
}
|
package/src/prompt/step-task.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadAgentProfile } from '../agents/profile.ts';
|
|
1
2
|
import type { LoadedWorkflow, WorkflowStep } from '../config/types.ts';
|
|
2
3
|
import type { WorkflowRun } from '../engine/state.ts';
|
|
3
4
|
import { createStepContract } from './step-contract.ts';
|
|
@@ -16,6 +17,8 @@ import {
|
|
|
16
17
|
|
|
17
18
|
type StepExecution = 'delegated' | 'main';
|
|
18
19
|
|
|
20
|
+
const rolePrompt = (name: string): string => loadAgentProfile(name).prompt;
|
|
21
|
+
|
|
19
22
|
type BuildStepTaskOptions =
|
|
20
23
|
| {
|
|
21
24
|
readonly execution: 'delegated';
|
|
@@ -123,15 +126,13 @@ export function buildStepTask(options: BuildStepTaskOptions): string {
|
|
|
123
126
|
`Run: ${run.runId}`,
|
|
124
127
|
`Iteration: ${run.iteration ?? 1}`,
|
|
125
128
|
`Step: ${run.currentStepId} (${step.title})`,
|
|
126
|
-
...(
|
|
127
|
-
? [
|
|
128
|
-
`Agent profile: ${step.subagent?.agent ?? 'generalist'}`,
|
|
129
|
-
'Context: fresh workflow-step context; no parent or sibling transcript is inherited.',
|
|
130
|
-
]
|
|
131
|
-
: []),
|
|
129
|
+
...(step.agent ? [`Agent profile: ${step.agent.name}`] : []),
|
|
132
130
|
'',
|
|
133
131
|
'## Step instructions',
|
|
134
132
|
'',
|
|
133
|
+
...(step.agent
|
|
134
|
+
? ['## Role prompt', '', rolePrompt(step.agent.name), '']
|
|
135
|
+
: []),
|
|
135
136
|
prompt,
|
|
136
137
|
'',
|
|
137
138
|
...(isDelegated ? buildDelegatedHandoffSection(handoff) : []),
|
|
@@ -26,7 +26,11 @@ export function formatWorkflowProgressStatus(
|
|
|
26
26
|
);
|
|
27
27
|
const activity =
|
|
28
28
|
run.status === 'awaiting-gate' ? 'awaiting review' : 'working';
|
|
29
|
-
|
|
29
|
+
const workerProgress =
|
|
30
|
+
snapshot.execution?.kind === 'subagent'
|
|
31
|
+
? ` · ${snapshot.execution.progress}`
|
|
32
|
+
: '';
|
|
33
|
+
return `${workflowStatusIcon(run, snapshot.now)} ${run.workflowId} · step ${currentStep} · ${activity}${workerProgress} · ${statusShortcutLabel}`;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/** Format a plain-text workflow status suitable for fallback notifications. */
|