@wichayutdew/pi-workflows 2.7.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +117 -6
- package/package.json +1 -1
- package/src/harness/delegation-recovery.ts +15 -0
- package/src/harness/delegation-response-actions.ts +21 -1
- package/src/integrations/subagents/child-runtime-repair.ts +23 -0
- package/src/integrations/subagents/child-runtime.ts +25 -0
- package/src/integrations/subagents/client.ts +84 -1
- package/src/integrations/subagents/diagnostics.ts +43 -0
- package/src/integrations/subagents/protocol-events.ts +3 -0
package/dist/index.js
CHANGED
|
@@ -1505,17 +1505,19 @@ var directWorkerCommand = (request) => [
|
|
|
1505
1505
|
"--print",
|
|
1506
1506
|
request.task
|
|
1507
1507
|
];
|
|
1508
|
-
function directWorkerResponse(request, code, signal, stderr) {
|
|
1508
|
+
function directWorkerResponse(request, code, signal, stderr, diagnostic) {
|
|
1509
1509
|
const status = code === 0 ? "completed" : signal ? "cancelled" : "failed";
|
|
1510
1510
|
return {
|
|
1511
1511
|
requestId: request.requestId,
|
|
1512
1512
|
agent: request.agent,
|
|
1513
1513
|
status,
|
|
1514
1514
|
...code === null ? {} : { exitCode: code },
|
|
1515
|
-
...status !== "completed" && stderr.trim() ? { error: stderr.trim().slice(-4000) } : {}
|
|
1515
|
+
...status !== "completed" && stderr.trim() ? { error: stderr.trim().slice(-4000) } : {},
|
|
1516
|
+
...diagnostic ? { diagnostic } : {}
|
|
1516
1517
|
};
|
|
1517
1518
|
}
|
|
1518
1519
|
var MAX_PROGRESS_DETAIL_CHARS = 480;
|
|
1520
|
+
var MAX_DIAGNOSTIC_CALLS = 64;
|
|
1519
1521
|
var SECRET_KEY = /authorization|cookie|password|secret|token|api[-_]?key/i;
|
|
1520
1522
|
function redactProgressValue(value, key = "") {
|
|
1521
1523
|
if (SECRET_KEY.test(key))
|
|
@@ -1537,6 +1539,52 @@ function formatToolCall(toolName, args) {
|
|
|
1537
1539
|
const rendered = JSON.stringify(redactProgressValue(args));
|
|
1538
1540
|
return `call ${toolName} ${rendered}`.slice(0, MAX_PROGRESS_DETAIL_CHARS);
|
|
1539
1541
|
}
|
|
1542
|
+
var createDiagnostic2 = () => ({
|
|
1543
|
+
settled: false,
|
|
1544
|
+
truncated: false,
|
|
1545
|
+
calls: new Map
|
|
1546
|
+
});
|
|
1547
|
+
var diagnosticSnapshot = (diagnostic) => ({
|
|
1548
|
+
settled: diagnostic.settled,
|
|
1549
|
+
truncated: diagnostic.truncated,
|
|
1550
|
+
calls: [...diagnostic.calls.values()]
|
|
1551
|
+
});
|
|
1552
|
+
var recordWorkerDiagnostic = (line, diagnostic) => {
|
|
1553
|
+
let event;
|
|
1554
|
+
try {
|
|
1555
|
+
const parsed = JSON.parse(line);
|
|
1556
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
1557
|
+
return;
|
|
1558
|
+
event = parsed;
|
|
1559
|
+
} catch {
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (event.type === "agent_settled") {
|
|
1563
|
+
diagnostic.settled = true;
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
if (event.type !== "tool_execution_start" && event.type !== "tool_execution_end" || typeof event.toolName !== "string" || typeof event.toolCallId !== "string") {
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
if (!diagnostic.calls.has(event.toolCallId)) {
|
|
1570
|
+
if (diagnostic.calls.size >= MAX_DIAGNOSTIC_CALLS) {
|
|
1571
|
+
diagnostic.truncated = true;
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
diagnostic.calls.set(event.toolCallId, {
|
|
1575
|
+
id: event.toolCallId,
|
|
1576
|
+
name: event.toolName,
|
|
1577
|
+
state: "started"
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
if (event.type === "tool_execution_end") {
|
|
1581
|
+
diagnostic.calls.set(event.toolCallId, {
|
|
1582
|
+
id: event.toolCallId,
|
|
1583
|
+
name: event.toolName,
|
|
1584
|
+
state: event.isError === false ? "completed" : "failed"
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1540
1588
|
function workerProgressFromJsonLine(line, requestId, toolCount, responseText = "") {
|
|
1541
1589
|
let event;
|
|
1542
1590
|
try {
|
|
@@ -1609,6 +1657,7 @@ function createSubagentDelegationClient(spawnWorker = spawn) {
|
|
|
1609
1657
|
let stdoutBuffer = "";
|
|
1610
1658
|
let toolCount = 0;
|
|
1611
1659
|
let responseText = "";
|
|
1660
|
+
const diagnostic = createDiagnostic2();
|
|
1612
1661
|
const stdoutDecoder = new StringDecoder("utf8");
|
|
1613
1662
|
const consumeWorkerLines = () => {
|
|
1614
1663
|
while (true) {
|
|
@@ -1618,6 +1667,7 @@ function createSubagentDelegationClient(spawnWorker = spawn) {
|
|
|
1618
1667
|
return;
|
|
1619
1668
|
const line = stdoutBuffer.slice(0, newline);
|
|
1620
1669
|
stdoutBuffer = stdoutBuffer.slice(newline + 1);
|
|
1670
|
+
recordWorkerDiagnostic(line, diagnostic);
|
|
1621
1671
|
const progress = workerProgressFromJsonLine(line, request.requestId, toolCount, responseText);
|
|
1622
1672
|
toolCount = progress.toolCount;
|
|
1623
1673
|
responseText = progress.responseText;
|
|
@@ -1648,7 +1698,7 @@ function createSubagentDelegationClient(spawnWorker = spawn) {
|
|
|
1648
1698
|
if (active?.process === child)
|
|
1649
1699
|
active = undefined;
|
|
1650
1700
|
options.signal?.removeEventListener("abort", abort);
|
|
1651
|
-
resolve3(directWorkerResponse(request, code, signal, stderr));
|
|
1701
|
+
resolve3(directWorkerResponse(request, code, signal, stderr, diagnosticSnapshot(diagnostic)));
|
|
1652
1702
|
});
|
|
1653
1703
|
});
|
|
1654
1704
|
};
|
|
@@ -6563,6 +6613,26 @@ function createStepExecutionActions() {
|
|
|
6563
6613
|
};
|
|
6564
6614
|
}
|
|
6565
6615
|
|
|
6616
|
+
// src/integrations/subagents/diagnostics.ts
|
|
6617
|
+
var READ_ONLY_TOOLS = new Set([
|
|
6618
|
+
"read",
|
|
6619
|
+
"ls",
|
|
6620
|
+
"grep",
|
|
6621
|
+
"structured_output"
|
|
6622
|
+
]);
|
|
6623
|
+
var classifyRecoverySafety = (diagnostic) => {
|
|
6624
|
+
if (!diagnostic || !diagnostic.settled || diagnostic.truncated) {
|
|
6625
|
+
return "incomplete";
|
|
6626
|
+
}
|
|
6627
|
+
if (diagnostic.calls.some((call) => call.state !== "completed" || !READ_ONLY_TOOLS.has(call.name))) {
|
|
6628
|
+
return "unsafe";
|
|
6629
|
+
}
|
|
6630
|
+
return "read-only";
|
|
6631
|
+
};
|
|
6632
|
+
|
|
6633
|
+
// src/harness/delegation-recovery.ts
|
|
6634
|
+
var shouldRetryMissingCompletion = (diagnostic, subagentAttemptCount) => subagentAttemptCount === 1 && classifyRecoverySafety(diagnostic) === "read-only";
|
|
6635
|
+
|
|
6566
6636
|
// src/harness/delegation-response-actions.ts
|
|
6567
6637
|
function hasErrorCode(error, code) {
|
|
6568
6638
|
return error instanceof Error && "code" in error && error.code === code;
|
|
@@ -6638,7 +6708,15 @@ async function finishDelegation(active, response) {
|
|
|
6638
6708
|
serializedResult = await this.dependencies.readDelegatedResult(active);
|
|
6639
6709
|
} catch (error) {
|
|
6640
6710
|
if (hasErrorCode(error, "ENOENT")) {
|
|
6641
|
-
|
|
6711
|
+
const subagentAttemptCount = this.run.currentStepAttempts?.filter((attempt) => attempt.kind === "subagent").length ?? 0;
|
|
6712
|
+
if (shouldRetryMissingCompletion(response.diagnostic, subagentAttemptCount)) {
|
|
6713
|
+
cleanupAttempted = true;
|
|
6714
|
+
await this.cleanupDelegation(active);
|
|
6715
|
+
this.launchCurrentStep(workflow);
|
|
6716
|
+
return;
|
|
6717
|
+
}
|
|
6718
|
+
const diagnosticState = response.diagnostic ? `settled=${response.diagnostic.settled}, truncated=${response.diagnostic.truncated}, calls=${response.diagnostic.calls.length}` : "unavailable";
|
|
6719
|
+
throw new Error(`Subagent "${active.agent}" completed without producing the required correlated structured_output result (request ${active.requestId}; diagnostic ${diagnosticState})`, { cause: error });
|
|
6642
6720
|
}
|
|
6643
6721
|
throw error;
|
|
6644
6722
|
}
|
|
@@ -7521,6 +7599,24 @@ var DEFAULT_CHILD_RUNTIME_DEPENDENCIES = {
|
|
|
7521
7599
|
tokensAreEqual
|
|
7522
7600
|
};
|
|
7523
7601
|
|
|
7602
|
+
// src/integrations/subagents/child-runtime-repair.ts
|
|
7603
|
+
var COMPLETION_REPAIR_PROMPT = [
|
|
7604
|
+
"The delegated step settled without its required correlated result.",
|
|
7605
|
+
"Do not repeat completed work and do not execute work tools.",
|
|
7606
|
+
"Call `structured_output` exactly once, alone, with one configured outcome and the required summary, artifact, and workspace fields."
|
|
7607
|
+
].join(`
|
|
7608
|
+
`);
|
|
7609
|
+
var needsCompletionRepair = ({
|
|
7610
|
+
policy,
|
|
7611
|
+
dependencies
|
|
7612
|
+
}) => {
|
|
7613
|
+
try {
|
|
7614
|
+
return !dependencies.fileSystem.exists(policy.resultPath);
|
|
7615
|
+
} catch {
|
|
7616
|
+
return false;
|
|
7617
|
+
}
|
|
7618
|
+
};
|
|
7619
|
+
|
|
7524
7620
|
// src/integrations/subagents/child-runtime-files.ts
|
|
7525
7621
|
var verifyChildWorkingDirectory = (policy, dependencies) => {
|
|
7526
7622
|
let expected;
|
|
@@ -7618,7 +7714,8 @@ var INITIAL_STATE = {
|
|
|
7618
7714
|
activePolicy: undefined,
|
|
7619
7715
|
policyError: undefined,
|
|
7620
7716
|
invalidCompletionCalls: new Set,
|
|
7621
|
-
effectiveTools: new Set
|
|
7717
|
+
effectiveTools: new Set,
|
|
7718
|
+
repairRequested: false
|
|
7622
7719
|
};
|
|
7623
7720
|
var errorMessage2 = (error) => error instanceof Error ? error.message : String(error);
|
|
7624
7721
|
var invalidPolicyInput = (pi, policyError, images) => {
|
|
@@ -7684,7 +7781,8 @@ var registerSubagentChildRuntime = (pi, options = {}) => {
|
|
|
7684
7781
|
...state,
|
|
7685
7782
|
activePolicy: extracted.policy,
|
|
7686
7783
|
policyError: undefined,
|
|
7687
|
-
effectiveTools
|
|
7784
|
+
effectiveTools,
|
|
7785
|
+
repairRequested: false
|
|
7688
7786
|
};
|
|
7689
7787
|
} catch (error) {
|
|
7690
7788
|
const policyError = errorMessage2(error);
|
|
@@ -7717,6 +7815,19 @@ ${childSystemPrompt(state.activePolicy)}`
|
|
|
7717
7815
|
pi.on("turn_start", () => {
|
|
7718
7816
|
state = { ...state, invalidCompletionCalls: new Set };
|
|
7719
7817
|
});
|
|
7818
|
+
pi.on("agent_settled", () => {
|
|
7819
|
+
const policy = state.activePolicy;
|
|
7820
|
+
if (!policy || state.repairRequested || !needsCompletionRepair({ policy, dependencies })) {
|
|
7821
|
+
return;
|
|
7822
|
+
}
|
|
7823
|
+
state = {
|
|
7824
|
+
...state,
|
|
7825
|
+
repairRequested: true,
|
|
7826
|
+
effectiveTools: new Set([CHILD_COMPLETION_TOOL])
|
|
7827
|
+
};
|
|
7828
|
+
pi.setActiveTools([CHILD_COMPLETION_TOOL]);
|
|
7829
|
+
pi.sendUserMessage(COMPLETION_REPAIR_PROMPT, { deliverAs: "followUp" });
|
|
7830
|
+
});
|
|
7720
7831
|
pi.on("message_end", (event) => {
|
|
7721
7832
|
if (!state.activePolicy)
|
|
7722
7833
|
return;
|
package/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
classifyRecoverySafety,
|
|
3
|
+
type DelegationDiagnostic,
|
|
4
|
+
} from '../integrations/subagents/diagnostics.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Allows one fresh retry only after the same-child repair settled with complete
|
|
8
|
+
* read-only evidence. The caller owns preserving run identity and cleanup.
|
|
9
|
+
*/
|
|
10
|
+
export const shouldRetryMissingCompletion = (
|
|
11
|
+
diagnostic: DelegationDiagnostic | undefined,
|
|
12
|
+
subagentAttemptCount: number,
|
|
13
|
+
): boolean =>
|
|
14
|
+
subagentAttemptCount === 1 &&
|
|
15
|
+
classifyRecoverySafety(diagnostic) === 'read-only';
|
|
@@ -9,6 +9,7 @@ import type { WorkflowStepResult } from '../runtime/step-result.ts';
|
|
|
9
9
|
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
10
10
|
import type { ActiveDelegation } from './types.ts';
|
|
11
11
|
import { resolveStepEffects } from './step-effects.ts';
|
|
12
|
+
import { shouldRetryMissingCompletion } from './delegation-recovery.ts';
|
|
12
13
|
|
|
13
14
|
type HarnessActionContext = Pick<
|
|
14
15
|
FullHarnessActionContext,
|
|
@@ -19,6 +20,7 @@ type HarnessActionContext = Pick<
|
|
|
19
20
|
| 'finishDelegation'
|
|
20
21
|
| 'isSessionActive'
|
|
21
22
|
| 'latestContext'
|
|
23
|
+
| 'launchCurrentStep'
|
|
22
24
|
| 'mutationQueue'
|
|
23
25
|
| 'pauseForDelegationFailure'
|
|
24
26
|
| 'releaseMainAfterCancellation'
|
|
@@ -174,8 +176,26 @@ async function finishDelegation(
|
|
|
174
176
|
serializedResult = await this.dependencies.readDelegatedResult(active);
|
|
175
177
|
} catch (error) {
|
|
176
178
|
if (hasErrorCode(error, 'ENOENT')) {
|
|
179
|
+
const subagentAttemptCount =
|
|
180
|
+
this.run.currentStepAttempts?.filter(
|
|
181
|
+
(attempt) => attempt.kind === 'subagent',
|
|
182
|
+
).length ?? 0;
|
|
183
|
+
if (
|
|
184
|
+
shouldRetryMissingCompletion(
|
|
185
|
+
response.diagnostic,
|
|
186
|
+
subagentAttemptCount,
|
|
187
|
+
)
|
|
188
|
+
) {
|
|
189
|
+
cleanupAttempted = true;
|
|
190
|
+
await this.cleanupDelegation(active);
|
|
191
|
+
this.launchCurrentStep(workflow);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const diagnosticState = response.diagnostic
|
|
195
|
+
? `settled=${response.diagnostic.settled}, truncated=${response.diagnostic.truncated}, calls=${response.diagnostic.calls.length}`
|
|
196
|
+
: 'unavailable';
|
|
177
197
|
throw new Error(
|
|
178
|
-
`Subagent "${active.agent}" completed without producing the required correlated structured_output result`,
|
|
198
|
+
`Subagent "${active.agent}" completed without producing the required correlated structured_output result (request ${active.requestId}; diagnostic ${diagnosticState})`,
|
|
179
199
|
{ cause: error },
|
|
180
200
|
);
|
|
181
201
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ChildStepPolicy } from './child-policy-types.ts';
|
|
2
|
+
import type { SubagentChildRuntimeDependencies } from './child-runtime-types.ts';
|
|
3
|
+
|
|
4
|
+
export const COMPLETION_REPAIR_PROMPT = [
|
|
5
|
+
'The delegated step settled without its required correlated result.',
|
|
6
|
+
'Do not repeat completed work and do not execute work tools.',
|
|
7
|
+
'Call `structured_output` exactly once, alone, with one configured outcome and the required summary, artifact, and workspace fields.',
|
|
8
|
+
].join('\n');
|
|
9
|
+
|
|
10
|
+
/** Returns whether a same-child completion repair may be requested safely. */
|
|
11
|
+
export const needsCompletionRepair = ({
|
|
12
|
+
policy,
|
|
13
|
+
dependencies,
|
|
14
|
+
}: {
|
|
15
|
+
readonly policy: ChildStepPolicy;
|
|
16
|
+
readonly dependencies: SubagentChildRuntimeDependencies;
|
|
17
|
+
}): boolean => {
|
|
18
|
+
try {
|
|
19
|
+
return !dependencies.fileSystem.exists(policy.resultPath);
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
parseChildStructuredResult,
|
|
14
14
|
} from './child-runtime-completion.ts';
|
|
15
15
|
import { DEFAULT_CHILD_RUNTIME_DEPENDENCIES } from './child-runtime-dependencies.ts';
|
|
16
|
+
import {
|
|
17
|
+
COMPLETION_REPAIR_PROMPT,
|
|
18
|
+
needsCompletionRepair,
|
|
19
|
+
} from './child-runtime-repair.ts';
|
|
16
20
|
import {
|
|
17
21
|
verifyChildCapability,
|
|
18
22
|
verifyChildWorkingDirectory,
|
|
@@ -39,6 +43,7 @@ type ChildRuntimeState = {
|
|
|
39
43
|
readonly policyError: string | undefined;
|
|
40
44
|
readonly invalidCompletionCalls: ReadonlySet<string>;
|
|
41
45
|
readonly effectiveTools: ReadonlySet<string>;
|
|
46
|
+
readonly repairRequested: boolean;
|
|
42
47
|
};
|
|
43
48
|
|
|
44
49
|
const INITIAL_STATE: ChildRuntimeState = {
|
|
@@ -46,6 +51,7 @@ const INITIAL_STATE: ChildRuntimeState = {
|
|
|
46
51
|
policyError: undefined,
|
|
47
52
|
invalidCompletionCalls: new Set(),
|
|
48
53
|
effectiveTools: new Set(),
|
|
54
|
+
repairRequested: false,
|
|
49
55
|
};
|
|
50
56
|
|
|
51
57
|
const errorMessage = (error: unknown): string =>
|
|
@@ -142,6 +148,7 @@ export const registerSubagentChildRuntime = (
|
|
|
142
148
|
activePolicy: extracted.policy,
|
|
143
149
|
policyError: undefined,
|
|
144
150
|
effectiveTools,
|
|
151
|
+
repairRequested: false,
|
|
145
152
|
};
|
|
146
153
|
} catch (error) {
|
|
147
154
|
const policyError = errorMessage(error);
|
|
@@ -175,6 +182,24 @@ export const registerSubagentChildRuntime = (
|
|
|
175
182
|
state = { ...state, invalidCompletionCalls: new Set() };
|
|
176
183
|
});
|
|
177
184
|
|
|
185
|
+
pi.on('agent_settled', () => {
|
|
186
|
+
const policy = state.activePolicy;
|
|
187
|
+
if (
|
|
188
|
+
!policy ||
|
|
189
|
+
state.repairRequested ||
|
|
190
|
+
!needsCompletionRepair({ policy, dependencies })
|
|
191
|
+
) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
state = {
|
|
195
|
+
...state,
|
|
196
|
+
repairRequested: true,
|
|
197
|
+
effectiveTools: new Set([CHILD_COMPLETION_TOOL]),
|
|
198
|
+
};
|
|
199
|
+
pi.setActiveTools([CHILD_COMPLETION_TOOL]);
|
|
200
|
+
pi.sendUserMessage(COMPLETION_REPAIR_PROMPT, { deliverAs: 'followUp' });
|
|
201
|
+
});
|
|
202
|
+
|
|
178
203
|
pi.on('message_end', (event) => {
|
|
179
204
|
if (!state.activePolicy) return;
|
|
180
205
|
const invalid = invalidCompletionCallIds(
|
|
@@ -5,6 +5,10 @@ import type {
|
|
|
5
5
|
SubagentDelegationResponse,
|
|
6
6
|
SubagentDelegationUpdate,
|
|
7
7
|
} from './protocol-events.ts';
|
|
8
|
+
import type {
|
|
9
|
+
DelegationDiagnostic,
|
|
10
|
+
DelegationDiagnosticCall,
|
|
11
|
+
} from './diagnostics.ts';
|
|
8
12
|
|
|
9
13
|
export type DelegateOptions = {
|
|
10
14
|
readonly signal?: AbortSignal;
|
|
@@ -52,6 +56,7 @@ export function directWorkerResponse(
|
|
|
52
56
|
code: number | null,
|
|
53
57
|
signal: NodeJS.Signals | null,
|
|
54
58
|
stderr: string,
|
|
59
|
+
diagnostic?: DelegationDiagnostic,
|
|
55
60
|
): SubagentDelegationResponse {
|
|
56
61
|
const status = code === 0 ? 'completed' : signal ? 'cancelled' : 'failed';
|
|
57
62
|
return {
|
|
@@ -62,12 +67,15 @@ export function directWorkerResponse(
|
|
|
62
67
|
...(status !== 'completed' && stderr.trim()
|
|
63
68
|
? { error: stderr.trim().slice(-4_000) }
|
|
64
69
|
: {}),
|
|
70
|
+
...(diagnostic ? { diagnostic } : {}),
|
|
65
71
|
};
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
type WorkerJsonEvent = {
|
|
69
75
|
readonly type?: unknown;
|
|
76
|
+
readonly toolCallId?: unknown;
|
|
70
77
|
readonly toolName?: unknown;
|
|
78
|
+
readonly isError?: unknown;
|
|
71
79
|
readonly args?: unknown;
|
|
72
80
|
readonly message?: { readonly role?: unknown };
|
|
73
81
|
readonly assistantMessageEvent?: {
|
|
@@ -83,6 +91,7 @@ type WorkerProgress = {
|
|
|
83
91
|
};
|
|
84
92
|
|
|
85
93
|
const MAX_PROGRESS_DETAIL_CHARS = 480;
|
|
94
|
+
const MAX_DIAGNOSTIC_CALLS = 64;
|
|
86
95
|
const SECRET_KEY = /authorization|cookie|password|secret|token|api[-_]?key/i;
|
|
87
96
|
|
|
88
97
|
function redactProgressValue(value: unknown, key = ''): unknown {
|
|
@@ -110,6 +119,70 @@ function formatToolCall(toolName: string, args: unknown): string {
|
|
|
110
119
|
return `call ${toolName} ${rendered}`.slice(0, MAX_PROGRESS_DETAIL_CHARS);
|
|
111
120
|
}
|
|
112
121
|
|
|
122
|
+
type MutableDiagnostic = {
|
|
123
|
+
settled: boolean;
|
|
124
|
+
truncated: boolean;
|
|
125
|
+
calls: Map<string, DelegationDiagnosticCall>;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const createDiagnostic = (): MutableDiagnostic => ({
|
|
129
|
+
settled: false,
|
|
130
|
+
truncated: false,
|
|
131
|
+
calls: new Map(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const diagnosticSnapshot = (
|
|
135
|
+
diagnostic: MutableDiagnostic,
|
|
136
|
+
): DelegationDiagnostic => ({
|
|
137
|
+
settled: diagnostic.settled,
|
|
138
|
+
truncated: diagnostic.truncated,
|
|
139
|
+
calls: [...diagnostic.calls.values()],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const recordWorkerDiagnostic = (
|
|
143
|
+
line: string,
|
|
144
|
+
diagnostic: MutableDiagnostic,
|
|
145
|
+
): void => {
|
|
146
|
+
let event: WorkerJsonEvent;
|
|
147
|
+
try {
|
|
148
|
+
const parsed: unknown = JSON.parse(line);
|
|
149
|
+
if (typeof parsed !== 'object' || parsed === null) return;
|
|
150
|
+
event = parsed;
|
|
151
|
+
} catch {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (event.type === 'agent_settled') {
|
|
155
|
+
diagnostic.settled = true;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (
|
|
159
|
+
(event.type !== 'tool_execution_start' &&
|
|
160
|
+
event.type !== 'tool_execution_end') ||
|
|
161
|
+
typeof event.toolName !== 'string' ||
|
|
162
|
+
typeof event.toolCallId !== 'string'
|
|
163
|
+
) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!diagnostic.calls.has(event.toolCallId)) {
|
|
167
|
+
if (diagnostic.calls.size >= MAX_DIAGNOSTIC_CALLS) {
|
|
168
|
+
diagnostic.truncated = true;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
diagnostic.calls.set(event.toolCallId, {
|
|
172
|
+
id: event.toolCallId,
|
|
173
|
+
name: event.toolName,
|
|
174
|
+
state: 'started',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (event.type === 'tool_execution_end') {
|
|
178
|
+
diagnostic.calls.set(event.toolCallId, {
|
|
179
|
+
id: event.toolCallId,
|
|
180
|
+
name: event.toolName,
|
|
181
|
+
state: event.isError === false ? 'completed' : 'failed',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
113
186
|
/** Converts one Pi JSONL event into safe, operator-visible worker progress. */
|
|
114
187
|
export function workerProgressFromJsonLine(
|
|
115
188
|
line: string,
|
|
@@ -211,6 +284,7 @@ export function createSubagentDelegationClient(
|
|
|
211
284
|
let stdoutBuffer = '';
|
|
212
285
|
let toolCount = 0;
|
|
213
286
|
let responseText = '';
|
|
287
|
+
const diagnostic = createDiagnostic();
|
|
214
288
|
const stdoutDecoder = new StringDecoder('utf8');
|
|
215
289
|
const consumeWorkerLines = (): void => {
|
|
216
290
|
while (true) {
|
|
@@ -218,6 +292,7 @@ export function createSubagentDelegationClient(
|
|
|
218
292
|
if (newline === -1) return;
|
|
219
293
|
const line = stdoutBuffer.slice(0, newline);
|
|
220
294
|
stdoutBuffer = stdoutBuffer.slice(newline + 1);
|
|
295
|
+
recordWorkerDiagnostic(line, diagnostic);
|
|
221
296
|
const progress = workerProgressFromJsonLine(
|
|
222
297
|
line,
|
|
223
298
|
request.requestId,
|
|
@@ -250,7 +325,15 @@ export function createSubagentDelegationClient(
|
|
|
250
325
|
consumeWorkerLines();
|
|
251
326
|
if (active?.process === child) active = undefined;
|
|
252
327
|
options.signal?.removeEventListener('abort', abort);
|
|
253
|
-
resolve(
|
|
328
|
+
resolve(
|
|
329
|
+
directWorkerResponse(
|
|
330
|
+
request,
|
|
331
|
+
code,
|
|
332
|
+
signal,
|
|
333
|
+
stderr,
|
|
334
|
+
diagnosticSnapshot(diagnostic),
|
|
335
|
+
),
|
|
336
|
+
);
|
|
254
337
|
});
|
|
255
338
|
});
|
|
256
339
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type DiagnosticCallState = 'completed' | 'failed' | 'started';
|
|
2
|
+
|
|
3
|
+
export type DelegationDiagnosticCall = {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly state: DiagnosticCallState;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type DelegationDiagnostic = {
|
|
10
|
+
readonly settled: boolean;
|
|
11
|
+
readonly truncated: boolean;
|
|
12
|
+
readonly calls: ReadonlyArray<DelegationDiagnosticCall>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type RecoverySafety = 'read-only' | 'unsafe' | 'incomplete';
|
|
16
|
+
|
|
17
|
+
const READ_ONLY_TOOLS: ReadonlySet<string> = new Set([
|
|
18
|
+
'read',
|
|
19
|
+
'ls',
|
|
20
|
+
'grep',
|
|
21
|
+
'structured_output',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Decides whether a fresh child may safely repeat a step after same-child
|
|
26
|
+
* completion repair failed. Unknown, partial, and mutation-capable evidence
|
|
27
|
+
* always fails closed.
|
|
28
|
+
*/
|
|
29
|
+
export const classifyRecoverySafety = (
|
|
30
|
+
diagnostic: DelegationDiagnostic | undefined,
|
|
31
|
+
): RecoverySafety => {
|
|
32
|
+
if (!diagnostic || !diagnostic.settled || diagnostic.truncated) {
|
|
33
|
+
return 'incomplete';
|
|
34
|
+
}
|
|
35
|
+
if (
|
|
36
|
+
diagnostic.calls.some(
|
|
37
|
+
(call) => call.state !== 'completed' || !READ_ONLY_TOOLS.has(call.name),
|
|
38
|
+
)
|
|
39
|
+
) {
|
|
40
|
+
return 'unsafe';
|
|
41
|
+
}
|
|
42
|
+
return 'read-only';
|
|
43
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { DelegationDiagnostic } from './diagnostics.ts';
|
|
2
|
+
|
|
1
3
|
export const SUBAGENT_DELEGATION_PROTOCOL_VERSION = 1 as const;
|
|
2
4
|
export const SUBAGENT_DELEGATION_REQUEST_EVENT =
|
|
3
5
|
'prompt-template:subagent:request';
|
|
@@ -41,4 +43,5 @@ export type SubagentDelegationResponse = {
|
|
|
41
43
|
readonly error?: string;
|
|
42
44
|
readonly exitCode?: number;
|
|
43
45
|
readonly warnings?: ReadonlyArray<string>;
|
|
46
|
+
readonly diagnostic?: DelegationDiagnostic;
|
|
44
47
|
};
|