@akagilnc/pi-workflow-roles 0.1.4239 → 0.1.4259
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 -1
- package/README.zh-CN.md +1 -1
- package/dist/acp-host/production-host.js +547 -221
- package/dist/engine-detour-tool.js +64 -5
- package/dist/engine-detour-usage.js +285 -0
- package/dist/headless-host/production-host.js +557 -231
- package/dist/pi/role-turn-host.js +10 -0
- package/dist/public-cli/main.js +433 -202
- package/dist/public-cli/post-admission.js +72 -43
- package/dist/public-cli/settlement.js +80 -24
- package/dist/public-cli/turn-request.js +3 -0
- package/package.json +1 -1
- package/src/engine-detour-tool.ts +106 -8
- package/src/engine-detour-usage.ts +373 -0
- package/src/host-contracts.ts +12 -1
- package/src/pi/adapter.ts +6 -0
- package/src/pi/role-turn-host.ts +6 -0
- package/src/public-cli/post-admission.ts +91 -49
- package/src/public-cli/settlement.ts +148 -23
- package/src/public-cli/turn-request.ts +5 -0
- package/src/role-envelope.ts +2 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
1
2
|
import { Type } from "typebox";
|
|
2
3
|
import {
|
|
3
4
|
ENGINE_DETOUR_TOOL_NAME,
|
|
@@ -7,6 +8,16 @@ import {
|
|
|
7
8
|
resolveEngineName,
|
|
8
9
|
runEngineDetourOnce
|
|
9
10
|
} from "./engine-detour.js";
|
|
11
|
+
import {
|
|
12
|
+
engineDetourStdoutByteLength,
|
|
13
|
+
reportEngineDetourCall
|
|
14
|
+
} from "./engine-detour-usage.js";
|
|
15
|
+
function basenameRunId(runDirectory) {
|
|
16
|
+
const leaf = basename(runDirectory);
|
|
17
|
+
const at = leaf.indexOf("@");
|
|
18
|
+
if (at <= 0) return void 0;
|
|
19
|
+
return leaf.slice(0, at);
|
|
20
|
+
}
|
|
10
21
|
const engineDetourArgsSchema = Type.Object(
|
|
11
22
|
{
|
|
12
23
|
argv: Type.Array(Type.String({ minLength: 1 }), {
|
|
@@ -23,6 +34,9 @@ function isCallerCancellation(error, signal) {
|
|
|
23
34
|
}
|
|
24
35
|
return false;
|
|
25
36
|
}
|
|
37
|
+
function asError(error, fallback) {
|
|
38
|
+
return error instanceof Error ? error : new Error(String(error).trim() || fallback);
|
|
39
|
+
}
|
|
26
40
|
function createEngineDetourToolDefinition(input) {
|
|
27
41
|
const engineName = input.engineName;
|
|
28
42
|
const engineModel = input.engineModel;
|
|
@@ -43,6 +57,45 @@ function createEngineDetourToolDefinition(input) {
|
|
|
43
57
|
ctx
|
|
44
58
|
);
|
|
45
59
|
}
|
|
60
|
+
const sessionParent = ctx.sessionManager?.getSessionFile?.();
|
|
61
|
+
const startedAt = Date.now();
|
|
62
|
+
const runDirectory = typeof ctx.runDirectory === "string" && ctx.runDirectory.length > 0 ? ctx.runDirectory : void 0;
|
|
63
|
+
const runId = runDirectory === void 0 ? void 0 : basenameRunId(runDirectory);
|
|
64
|
+
const invocationScopeId = typeof ctx.invocationScopeId === "string" && ctx.invocationScopeId.trim() !== "" ? ctx.invocationScopeId.trim() : void 0;
|
|
65
|
+
const host = typeof ctx.host === "string" && ctx.host.trim() !== "" ? ctx.host.trim() : void 0;
|
|
66
|
+
const recordCall = (observed2) => {
|
|
67
|
+
if (typeof sessionParent !== "string" || sessionParent.length === 0) return;
|
|
68
|
+
reportEngineDetourCall({
|
|
69
|
+
toolCallId,
|
|
70
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
71
|
+
cwd: ctx.cwd,
|
|
72
|
+
sessionParent,
|
|
73
|
+
...runId === void 0 ? {} : { runId },
|
|
74
|
+
...invocationScopeId === void 0 ? {} : { invocationScopeId },
|
|
75
|
+
...host === void 0 ? {} : { host },
|
|
76
|
+
...observed2.code === void 0 ? {} : { code: observed2.code },
|
|
77
|
+
...observed2.stdoutByteLength === void 0 ? {} : { stdoutByteLength: observed2.stdoutByteLength }
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
const failAfterLedger = (engineCause, observed2, aggregateMessage) => {
|
|
81
|
+
try {
|
|
82
|
+
recordCall(observed2);
|
|
83
|
+
} catch (recordError) {
|
|
84
|
+
input.fail(
|
|
85
|
+
new AggregateError(
|
|
86
|
+
[
|
|
87
|
+
engineCause,
|
|
88
|
+
asError(recordError, "engine detour usage ledger write failed")
|
|
89
|
+
],
|
|
90
|
+
aggregateMessage,
|
|
91
|
+
{ cause: engineCause }
|
|
92
|
+
),
|
|
93
|
+
toolCallId,
|
|
94
|
+
ctx
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
input.fail(engineCause, toolCallId, ctx);
|
|
98
|
+
};
|
|
46
99
|
let result;
|
|
47
100
|
try {
|
|
48
101
|
result = await runEngineDetourOnce({
|
|
@@ -52,16 +105,22 @@ function createEngineDetourToolDefinition(input) {
|
|
|
52
105
|
});
|
|
53
106
|
} catch (error) {
|
|
54
107
|
if (isCallerCancellation(error, signal)) throw error;
|
|
55
|
-
|
|
56
|
-
|
|
108
|
+
return failAfterLedger(
|
|
109
|
+
asError(error, "\u52B3\u52A1\u5F15\u64CE spawn \u5931\u8D25"),
|
|
110
|
+
{},
|
|
111
|
+
"engine detour spawn and usage ledger both failed"
|
|
112
|
+
);
|
|
57
113
|
}
|
|
114
|
+
const stdoutByteLength = engineDetourStdoutByteLength(result.stdout);
|
|
115
|
+
const observed = { code: result.code, stdoutByteLength };
|
|
58
116
|
if (isEngineDetourFailure(result)) {
|
|
59
|
-
|
|
117
|
+
return failAfterLedger(
|
|
60
118
|
new Error(engineDetourFailureDiagnostic(result)),
|
|
61
|
-
|
|
62
|
-
|
|
119
|
+
observed,
|
|
120
|
+
"engine detour child-close and usage ledger both failed"
|
|
63
121
|
);
|
|
64
122
|
}
|
|
123
|
+
recordCall(observed);
|
|
65
124
|
return {
|
|
66
125
|
content: [{ type: "text", text: result.stdout }],
|
|
67
126
|
details: {
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #537 typed ak_engine_detour tool usage ledger.
|
|
3
|
+
*
|
|
4
|
+
* Observes only the package detour tool path. Bash/CLI ordinary path
|
|
5
|
+
* (resources/engine-dispatch.md ordinary path) stays outside this ledger — a
|
|
6
|
+
* permanent blind spot that must never be read as "the seat did not use an engine".
|
|
7
|
+
*
|
|
8
|
+
* Runtime owns these facts beside role payloads (ADR 0042); they land in
|
|
9
|
+
* TerminalResult.decisiveFacts and are written live via sitian (ADR 0077).
|
|
10
|
+
* No gate, no required field, no index bytes (ADR 0049 / 0057).
|
|
11
|
+
*
|
|
12
|
+
* Invocation scope is one public ak-role call (#537), owned by the shared Host
|
|
13
|
+
* execution envelope (RoleTurnRequest / HostContext). Auto-resume attempts inside
|
|
14
|
+
* that call share the same scope; only an explicit new public call (including
|
|
15
|
+
* `ak-role resume`) mints a new one. Never courtAttemptId, never Pi session
|
|
16
|
+
* toolResult join keys, and never a detour-owned sidecar file.
|
|
17
|
+
*/
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import { ENGINE_DETOUR_TOOL_NAME } from "./engine-detour.js";
|
|
22
|
+
import { readSitianRecords, resolveSitianRecordPath, sitianReport, } from "./sitian-facade.js";
|
|
23
|
+
/** Sitian event kind for one detour-tool call (volume under session/). */
|
|
24
|
+
export const ENGINE_DETOUR_CALL_KIND = "engine-detour-call";
|
|
25
|
+
/** decisiveFacts key — name states detour-tool scope, not full engine usage. */
|
|
26
|
+
export const ENGINE_DETOUR_TOOL_USAGE_FACT_KEY = "engineDetourToolUsage";
|
|
27
|
+
/**
|
|
28
|
+
* Run-relative sitian volume path for one detour call.
|
|
29
|
+
* Openable once the run directory is known (resume.command / top-level runId);
|
|
30
|
+
* contains no runId bytes itself (#108 + #537 AC8).
|
|
31
|
+
*/
|
|
32
|
+
export const ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE = `session/${ENGINE_DETOUR_CALL_KIND}/records.jsonl`;
|
|
33
|
+
/** stdout UTF-8 byte length (ticket-frozen metric; empty stdout is real 0). */
|
|
34
|
+
export function engineDetourStdoutByteLength(stdout) {
|
|
35
|
+
return Buffer.byteLength(stdout, "utf8");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Deterministic sitian identity: invocation scope + toolCallId.
|
|
39
|
+
* Must not embed runId — public Terminal decisiveFacts re-expose identity (#108).
|
|
40
|
+
*/
|
|
41
|
+
export function engineDetourCallIdentity(input) {
|
|
42
|
+
const scope = input.invocationScopeId ?? "";
|
|
43
|
+
return `engine-detour-call:${scope}:${input.toolCallId}`;
|
|
44
|
+
}
|
|
45
|
+
/** Live sitian write for one detour call. Returns the call fact with pointer. */
|
|
46
|
+
export function reportEngineDetourCall(input) {
|
|
47
|
+
const payload = {
|
|
48
|
+
tool: ENGINE_DETOUR_TOOL_NAME,
|
|
49
|
+
toolCallId: input.toolCallId,
|
|
50
|
+
durationMs: input.durationMs,
|
|
51
|
+
};
|
|
52
|
+
if (input.code !== undefined)
|
|
53
|
+
payload.code = input.code;
|
|
54
|
+
if (input.stdoutByteLength !== undefined) {
|
|
55
|
+
payload.stdoutByteLength = input.stdoutByteLength;
|
|
56
|
+
}
|
|
57
|
+
if (input.invocationScopeId !== undefined) {
|
|
58
|
+
payload.invocationScopeId = input.invocationScopeId;
|
|
59
|
+
}
|
|
60
|
+
if (input.runId !== undefined)
|
|
61
|
+
payload.runId = input.runId;
|
|
62
|
+
// SitianSubject object form requires runId; invocation scope rides payload always.
|
|
63
|
+
const subject = input.runId === undefined
|
|
64
|
+
? undefined
|
|
65
|
+
: {
|
|
66
|
+
runId: input.runId,
|
|
67
|
+
...(input.invocationScopeId === undefined
|
|
68
|
+
? {}
|
|
69
|
+
: { invocationScopeId: input.invocationScopeId }),
|
|
70
|
+
};
|
|
71
|
+
const pointer = sitianReport({
|
|
72
|
+
level: "event",
|
|
73
|
+
kind: ENGINE_DETOUR_CALL_KIND,
|
|
74
|
+
identity: engineDetourCallIdentity({
|
|
75
|
+
toolCallId: input.toolCallId,
|
|
76
|
+
...(input.invocationScopeId === undefined
|
|
77
|
+
? {}
|
|
78
|
+
: { invocationScopeId: input.invocationScopeId }),
|
|
79
|
+
}),
|
|
80
|
+
cwd: input.cwd,
|
|
81
|
+
sessionParent: input.sessionParent,
|
|
82
|
+
source: "engine-detour-tool",
|
|
83
|
+
payload,
|
|
84
|
+
raw: {
|
|
85
|
+
sessionFile: input.sessionParent,
|
|
86
|
+
entryId: input.toolCallId,
|
|
87
|
+
},
|
|
88
|
+
...(input.home === undefined ? {} : { home: input.home }),
|
|
89
|
+
...(input.host === undefined ? {} : { host: input.host }),
|
|
90
|
+
...(subject === undefined ? {} : { subject }),
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
toolCallId: input.toolCallId,
|
|
94
|
+
durationMs: input.durationMs,
|
|
95
|
+
...(input.code === undefined ? {} : { code: input.code }),
|
|
96
|
+
...(input.stdoutByteLength === undefined
|
|
97
|
+
? {}
|
|
98
|
+
: { stdoutByteLength: input.stdoutByteLength }),
|
|
99
|
+
recordPointer: pointer,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function isRecord(value) {
|
|
103
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
104
|
+
}
|
|
105
|
+
function callFactFromSitianPayload(payload, pointer) {
|
|
106
|
+
if (!isRecord(payload))
|
|
107
|
+
return undefined;
|
|
108
|
+
if (payload.tool !== ENGINE_DETOUR_TOOL_NAME)
|
|
109
|
+
return undefined;
|
|
110
|
+
if (typeof payload.toolCallId !== "string" || payload.toolCallId.length === 0) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
if (typeof payload.durationMs !== "number" || !Number.isFinite(payload.durationMs)) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
toolCallId: payload.toolCallId,
|
|
118
|
+
durationMs: payload.durationMs,
|
|
119
|
+
...(typeof payload.code === "number" ? { code: payload.code } : {}),
|
|
120
|
+
...(typeof payload.stdoutByteLength === "number"
|
|
121
|
+
? { stdoutByteLength: payload.stdoutByteLength }
|
|
122
|
+
: {}),
|
|
123
|
+
recordPointer: pointer,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function invocationScopeIdOfRecord(record) {
|
|
127
|
+
if (isRecord(record.subject)) {
|
|
128
|
+
const fromSubject = record.subject.invocationScopeId;
|
|
129
|
+
if (typeof fromSubject === "string" && fromSubject.length > 0)
|
|
130
|
+
return fromSubject;
|
|
131
|
+
}
|
|
132
|
+
if (isRecord(record.payload)) {
|
|
133
|
+
const fromPayload = record.payload.invocationScopeId;
|
|
134
|
+
if (typeof fromPayload === "string" && fromPayload.length > 0)
|
|
135
|
+
return fromPayload;
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Read this-invocation detour-tool usage from sitian volume.
|
|
141
|
+
* When `invocationScopeId` is provided, only records bound to that public call count
|
|
142
|
+
* (host-neutral boundary — not session toolResult join keys, not courtAttemptId).
|
|
143
|
+
* When engineMounted is false, returns undefined (field absent).
|
|
144
|
+
* When engineMounted is true and no calls, returns callCount 0.
|
|
145
|
+
*/
|
|
146
|
+
export async function readEngineDetourToolUsage(input) {
|
|
147
|
+
if (!input.engineMounted)
|
|
148
|
+
return undefined;
|
|
149
|
+
const { recordFile } = resolveSitianRecordPath({
|
|
150
|
+
level: "event",
|
|
151
|
+
kind: ENGINE_DETOUR_CALL_KIND,
|
|
152
|
+
sessionParent: input.sessionParent,
|
|
153
|
+
...(input.home === undefined ? {} : { home: input.home }),
|
|
154
|
+
...(input.cwd === undefined ? {} : { cwd: input.cwd }),
|
|
155
|
+
});
|
|
156
|
+
const { records } = await readSitianRecords(recordFile);
|
|
157
|
+
const calls = [];
|
|
158
|
+
for (const record of records) {
|
|
159
|
+
if (record.kind !== ENGINE_DETOUR_CALL_KIND)
|
|
160
|
+
continue;
|
|
161
|
+
const boundScope = invocationScopeIdOfRecord(record);
|
|
162
|
+
if (input.invocationScopeId !== undefined && input.invocationScopeId.length > 0) {
|
|
163
|
+
if (boundScope !== input.invocationScopeId)
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
else if (boundScope !== undefined) {
|
|
167
|
+
// Unscoped settlement must not pull invocation-bound rows across resume.
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const pointer = {
|
|
171
|
+
identity: record.identity,
|
|
172
|
+
recordFile,
|
|
173
|
+
kind: record.kind,
|
|
174
|
+
level: record.level,
|
|
175
|
+
};
|
|
176
|
+
const fact = callFactFromSitianPayload(record.payload, pointer);
|
|
177
|
+
if (fact === undefined)
|
|
178
|
+
continue;
|
|
179
|
+
calls.push(fact);
|
|
180
|
+
}
|
|
181
|
+
return { callCount: calls.length, calls };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Project usage onto the public Terminal face.
|
|
185
|
+
* Non-resumable: keep absolute recordFile (runId already public via top-level runId).
|
|
186
|
+
* Resumable: keep an openable run-relative recordFile and identity with no runId
|
|
187
|
+
* bytes (#108 single disclosure + #537 AC8 reopen).
|
|
188
|
+
*/
|
|
189
|
+
export function projectEngineDetourToolUsageForPublicTerminal(usage, options) {
|
|
190
|
+
if (options.discloseRecordFile)
|
|
191
|
+
return usage;
|
|
192
|
+
return {
|
|
193
|
+
callCount: usage.callCount,
|
|
194
|
+
calls: usage.calls.map((call) => ({
|
|
195
|
+
toolCallId: call.toolCallId,
|
|
196
|
+
durationMs: call.durationMs,
|
|
197
|
+
...(call.code === undefined ? {} : { code: call.code }),
|
|
198
|
+
...(call.stdoutByteLength === undefined
|
|
199
|
+
? {}
|
|
200
|
+
: { stdoutByteLength: call.stdoutByteLength }),
|
|
201
|
+
recordPointer: {
|
|
202
|
+
identity: call.recordPointer.identity,
|
|
203
|
+
kind: call.recordPointer.kind,
|
|
204
|
+
level: call.recordPointer.level,
|
|
205
|
+
recordFile: ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE,
|
|
206
|
+
},
|
|
207
|
+
})),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/** One shared invocation.json reader (ENOENT → undefined; other errors propagate). */
|
|
211
|
+
function readInvocationRecord(runDirectory) {
|
|
212
|
+
try {
|
|
213
|
+
const raw = JSON.parse(readFileSync(join(runDirectory, "invocation.json"), "utf8"));
|
|
214
|
+
return isRecord(raw) ? raw : undefined;
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (error?.code === "ENOENT")
|
|
218
|
+
return undefined;
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** True when invocation.json carries a non-empty engine axis. */
|
|
223
|
+
export async function readInvocationEngineMounted(runDirectory) {
|
|
224
|
+
const raw = readInvocationRecord(runDirectory);
|
|
225
|
+
if (raw === undefined)
|
|
226
|
+
return false;
|
|
227
|
+
return typeof raw.engine === "string" && raw.engine.trim() !== "";
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Selected host from the admission invocation page — for post-admission
|
|
231
|
+
* host-transition classification only. In-turn tools take host from the shared
|
|
232
|
+
* Host envelope (RoleTurnRequest / HostContext), never this reader.
|
|
233
|
+
* Undefined when absent — callers must not invent "pi".
|
|
234
|
+
*/
|
|
235
|
+
export function readInvocationSelectedHost(runDirectory) {
|
|
236
|
+
const raw = readInvocationRecord(runDirectory);
|
|
237
|
+
if (raw === undefined)
|
|
238
|
+
return undefined;
|
|
239
|
+
return typeof raw.host === "string" && raw.host.trim() !== ""
|
|
240
|
+
? raw.host.trim()
|
|
241
|
+
: undefined;
|
|
242
|
+
}
|
|
243
|
+
/** Merge usage into decisiveFacts without touching role payloads. */
|
|
244
|
+
export function withEngineDetourToolUsageFact(outcome, usage) {
|
|
245
|
+
if (usage === undefined)
|
|
246
|
+
return outcome;
|
|
247
|
+
const prior = isRecord(outcome.decisiveFacts) ? outcome.decisiveFacts : {};
|
|
248
|
+
return {
|
|
249
|
+
...outcome,
|
|
250
|
+
decisiveFacts: {
|
|
251
|
+
...prior,
|
|
252
|
+
[ENGINE_DETOUR_TOOL_USAGE_FACT_KEY]: usage,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** runDirectory owning a session directory (.../runs/<id>@role/session). */
|
|
257
|
+
export function runDirectoryFromSessionDirectory(sessionDirectory) {
|
|
258
|
+
return dirname(sessionDirectory);
|
|
259
|
+
}
|
|
260
|
+
/** session.jsonl under a session directory. */
|
|
261
|
+
export function sessionFileFromSessionDirectory(sessionDirectory) {
|
|
262
|
+
return join(sessionDirectory, "session.jsonl");
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Mint one public-invocation scope id when an engine is mounted.
|
|
266
|
+
* Call once at the public-entry boundary — never inside the auto-resume loop.
|
|
267
|
+
* The id lives on RoleTurnRequest / HostContext only (no detour sidecar file).
|
|
268
|
+
*/
|
|
269
|
+
export function mintEngineDetourInvocationScope(input) {
|
|
270
|
+
const engine = input.effectiveEngine?.trim();
|
|
271
|
+
if (engine === undefined || engine.length === 0)
|
|
272
|
+
return undefined;
|
|
273
|
+
return randomUUID();
|
|
274
|
+
}
|
|
275
|
+
/** Attach a minted scope onto a turn request (shared Host envelope field). */
|
|
276
|
+
export function withEngineDetourInvocationScope(request, invocationScopeId) {
|
|
277
|
+
if (invocationScopeId === undefined || invocationScopeId.length === 0) {
|
|
278
|
+
return request;
|
|
279
|
+
}
|
|
280
|
+
if (typeof request.invocationScopeId === "string" &&
|
|
281
|
+
request.invocationScopeId.length > 0) {
|
|
282
|
+
return request;
|
|
283
|
+
}
|
|
284
|
+
return { ...request, invocationScopeId };
|
|
285
|
+
}
|