@wichayutdew/pi-workflows 2.7.0 → 3.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 +1 -2
- package/dist/index.js +121 -8
- package/package.json +1 -1
- package/src/agents/profile.ts +5 -3
- 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/README.md
CHANGED
|
@@ -33,8 +33,7 @@ dictating your language, framework, or delivery process.
|
|
|
33
33
|
- Keep worktree-bound iterations safe, including follow-up enhancements.
|
|
34
34
|
- Enforce declared resources and stop unsafe loops before they run away.
|
|
35
35
|
- Assign a workflow-owned role prompt with `agent: planner`, `worker`,
|
|
36
|
-
`reviewer`, or `scout`; customize profiles under
|
|
37
|
-
directory's `agents/` folder.
|
|
36
|
+
`reviewer`, or `scout`; customize profiles under `~/.agents/agents`.
|
|
38
37
|
|
|
39
38
|
## Herdr workflow status
|
|
40
39
|
|
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
|
};
|
|
@@ -5461,6 +5511,7 @@ function buildMainWorkflowNotice(workflow, run, statusShortcutLabel = "Ctrl+Alt+
|
|
|
5461
5511
|
}
|
|
5462
5512
|
// src/agents/profile.ts
|
|
5463
5513
|
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
5514
|
+
import { homedir as homedir3 } from "node:os";
|
|
5464
5515
|
import { join as join5 } from "node:path";
|
|
5465
5516
|
import { fileURLToPath } from "node:url";
|
|
5466
5517
|
import { parse } from "yaml";
|
|
@@ -5509,8 +5560,9 @@ function parseAgentProfile(source, name) {
|
|
|
5509
5560
|
...typeof thinking === "string" ? { thinking } : {}
|
|
5510
5561
|
};
|
|
5511
5562
|
}
|
|
5512
|
-
|
|
5513
|
-
|
|
5563
|
+
var DEFAULT_AGENT_PROFILE_DIRECTORY = join5(homedir3(), ".agents", "agents");
|
|
5564
|
+
function loadAgentProfile(name, agentProfileDirectory = DEFAULT_AGENT_PROFILE_DIRECTORY) {
|
|
5565
|
+
const userPath = join5(agentProfileDirectory, `${name}.md`);
|
|
5514
5566
|
const bundledUrl = new URL(`../../examples/starter-kit/agents/${name}.md`, import.meta.url);
|
|
5515
5567
|
const path = existsSync2(userPath) ? userPath : fileURLToPath(bundledUrl);
|
|
5516
5568
|
try {
|
|
@@ -6563,6 +6615,26 @@ function createStepExecutionActions() {
|
|
|
6563
6615
|
};
|
|
6564
6616
|
}
|
|
6565
6617
|
|
|
6618
|
+
// src/integrations/subagents/diagnostics.ts
|
|
6619
|
+
var READ_ONLY_TOOLS = new Set([
|
|
6620
|
+
"read",
|
|
6621
|
+
"ls",
|
|
6622
|
+
"grep",
|
|
6623
|
+
"structured_output"
|
|
6624
|
+
]);
|
|
6625
|
+
var classifyRecoverySafety = (diagnostic) => {
|
|
6626
|
+
if (!diagnostic || !diagnostic.settled || diagnostic.truncated) {
|
|
6627
|
+
return "incomplete";
|
|
6628
|
+
}
|
|
6629
|
+
if (diagnostic.calls.some((call) => call.state !== "completed" || !READ_ONLY_TOOLS.has(call.name))) {
|
|
6630
|
+
return "unsafe";
|
|
6631
|
+
}
|
|
6632
|
+
return "read-only";
|
|
6633
|
+
};
|
|
6634
|
+
|
|
6635
|
+
// src/harness/delegation-recovery.ts
|
|
6636
|
+
var shouldRetryMissingCompletion = (diagnostic, subagentAttemptCount) => subagentAttemptCount === 1 && classifyRecoverySafety(diagnostic) === "read-only";
|
|
6637
|
+
|
|
6566
6638
|
// src/harness/delegation-response-actions.ts
|
|
6567
6639
|
function hasErrorCode(error, code) {
|
|
6568
6640
|
return error instanceof Error && "code" in error && error.code === code;
|
|
@@ -6638,7 +6710,15 @@ async function finishDelegation(active, response) {
|
|
|
6638
6710
|
serializedResult = await this.dependencies.readDelegatedResult(active);
|
|
6639
6711
|
} catch (error) {
|
|
6640
6712
|
if (hasErrorCode(error, "ENOENT")) {
|
|
6641
|
-
|
|
6713
|
+
const subagentAttemptCount = this.run.currentStepAttempts?.filter((attempt) => attempt.kind === "subagent").length ?? 0;
|
|
6714
|
+
if (shouldRetryMissingCompletion(response.diagnostic, subagentAttemptCount)) {
|
|
6715
|
+
cleanupAttempted = true;
|
|
6716
|
+
await this.cleanupDelegation(active);
|
|
6717
|
+
this.launchCurrentStep(workflow);
|
|
6718
|
+
return;
|
|
6719
|
+
}
|
|
6720
|
+
const diagnosticState = response.diagnostic ? `settled=${response.diagnostic.settled}, truncated=${response.diagnostic.truncated}, calls=${response.diagnostic.calls.length}` : "unavailable";
|
|
6721
|
+
throw new Error(`Subagent "${active.agent}" completed without producing the required correlated structured_output result (request ${active.requestId}; diagnostic ${diagnosticState})`, { cause: error });
|
|
6642
6722
|
}
|
|
6643
6723
|
throw error;
|
|
6644
6724
|
}
|
|
@@ -7521,6 +7601,24 @@ var DEFAULT_CHILD_RUNTIME_DEPENDENCIES = {
|
|
|
7521
7601
|
tokensAreEqual
|
|
7522
7602
|
};
|
|
7523
7603
|
|
|
7604
|
+
// src/integrations/subagents/child-runtime-repair.ts
|
|
7605
|
+
var COMPLETION_REPAIR_PROMPT = [
|
|
7606
|
+
"The delegated step settled without its required correlated result.",
|
|
7607
|
+
"Do not repeat completed work and do not execute work tools.",
|
|
7608
|
+
"Call `structured_output` exactly once, alone, with one configured outcome and the required summary, artifact, and workspace fields."
|
|
7609
|
+
].join(`
|
|
7610
|
+
`);
|
|
7611
|
+
var needsCompletionRepair = ({
|
|
7612
|
+
policy,
|
|
7613
|
+
dependencies
|
|
7614
|
+
}) => {
|
|
7615
|
+
try {
|
|
7616
|
+
return !dependencies.fileSystem.exists(policy.resultPath);
|
|
7617
|
+
} catch {
|
|
7618
|
+
return false;
|
|
7619
|
+
}
|
|
7620
|
+
};
|
|
7621
|
+
|
|
7524
7622
|
// src/integrations/subagents/child-runtime-files.ts
|
|
7525
7623
|
var verifyChildWorkingDirectory = (policy, dependencies) => {
|
|
7526
7624
|
let expected;
|
|
@@ -7618,7 +7716,8 @@ var INITIAL_STATE = {
|
|
|
7618
7716
|
activePolicy: undefined,
|
|
7619
7717
|
policyError: undefined,
|
|
7620
7718
|
invalidCompletionCalls: new Set,
|
|
7621
|
-
effectiveTools: new Set
|
|
7719
|
+
effectiveTools: new Set,
|
|
7720
|
+
repairRequested: false
|
|
7622
7721
|
};
|
|
7623
7722
|
var errorMessage2 = (error) => error instanceof Error ? error.message : String(error);
|
|
7624
7723
|
var invalidPolicyInput = (pi, policyError, images) => {
|
|
@@ -7684,7 +7783,8 @@ var registerSubagentChildRuntime = (pi, options = {}) => {
|
|
|
7684
7783
|
...state,
|
|
7685
7784
|
activePolicy: extracted.policy,
|
|
7686
7785
|
policyError: undefined,
|
|
7687
|
-
effectiveTools
|
|
7786
|
+
effectiveTools,
|
|
7787
|
+
repairRequested: false
|
|
7688
7788
|
};
|
|
7689
7789
|
} catch (error) {
|
|
7690
7790
|
const policyError = errorMessage2(error);
|
|
@@ -7717,6 +7817,19 @@ ${childSystemPrompt(state.activePolicy)}`
|
|
|
7717
7817
|
pi.on("turn_start", () => {
|
|
7718
7818
|
state = { ...state, invalidCompletionCalls: new Set };
|
|
7719
7819
|
});
|
|
7820
|
+
pi.on("agent_settled", () => {
|
|
7821
|
+
const policy = state.activePolicy;
|
|
7822
|
+
if (!policy || state.repairRequested || !needsCompletionRepair({ policy, dependencies })) {
|
|
7823
|
+
return;
|
|
7824
|
+
}
|
|
7825
|
+
state = {
|
|
7826
|
+
...state,
|
|
7827
|
+
repairRequested: true,
|
|
7828
|
+
effectiveTools: new Set([CHILD_COMPLETION_TOOL])
|
|
7829
|
+
};
|
|
7830
|
+
pi.setActiveTools([CHILD_COMPLETION_TOOL]);
|
|
7831
|
+
pi.sendUserMessage(COMPLETION_REPAIR_PROMPT, { deliverAs: "followUp" });
|
|
7832
|
+
});
|
|
7720
7833
|
pi.on("message_end", (event) => {
|
|
7721
7834
|
if (!state.activePolicy)
|
|
7722
7835
|
return;
|
package/package.json
CHANGED
package/src/agents/profile.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { parse } from 'yaml';
|
|
5
|
-
import { defaultUserWorkflowDirectory } from '../config/load.ts';
|
|
6
6
|
|
|
7
7
|
export const THINKING_LEVELS = [
|
|
8
8
|
'off',
|
|
@@ -74,12 +74,14 @@ export function parseAgentProfile(source: string, name: string): AgentProfile {
|
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
const DEFAULT_AGENT_PROFILE_DIRECTORY = join(homedir(), '.agents', 'agents');
|
|
78
|
+
|
|
77
79
|
/** Loads a user-owned agent profile, with a bundled prompt-only fallback. */
|
|
78
80
|
export function loadAgentProfile(
|
|
79
81
|
name: string,
|
|
80
|
-
|
|
82
|
+
agentProfileDirectory = DEFAULT_AGENT_PROFILE_DIRECTORY,
|
|
81
83
|
): AgentProfile {
|
|
82
|
-
const userPath = join(
|
|
84
|
+
const userPath = join(agentProfileDirectory, `${name}.md`);
|
|
83
85
|
const bundledUrl = new URL(
|
|
84
86
|
`../../examples/starter-kit/agents/${name}.md`,
|
|
85
87
|
import.meta.url,
|
|
@@ -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
|
};
|