@akagilnc/pi-workflow-roles 0.1.2091 → 0.1.2096
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 +8 -1
- package/README.zh-CN.md +8 -1
- package/dist/engine-detour-tool.js +57 -4
- package/dist/engine-detour.js +53 -4
- package/dist/engine-labor-fallback.js +121 -0
- package/dist/evidence-child-executor.js +3 -15
- package/dist/package-owned-tool-idle.js +64 -7
- package/dist/public-cli/main.js +47 -2
- package/package.json +1 -1
- package/resources/engines/cursor.md +7 -0
- package/resources/engines/opus.md +12 -5
- package/src/engine-detour-tool.ts +83 -13
- package/src/engine-detour.ts +55 -4
- package/src/engine-labor-fallback.ts +172 -0
- package/src/evidence-child-executor.ts +3 -15
- package/src/judge-role.ts +11 -3
- package/src/package-owned-tool-idle.ts +73 -7
- package/src/public-cli/settlement.ts +9 -1
- package/src/reviewer-role.ts +13 -5
- package/src/role-runtime.ts +22 -2
|
@@ -14,18 +14,25 @@ The machine entrypoint is `claude`. Run from the role project root. Non-interact
|
|
|
14
14
|
print mode (`-p` / `--print`) is verified available on this host:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
claude -p "YOUR_LABOR_PROMPT"
|
|
17
|
+
claude -p --output-format=stream-json "YOUR_LABOR_PROMPT"
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Pin the Opus model explicitly (`--model opus` verified accepted on this host):
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
claude -p --model opus "YOUR_LABOR_PROMPT"
|
|
23
|
+
claude -p --model opus --output-format=stream-json "YOUR_LABOR_PROMPT"
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
Use `--output-format=stream-json` (verified present on this host) so the package
|
|
27
|
+
idle clock can see subprocess activity while the engine works; take the labor body
|
|
28
|
+
from the final `result` event, not from intermediate stream rows.
|
|
29
|
+
|
|
26
30
|
Prefer `claude --help` on the host over any remembered flag set. Do not wrap this
|
|
27
31
|
engine behind `ak-role` flags.
|
|
28
32
|
|
|
29
|
-
When the package detour tool is available, start exactly one subprocess
|
|
30
|
-
it with argv assembled from this material and the
|
|
31
|
-
labor content to the same session for the existing
|
|
33
|
+
When the package detour tool is available, start exactly one subprocess per
|
|
34
|
+
labor invocation through it with argv assembled from this material and the
|
|
35
|
+
local CLI; return the stdout labor content to the same session for the existing
|
|
36
|
+
typed submission path. One labor turn = one process (not one process for the
|
|
37
|
+
whole role run). If the detour fails, continue labor in-session on the seat main
|
|
38
|
+
road and still submit via the existing typed path.
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Package-owned engine detour tool (#357 T2 / #378).
|
|
2
|
+
* Package-owned engine detour tool (#357 T2 / #378 / #380).
|
|
3
3
|
* Registered by shared role-runtime when Judge|Reviewer + engine activation signal is present.
|
|
4
4
|
* Evidence-child legs install the same definition via customTools (no spawn in role modules).
|
|
5
|
+
* #380: engine process failure soft-returns so the seat rejoins the main road; declaration
|
|
6
|
+
* is recorded once via engine-labor-fallback (no fail-closed reject-leg).
|
|
7
|
+
* Caller AbortSignal cancel propagates without fallback; package-owned idle abort soft-fails.
|
|
5
8
|
*/
|
|
6
9
|
import type {
|
|
7
10
|
AgentToolResult,
|
|
@@ -19,7 +22,16 @@ import {
|
|
|
19
22
|
isEngineDetourFailure,
|
|
20
23
|
runEngineDetourOnce,
|
|
21
24
|
} from "./engine-detour.ts";
|
|
22
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
activationEngineLaborFallbackLatch,
|
|
27
|
+
recordEngineLaborFallback,
|
|
28
|
+
type EngineLaborFallbackField,
|
|
29
|
+
} from "./engine-labor-fallback.ts";
|
|
30
|
+
import {
|
|
31
|
+
isPackageOwnedToolIdleTimeoutError,
|
|
32
|
+
pokePackageOwnedToolIdle,
|
|
33
|
+
wrapPackageOwnedToolDefinition,
|
|
34
|
+
} from "./package-owned-tool-idle.ts";
|
|
23
35
|
|
|
24
36
|
const engineDetourArgsSchema = Type.Object(
|
|
25
37
|
{
|
|
@@ -49,10 +61,55 @@ export type EngineDetourToolRegistration = {
|
|
|
49
61
|
|
|
50
62
|
type EngineDetourLatch = { used: boolean };
|
|
51
63
|
|
|
64
|
+
function seatFallbackToolResult(
|
|
65
|
+
field: EngineLaborFallbackField,
|
|
66
|
+
failure: string,
|
|
67
|
+
): AgentToolResult<unknown> {
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text" as const,
|
|
72
|
+
text:
|
|
73
|
+
`Engine detour failed: ${failure}. Perform the labor in this session (seat main road) and submit via the existing typed path. A mechanical fallback declaration will attach to the typed receipt.`,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
details: {
|
|
77
|
+
tool: ENGINE_DETOUR_TOOL_NAME,
|
|
78
|
+
detourFailed: true,
|
|
79
|
+
...field,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Caller/upper-layer cancel must propagate; idle backstop is seat-fallback, not cancel. */
|
|
85
|
+
function isCallerCancellation(
|
|
86
|
+
error: unknown,
|
|
87
|
+
signal: AbortSignal | undefined,
|
|
88
|
+
): boolean {
|
|
89
|
+
if (isPackageOwnedToolIdleTimeoutError(error)) return false;
|
|
90
|
+
if (
|
|
91
|
+
signal !== undefined &&
|
|
92
|
+
isPackageOwnedToolIdleTimeoutError(signal.reason)
|
|
93
|
+
) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (signal?.aborted === true) return true;
|
|
97
|
+
if (
|
|
98
|
+
typeof error === "object" &&
|
|
99
|
+
error !== null &&
|
|
100
|
+
(error as { name?: unknown }).name === "AbortError"
|
|
101
|
+
) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
52
107
|
/**
|
|
53
108
|
* Build one once-latch detour tool definition for a configured engine name.
|
|
54
109
|
* `latch` is shared so parent registration can reset between activations.
|
|
55
|
-
* `fail` owns host abort (parent) vs throw (evidence child).
|
|
110
|
+
* `fail` owns host abort (parent) vs throw (evidence child) for tool misuse only.
|
|
111
|
+
* Engine process failure (nonzero/empty/spawn/idle-timeout) soft-returns seat fallback (#380).
|
|
112
|
+
* Caller AbortSignal cancel propagates without writing fallback.
|
|
56
113
|
*/
|
|
57
114
|
export function createEngineDetourToolDefinition(input: {
|
|
58
115
|
engineName: string;
|
|
@@ -71,6 +128,7 @@ export function createEngineDetourToolDefinition(input: {
|
|
|
71
128
|
`Use ${ENGINE_DETOUR_TOOL_NAME} exactly once for the configured engine (${engineName}). Optional packaged notes are guidance when present; a bare engine name alone is also a valid call path.`,
|
|
72
129
|
"Pass argv for the host CLI of this engine name — first element is the executable name on PATH. Follow optional packaged notes when delivered; otherwise act from the engine name and the host CLI actual interface. Do not invent package flags.",
|
|
73
130
|
"On success, use the returned stdout as labor content for the existing typed submission / report path.",
|
|
131
|
+
"On engine failure the tool returns a soft failure: continue labor in this session and submit via the existing typed path. Do not treat engine failure as a reason to withhold the typed receipt.",
|
|
74
132
|
],
|
|
75
133
|
parameters: engineDetourArgsSchema,
|
|
76
134
|
async execute(
|
|
@@ -99,27 +157,39 @@ export function createEngineDetourToolDefinition(input: {
|
|
|
99
157
|
);
|
|
100
158
|
}
|
|
101
159
|
|
|
160
|
+
const softFail = (failure: string): AgentToolResult<unknown> => {
|
|
161
|
+
// Activation-scoped latch is the sole shared recorder (parent seat + legs).
|
|
162
|
+
const fallbackLatch =
|
|
163
|
+
activationEngineLaborFallbackLatch() ?? { field: undefined };
|
|
164
|
+
const field = recordEngineLaborFallback(fallbackLatch, {
|
|
165
|
+
engine: engineName,
|
|
166
|
+
failure,
|
|
167
|
+
});
|
|
168
|
+
return seatFallbackToolResult(field, failure);
|
|
169
|
+
};
|
|
170
|
+
|
|
102
171
|
let result: Awaited<ReturnType<typeof runEngineDetourOnce>>;
|
|
103
172
|
try {
|
|
173
|
+
// Byte activity on stdout/stderr touches the outer package-owned idle clock
|
|
174
|
+
// (183s silence law unchanged). True hangs still die; slow streaming engines live.
|
|
104
175
|
result = await runEngineDetourOnce({
|
|
105
176
|
argv,
|
|
106
177
|
cwd: ctx.cwd,
|
|
107
178
|
...(signal === undefined ? {} : { signal }),
|
|
179
|
+
onOutputActivity: pokePackageOwnedToolIdle,
|
|
108
180
|
});
|
|
109
181
|
} catch (error) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
182
|
+
// Caller cancel: propagate. Idle backstop + spawn/engine failure: seat fallback.
|
|
183
|
+
if (isCallerCancellation(error, signal)) {
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
const failure =
|
|
187
|
+
error instanceof Error ? error.message : String(error);
|
|
188
|
+
return softFail(failure.trim() === "" ? "engine detour spawn failed" : failure);
|
|
115
189
|
}
|
|
116
190
|
|
|
117
191
|
if (isEngineDetourFailure(result)) {
|
|
118
|
-
|
|
119
|
-
new Error(engineDetourFailureDiagnostic(result)),
|
|
120
|
-
toolCallId,
|
|
121
|
-
ctx,
|
|
122
|
-
);
|
|
192
|
+
return softFail(engineDetourFailureDiagnostic(result));
|
|
123
193
|
}
|
|
124
194
|
|
|
125
195
|
return {
|
package/src/engine-detour.ts
CHANGED
|
@@ -28,11 +28,28 @@ export type EngineDetourRunInput = Readonly<{
|
|
|
28
28
|
cwd: string;
|
|
29
29
|
env?: NodeJS.ProcessEnv;
|
|
30
30
|
signal?: AbortSignal;
|
|
31
|
+
/**
|
|
32
|
+
* Called when the child emits at least one byte on stdout or stderr.
|
|
33
|
+
* Host wires this to the package-owned idle clock (activity = touch); no timer here.
|
|
34
|
+
*/
|
|
35
|
+
onOutputActivity?: () => void;
|
|
31
36
|
}>;
|
|
32
37
|
|
|
38
|
+
function abortReasonError(signal: AbortSignal): Error {
|
|
39
|
+
const reason = signal.reason;
|
|
40
|
+
if (reason instanceof Error) return reason;
|
|
41
|
+
if (typeof reason === "string" && reason.trim() !== "") {
|
|
42
|
+
return new Error(reason);
|
|
43
|
+
}
|
|
44
|
+
const error = new Error("aborted");
|
|
45
|
+
error.name = "AbortError";
|
|
46
|
+
return error;
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
/**
|
|
34
50
|
* Run one engine subprocess. First argv element is the executable (PATH lookup).
|
|
35
51
|
* stdio: ignore stdin, pipe stdout+stderr. No shell, no retry, no hang timer.
|
|
52
|
+
* AbortSignal cancels the child immediately via an explicit listener (reason preserved).
|
|
36
53
|
*/
|
|
37
54
|
export async function runEngineDetourOnce(
|
|
38
55
|
input: EngineDetourRunInput,
|
|
@@ -44,30 +61,64 @@ export async function runEngineDetourOnce(
|
|
|
44
61
|
const args = input.argv.slice(1);
|
|
45
62
|
return await new Promise<EngineDetourResult>((resolve, reject) => {
|
|
46
63
|
let settled = false;
|
|
64
|
+
const signal = input.signal;
|
|
65
|
+
// Own abort→kill explicitly so rejection preserves signal.reason (caller cancel
|
|
66
|
+
// vs package-owned idle). Do not pass `signal` to spawn (Node replaces reason).
|
|
47
67
|
const child = spawn(command, args, {
|
|
48
68
|
cwd: input.cwd,
|
|
49
69
|
env: input.env ?? process.env,
|
|
50
70
|
stdio: ["ignore", "pipe", "pipe"],
|
|
51
|
-
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
52
71
|
});
|
|
53
72
|
let stdout = "";
|
|
54
73
|
let stderr = "";
|
|
74
|
+
const noteActivity = (chunk: string): void => {
|
|
75
|
+
if (chunk.length === 0) return;
|
|
76
|
+
input.onOutputActivity?.();
|
|
77
|
+
};
|
|
55
78
|
child.stdout.setEncoding("utf8").on("data", (chunk: string) => {
|
|
56
79
|
stdout += chunk;
|
|
80
|
+
noteActivity(chunk);
|
|
57
81
|
});
|
|
58
82
|
child.stderr.setEncoding("utf8").on("data", (chunk: string) => {
|
|
59
83
|
stderr += chunk;
|
|
84
|
+
noteActivity(chunk);
|
|
60
85
|
});
|
|
61
86
|
const fail = (error: unknown): void => {
|
|
62
87
|
if (settled) return;
|
|
63
88
|
settled = true;
|
|
89
|
+
if (signal !== undefined) {
|
|
90
|
+
signal.removeEventListener("abort", onAbort);
|
|
91
|
+
}
|
|
64
92
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
65
93
|
};
|
|
66
|
-
|
|
67
|
-
child.on("close", (code) => {
|
|
94
|
+
const succeed = (result: EngineDetourResult): void => {
|
|
68
95
|
if (settled) return;
|
|
69
96
|
settled = true;
|
|
70
|
-
|
|
97
|
+
if (signal !== undefined) {
|
|
98
|
+
signal.removeEventListener("abort", onAbort);
|
|
99
|
+
}
|
|
100
|
+
resolve(result);
|
|
101
|
+
};
|
|
102
|
+
const onAbort = (): void => {
|
|
103
|
+
// Fail synchronously so cooperative idle/cancel paths can soft-settle
|
|
104
|
+
// before the outer package-owned idle hard-reject drain.
|
|
105
|
+
fail(signal !== undefined ? abortReasonError(signal) : new Error("aborted"));
|
|
106
|
+
try {
|
|
107
|
+
child.kill("SIGTERM");
|
|
108
|
+
} catch {
|
|
109
|
+
// already exited
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
if (signal !== undefined) {
|
|
113
|
+
if (signal.aborted) {
|
|
114
|
+
onAbort();
|
|
115
|
+
} else {
|
|
116
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
child.on("error", (error) => fail(error));
|
|
120
|
+
child.on("close", (code) => {
|
|
121
|
+
succeed({ code: code ?? 1, stdout, stderr });
|
|
71
122
|
});
|
|
72
123
|
});
|
|
73
124
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #380 — sole shared seat-fallback declaration after engine detour failure.
|
|
3
|
+
* Detour rejoins the main road (ADR 0069); silence is the only crime.
|
|
4
|
+
* Construction of the typed receipt field lives in exactly one site (S1).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type EngineLaborFallback = Readonly<{
|
|
8
|
+
engine: string;
|
|
9
|
+
failure: string;
|
|
10
|
+
laborBy: "seat";
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
/** Activation-scoped latch holder (one parent seat activation at a time). */
|
|
14
|
+
let activationLatch: EngineLaborFallbackLatch | undefined;
|
|
15
|
+
|
|
16
|
+
/** Sole construction site for typed receipt field `engineLaborFallback` (#380 S1). */
|
|
17
|
+
export function buildEngineLaborFallbackField(input: {
|
|
18
|
+
readonly engine: string;
|
|
19
|
+
readonly failure: string;
|
|
20
|
+
}) {
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
engineLaborFallback: Object.freeze({
|
|
23
|
+
engine: input.engine,
|
|
24
|
+
failure: input.failure,
|
|
25
|
+
laborBy: "seat" as const,
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Frozen field bag produced by the sole construction site. */
|
|
31
|
+
export type EngineLaborFallbackField = ReturnType<
|
|
32
|
+
typeof buildEngineLaborFallbackField
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
export type EngineLaborFallbackLatch = {
|
|
36
|
+
field: EngineLaborFallbackField | undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function createEngineLaborFallbackLatch(): EngineLaborFallbackLatch {
|
|
40
|
+
return { field: undefined };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Record first detour failure for this latch (first wins; parallel legs share one field).
|
|
45
|
+
* Always returns the latched first-wins value so tool details and receipt projection match.
|
|
46
|
+
*/
|
|
47
|
+
export function recordEngineLaborFallback(
|
|
48
|
+
latch: EngineLaborFallbackLatch,
|
|
49
|
+
input: { readonly engine: string; readonly failure: string },
|
|
50
|
+
): EngineLaborFallbackField {
|
|
51
|
+
const field = buildEngineLaborFallbackField(input);
|
|
52
|
+
if (latch.field === undefined) latch.field = field;
|
|
53
|
+
return latch.field;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function readEngineLaborFallbackField(
|
|
57
|
+
latch: EngineLaborFallbackLatch | undefined,
|
|
58
|
+
): EngineLaborFallbackField | undefined {
|
|
59
|
+
return latch?.field;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Merge sole-built field into typed receipt details.
|
|
64
|
+
* Spread only — must not construct the field key again (S1).
|
|
65
|
+
* Without a mechanical latch, strip any model-injected reserved key (no forged declaration).
|
|
66
|
+
*/
|
|
67
|
+
export function withEngineLaborFallbackField<T extends object>(
|
|
68
|
+
receipt: T,
|
|
69
|
+
field: EngineLaborFallbackField | undefined,
|
|
70
|
+
): T {
|
|
71
|
+
if (field !== undefined) {
|
|
72
|
+
return { ...receipt, ...field };
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
!Object.prototype.hasOwnProperty.call(receipt, "engineLaborFallback")
|
|
76
|
+
) {
|
|
77
|
+
return receipt;
|
|
78
|
+
}
|
|
79
|
+
const { engineLaborFallback: _forged, ...rest } = receipt as T & {
|
|
80
|
+
engineLaborFallback?: unknown;
|
|
81
|
+
};
|
|
82
|
+
return rest as T;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Install the activation-scoped latch (Judge/Reviewer session_start). */
|
|
86
|
+
export function installActivationEngineLaborFallbackLatch(
|
|
87
|
+
latch: EngineLaborFallbackLatch,
|
|
88
|
+
): void {
|
|
89
|
+
activationLatch = latch;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Clear activation latch (session end / next activation). */
|
|
93
|
+
export function clearActivationEngineLaborFallbackLatch(): void {
|
|
94
|
+
activationLatch = undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Active activation latch, if any (legs inherit parent seat activation). */
|
|
98
|
+
export function activationEngineLaborFallbackLatch():
|
|
99
|
+
| EngineLaborFallbackLatch
|
|
100
|
+
| undefined {
|
|
101
|
+
return activationLatch;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Read fallback field from the active activation latch. */
|
|
105
|
+
export function readActivationEngineLaborFallbackField():
|
|
106
|
+
| EngineLaborFallbackField
|
|
107
|
+
| undefined {
|
|
108
|
+
return readEngineLaborFallbackField(activationLatch);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Read a previously attached declaration from a typed receipt / details object.
|
|
113
|
+
* Rebuilds via the sole construction site — callers must spread, never re-key.
|
|
114
|
+
*/
|
|
115
|
+
export function readEngineLaborFallbackFieldFrom(
|
|
116
|
+
source: unknown,
|
|
117
|
+
): EngineLaborFallbackField | undefined {
|
|
118
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
let raw: unknown;
|
|
122
|
+
try {
|
|
123
|
+
raw = (source as Record<string, unknown>).engineLaborFallback;
|
|
124
|
+
} catch {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return undefined;
|
|
128
|
+
const rec = raw as Record<string, unknown>;
|
|
129
|
+
if (
|
|
130
|
+
typeof rec.engine !== "string" ||
|
|
131
|
+
typeof rec.failure !== "string" ||
|
|
132
|
+
rec.laborBy !== "seat"
|
|
133
|
+
) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
return buildEngineLaborFallbackField({
|
|
137
|
+
engine: rec.engine,
|
|
138
|
+
failure: rec.failure,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Restore activation latch from durable session tool results (#380 resume).
|
|
144
|
+
* Scans existing same-session detour tool results only — no sidecar / new entry type.
|
|
145
|
+
* Replays through recordEngineLaborFallback so first-wins + sole producer stay intact.
|
|
146
|
+
*/
|
|
147
|
+
export function restoreEngineLaborFallbackFromSessionEntries(
|
|
148
|
+
latch: EngineLaborFallbackLatch,
|
|
149
|
+
entries: readonly unknown[],
|
|
150
|
+
toolName: string,
|
|
151
|
+
): void {
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
154
|
+
const row = entry as { type?: unknown; message?: unknown };
|
|
155
|
+
if (row.type !== "message") continue;
|
|
156
|
+
const message = row.message;
|
|
157
|
+
if (typeof message !== "object" || message === null) continue;
|
|
158
|
+
const msg = message as {
|
|
159
|
+
role?: unknown;
|
|
160
|
+
toolName?: unknown;
|
|
161
|
+
details?: unknown;
|
|
162
|
+
};
|
|
163
|
+
if (msg.role !== "toolResult") continue;
|
|
164
|
+
if (msg.toolName !== toolName) continue;
|
|
165
|
+
const field = readEngineLaborFallbackFieldFrom(msg.details);
|
|
166
|
+
if (field === undefined) continue;
|
|
167
|
+
recordEngineLaborFallback(latch, {
|
|
168
|
+
engine: field.engineLaborFallback.engine,
|
|
169
|
+
failure: field.engineLaborFallback.failure,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -596,17 +596,15 @@ export async function executeEvidenceChild(
|
|
|
596
596
|
engine: engineName,
|
|
597
597
|
packageRoot: options.packageRoot,
|
|
598
598
|
});
|
|
599
|
-
// #378: detour
|
|
600
|
-
|
|
599
|
+
// #378/#380: same detour tool as parent seat. Engine process failure soft-returns
|
|
600
|
+
// seat main-road fallback (ADR 0069 detour-rejoins-main-road); tool misuse still throws.
|
|
601
601
|
const engineDetourTool =
|
|
602
602
|
engineName === undefined
|
|
603
603
|
? undefined
|
|
604
604
|
: createEngineDetourToolDefinition({
|
|
605
605
|
engineName,
|
|
606
606
|
fail(error) {
|
|
607
|
-
|
|
608
|
-
engineDetourFailure = failure;
|
|
609
|
-
throw failure;
|
|
607
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
610
608
|
},
|
|
611
609
|
});
|
|
612
610
|
// No tools allowlist — Pi defaults + unrestricted evidence surface (ADR 0064).
|
|
@@ -642,18 +640,8 @@ export async function executeEvidenceChild(
|
|
|
642
640
|
try {
|
|
643
641
|
await session.prompt(delivered);
|
|
644
642
|
} catch (error) {
|
|
645
|
-
// Engine detour fail becomes isError toolResult and must outrank a later
|
|
646
|
-
// provider-shaped throw from the same prompt turn (#378).
|
|
647
|
-
if (engineDetourFailure !== undefined) {
|
|
648
|
-
throw classifiedError(engineDetourFailure, "child");
|
|
649
|
-
}
|
|
650
643
|
throw classifiedError(error, "provider");
|
|
651
644
|
}
|
|
652
|
-
// Launched-leg detour non-zero / empty stdout / spawn failure is infrastructure:
|
|
653
|
-
// reject the leg even when the model still emits a non-blank report afterward.
|
|
654
|
-
if (engineDetourFailure !== undefined) {
|
|
655
|
-
throw classifiedError(engineDetourFailure, "child");
|
|
656
|
-
}
|
|
657
645
|
if (signal?.aborted) throw new Error("Evidence child was cancelled");
|
|
658
646
|
const lastAssistant = [...session.messages]
|
|
659
647
|
.reverse()
|
package/src/judge-role.ts
CHANGED
|
@@ -8,6 +8,10 @@ import { Type, type Static } from "typebox";
|
|
|
8
8
|
|
|
9
9
|
import { disposeComplianceDecision } from "./audit-escalation.ts";
|
|
10
10
|
import type { ComplianceDecision } from "./compliance-transport.ts";
|
|
11
|
+
import {
|
|
12
|
+
readActivationEngineLaborFallbackField,
|
|
13
|
+
withEngineLaborFallbackField,
|
|
14
|
+
} from "./engine-labor-fallback.ts";
|
|
11
15
|
|
|
12
16
|
import {
|
|
13
17
|
JUDGE_OUTPUT_TOOL_NAME,
|
|
@@ -126,18 +130,21 @@ export function createJudgeRoleRuntime(
|
|
|
126
130
|
} catch (error) {
|
|
127
131
|
hostActions.failInfrastructure(error, ctx, toolCallId);
|
|
128
132
|
}
|
|
133
|
+
// #380: attach sole-built engineLaborFallback when detour fell back to seat labor.
|
|
134
|
+
const laborFallback = readActivationEngineLaborFallbackField();
|
|
135
|
+
const acceptedDetails = withEngineLaborFallbackField(verdict, laborFallback);
|
|
129
136
|
return disposeComplianceDecision<AgentToolResult<unknown>>(
|
|
130
137
|
audit,
|
|
131
138
|
{
|
|
132
139
|
pass: (usage) => ({
|
|
133
140
|
content: [{ type: "text" as const, text: "Judge verdict accepted" }],
|
|
134
|
-
details:
|
|
141
|
+
details: acceptedDetails,
|
|
135
142
|
terminate: true as const,
|
|
136
143
|
...(usage === undefined ? {} : { usage }),
|
|
137
144
|
}),
|
|
138
145
|
noReceipt: (auditNoReceipt, usageProjection) => ({
|
|
139
146
|
content: [{ type: "text" as const, text: "Judge verdict accepted; compliance audit produced no receipt" }],
|
|
140
|
-
details: { ...
|
|
147
|
+
details: { ...acceptedDetails, auditNoReceipt },
|
|
141
148
|
terminate: true as const,
|
|
142
149
|
...usageProjection,
|
|
143
150
|
}),
|
|
@@ -149,7 +156,8 @@ export function createJudgeRoleRuntime(
|
|
|
149
156
|
escalate: (result) => result,
|
|
150
157
|
auditIncomplete: (result) => result,
|
|
151
158
|
},
|
|
152
|
-
|
|
159
|
+
// #380: escalate deliveredOutput must carry the same mechanical projection.
|
|
160
|
+
acceptedDetails,
|
|
153
161
|
);
|
|
154
162
|
},
|
|
155
163
|
});
|
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
* Fixed 183000ms silence clock on package-owned tool execute only.
|
|
5
5
|
* Real producing onUpdate resets; final resolve/reject clears; timeout throws so
|
|
6
6
|
* Pi settles the current call as an LLM-visible isError tool result. No retry,
|
|
7
|
-
* role failure, process termination,
|
|
7
|
+
* role failure, process termination, config, or Pi built-in coverage.
|
|
8
8
|
*
|
|
9
9
|
* #339: do not name-exempt whole terminating tools. Outer idle stays armed for
|
|
10
10
|
* pre/post-audit work. Only the real compliance-audit await suspends this single
|
|
11
11
|
* layer (ADR 0059 owns that interval); resume re-arms the same outer backstop.
|
|
12
|
+
*
|
|
13
|
+
* #380: on idle, abort a derived tool signal (not the caller signal) so detour
|
|
14
|
+
* spawn cleanup and softFail can run through the existing AbortSignal path.
|
|
15
|
+
* Cooperative soft-settle may win; non-listeners still hard-reject after a
|
|
16
|
+
* microtask drain (no second timer).
|
|
12
17
|
*/
|
|
13
18
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14
19
|
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
@@ -28,6 +33,8 @@ const WRAPPED = Symbol.for("ak.packageOwnedToolIdleWrapped");
|
|
|
28
33
|
type PackageOwnedToolIdleSuspension = {
|
|
29
34
|
suspend(): void;
|
|
30
35
|
resume(): void;
|
|
36
|
+
/** Reset the silence window (byte/activity signal from the active execute). */
|
|
37
|
+
poke(): void;
|
|
31
38
|
};
|
|
32
39
|
|
|
33
40
|
/**
|
|
@@ -46,6 +53,20 @@ export class PackageOwnedToolIdleTimeoutError extends Error {
|
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
|
|
56
|
+
export function isPackageOwnedToolIdleTimeoutError(
|
|
57
|
+
value: unknown,
|
|
58
|
+
): value is PackageOwnedToolIdleTimeoutError {
|
|
59
|
+
return (
|
|
60
|
+
value instanceof PackageOwnedToolIdleTimeoutError ||
|
|
61
|
+
(
|
|
62
|
+
typeof value === "object" &&
|
|
63
|
+
value !== null &&
|
|
64
|
+
(value as { name?: unknown }).name === "PackageOwnedToolIdleTimeoutError" &&
|
|
65
|
+
(value as { code?: unknown }).code === PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_CODE
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
/** Minimal executable tool shape accepted by the shared idle wrapper. */
|
|
50
71
|
export type PackageOwnedToolLike = {
|
|
51
72
|
readonly name: string;
|
|
@@ -87,6 +108,15 @@ export async function withPackageOwnedToolIdleSuspended<T>(
|
|
|
87
108
|
}
|
|
88
109
|
}
|
|
89
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Touch the active package-owned execute idle clock (reuse stream-idle-guard poke).
|
|
113
|
+
* No-op outside a wrapped package-owned execute or while that layer is suspended.
|
|
114
|
+
* Used by long-running tools (engine detour) that see real subprocess output bytes.
|
|
115
|
+
*/
|
|
116
|
+
export function pokePackageOwnedToolIdle(): void {
|
|
117
|
+
packageOwnedToolIdleScope.getStore()?.poke();
|
|
118
|
+
}
|
|
119
|
+
|
|
90
120
|
/**
|
|
91
121
|
* Single shared execute wrapper for package-owned tool definitions.
|
|
92
122
|
* Idempotent: wrapping twice returns the same protected definition.
|
|
@@ -103,7 +133,7 @@ export function wrapPackageOwnedToolDefinition<T extends PackageOwnedToolLike>(t
|
|
|
103
133
|
const wrappedExecute = function packageOwnedToolIdleExecute(
|
|
104
134
|
...args: unknown[]
|
|
105
135
|
): Promise<unknown> {
|
|
106
|
-
const
|
|
136
|
+
const parentSignal = args[2] as AbortSignal | undefined;
|
|
107
137
|
const onUpdate = args[3] as ((partialResult: unknown) => void) | undefined;
|
|
108
138
|
return new Promise((resolve, reject) => {
|
|
109
139
|
let settled = false;
|
|
@@ -112,15 +142,46 @@ export function wrapPackageOwnedToolDefinition<T extends PackageOwnedToolLike>(t
|
|
|
112
142
|
idleTimeoutMs: PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS,
|
|
113
143
|
});
|
|
114
144
|
|
|
145
|
+
// Derived tool signal: caller cancel forwards; idle aborts this only (not parent).
|
|
146
|
+
const toolController = new AbortController();
|
|
147
|
+
const onParentAbort = (): void => {
|
|
148
|
+
if (toolController.signal.aborted) return;
|
|
149
|
+
toolController.abort(parentSignal?.reason);
|
|
150
|
+
};
|
|
151
|
+
if (parentSignal !== undefined) {
|
|
152
|
+
if (parentSignal.aborted) {
|
|
153
|
+
toolController.abort(parentSignal.reason);
|
|
154
|
+
} else {
|
|
155
|
+
parentSignal.addEventListener("abort", onParentAbort);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const cleanup = (): void => {
|
|
160
|
+
idle.signal.removeEventListener("abort", onIdle);
|
|
161
|
+
idle.dispose();
|
|
162
|
+
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
163
|
+
};
|
|
164
|
+
|
|
115
165
|
const settle = (deliver: () => void): void => {
|
|
116
166
|
if (settled) return;
|
|
117
167
|
settled = true;
|
|
118
|
-
|
|
119
|
-
idle.dispose();
|
|
168
|
+
cleanup();
|
|
120
169
|
deliver();
|
|
121
170
|
};
|
|
122
171
|
const onIdle = (): void => {
|
|
123
|
-
|
|
172
|
+
const idleError = new PackageOwnedToolIdleTimeoutError();
|
|
173
|
+
if (!toolController.signal.aborted) {
|
|
174
|
+
toolController.abort(idleError);
|
|
175
|
+
}
|
|
176
|
+
// Cooperative tools (detour) soft-settle via the aborted signal in the
|
|
177
|
+
// next microtasks. Non-listeners still hard-reject after a short drain
|
|
178
|
+
// of those microtasks — no second wall-clock timer.
|
|
179
|
+
void Promise.resolve()
|
|
180
|
+
.then(() => {})
|
|
181
|
+
.then(() => {})
|
|
182
|
+
.then(() => {
|
|
183
|
+
settle(() => reject(idleError));
|
|
184
|
+
});
|
|
124
185
|
};
|
|
125
186
|
idle.signal.addEventListener("abort", onIdle, { once: true });
|
|
126
187
|
|
|
@@ -141,6 +202,10 @@ export function wrapPackageOwnedToolDefinition<T extends PackageOwnedToolLike>(t
|
|
|
141
202
|
});
|
|
142
203
|
idle.signal.addEventListener("abort", onIdle, { once: true });
|
|
143
204
|
},
|
|
205
|
+
poke(): void {
|
|
206
|
+
if (settled || suspended) return;
|
|
207
|
+
idle.poke();
|
|
208
|
+
},
|
|
144
209
|
};
|
|
145
210
|
|
|
146
211
|
const guardedOnUpdate = onUpdate === undefined
|
|
@@ -152,8 +217,9 @@ export function wrapPackageOwnedToolDefinition<T extends PackageOwnedToolLike>(t
|
|
|
152
217
|
};
|
|
153
218
|
|
|
154
219
|
const callArgs = args.slice();
|
|
155
|
-
//
|
|
156
|
-
|
|
220
|
+
// Pass derived signal so idle/cancel can clean up subprocesses without
|
|
221
|
+
// aborting the caller/parent agent signal identity.
|
|
222
|
+
callArgs[2] = toolController.signal;
|
|
157
223
|
callArgs[3] = guardedOnUpdate;
|
|
158
224
|
|
|
159
225
|
void packageOwnedToolIdleScope.run(suspension, async () => {
|