@akagilnc/pi-workflow-roles 0.1.3826 → 0.1.3873
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/acp-host/production-host.js +22757 -22676
- package/dist/canonical-skill-binding.js +0 -3
- package/dist/engine-detour-tool.js +2 -2
- package/dist/engine-detour.js +23 -7
- package/dist/headless-host/production-host.js +4658 -4593
- package/dist/host-descriptions.js +5 -2
- package/dist/pi/adapter.js +11 -0
- package/dist/pi/role-turn-host.js +44 -93
- package/dist/public-cli/invocation.js +4 -5
- package/dist/public-cli/load-production-acp-host.js +1 -1
- package/dist/public-cli/load-production-external-host.js +10 -0
- package/dist/public-cli/load-production-headless-host.js +1 -1
- package/dist/public-cli/main.js +145 -140
- package/dist/public-role-summons.js +7 -49
- package/dist/reviewer-agent.js +1 -0
- package/dist/reviewer-child-executor.js +3 -2
- package/dist/role-activation-flags.js +50 -0
- package/dist/role-runtime.js +12 -10
- package/dist/worker-role.js +14 -12
- package/package.json +1 -1
- package/scripts/build-package.mjs +2 -2
- package/src/acp-host/production-host.ts +31 -94
- package/src/acp-host/role-turn-host.ts +178 -263
- package/src/canonical-skill-binding.ts +4 -4
- package/src/diagnostic-tail.ts +18 -0
- package/src/engine-detour-tool.ts +5 -4
- package/src/engine-detour.ts +44 -9
- package/src/external-host-turn-loop.ts +172 -0
- package/src/headless-host/production-host.ts +10 -7
- package/src/headless-host/role-turn-host.ts +271 -263
- package/src/host-contracts.ts +6 -0
- package/src/host-descriptions.ts +5 -2
- package/src/host-session-record.ts +41 -0
- package/src/package-resources/method-skill-binding.ts +0 -3
- package/src/pi/adapter.ts +13 -0
- package/src/pi/role-turn-host.ts +46 -94
- package/src/prepared-role-turn.ts +68 -0
- package/src/public-cli/cli.ts +11 -20
- package/src/public-cli/invocation.ts +4 -5
- package/src/public-cli/load-production-acp-host.ts +2 -1
- package/src/public-cli/load-production-external-host.ts +17 -0
- package/src/public-cli/load-production-headless-host.ts +2 -1
- package/src/public-role-summons.ts +12 -69
- package/src/reviewer-agent.ts +12 -1
- package/src/reviewer-child-executor.ts +10 -2
- package/src/role-activation-flags.ts +53 -0
- package/src/{acp-host/role-envelope.ts → role-envelope.ts} +41 -98
- package/src/role-runtime-dependencies.ts +91 -0
- package/src/role-runtime.ts +21 -11
- package/src/{acp-host/session-identity.ts → session-identity.ts} +6 -6
- package/src/worker-role.ts +14 -13
- /package/src/{acp-host/mcp-relay.mjs → mcp-relay.mjs} +0 -0
|
@@ -2,7 +2,18 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
|
|
4
4
|
import type { RoleTurnHost, RoleTurnKnownFailure, RoleTurnRequest, RoleTurnResult } from "../host-contracts.ts";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createSerializedRoleTurnHost,
|
|
7
|
+
driveExternalRoleTurnRounds,
|
|
8
|
+
raceAgainstHostAbort,
|
|
9
|
+
} from "../external-host-turn-loop.ts";
|
|
10
|
+
import { retainDiagnosticTail } from "../diagnostic-tail.ts";
|
|
11
|
+
import { reportHostSessionEvent } from "../host-session-record.ts";
|
|
12
|
+
import {
|
|
13
|
+
renderSystemPromptOverride,
|
|
14
|
+
type PreparedRoleTurn,
|
|
15
|
+
type SessionIdentityAuthority,
|
|
16
|
+
} from "../prepared-role-turn.ts";
|
|
6
17
|
import { acpModelId, type AcpHostDescription } from "./description.ts";
|
|
7
18
|
|
|
8
19
|
/** ACP v1 surface used by the generic ACP adapter. Protocol details stay in this module. */
|
|
@@ -14,74 +25,10 @@ export interface AcpConnection {
|
|
|
14
25
|
close(): Promise<void>;
|
|
15
26
|
}
|
|
16
27
|
|
|
17
|
-
/** The shared envelope, prepared before session/new (systemPrompt delivery) and
|
|
18
|
-
* able to observe the host's real builtin tool surface once it arrives post-session. */
|
|
19
|
-
export type AcpPreparedTurn = Readonly<{
|
|
20
|
-
mcpServers: readonly Readonly<Record<string, unknown>>[];
|
|
21
|
-
/**
|
|
22
|
-
* Structured system-prompt authority. `body` is the provider-facing prompt
|
|
23
|
-
* bytes; `materials` are typed agent-start reading materials (e.g. Notary
|
|
24
|
-
* session bound) that the adapter folds into the override at the provider
|
|
25
|
-
* boundary. This structure is the authoritative production input of the
|
|
26
|
-
* send path — never a test-only parallel face.
|
|
27
|
-
*/
|
|
28
|
-
systemPrompt: { readonly body: string; readonly materials: readonly unknown[] };
|
|
29
|
-
/** Effective user prompt after host-side input transform (canonical Skill invocation). */
|
|
30
|
-
prompt: string;
|
|
31
|
-
/**
|
|
32
|
-
* Host abort signal armed only by typed infrastructure failure (envelope
|
|
33
|
-
* rememberInfrastructureFailure / non-correctable MCP catch). Lawful
|
|
34
|
-
* context.abort() (seal / non-sole) does not arm it. executeTurn races
|
|
35
|
-
* session/prompt against this so infra declarations terminate even when
|
|
36
|
-
* ACP never resolves (#593).
|
|
37
|
-
*/
|
|
38
|
-
abortSignal?: AbortSignal;
|
|
39
|
-
/** Shared ledger consumes the complete ACP round after session/prompt resolves. */
|
|
40
|
-
closeRound(): Promise<
|
|
41
|
-
| { readonly accepted: true }
|
|
42
|
-
| {
|
|
43
|
-
readonly accepted: false;
|
|
44
|
-
/** Shared envelope provides officer/correctable text; adapters only deliver it (#813). */
|
|
45
|
-
readonly retry: {
|
|
46
|
-
readonly code: string;
|
|
47
|
-
readonly toolCallIds: readonly string[];
|
|
48
|
-
readonly message: string;
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
| { readonly accepted: false; readonly failure: RoleTurnKnownFailure }
|
|
52
|
-
>;
|
|
53
|
-
dispose?(): Promise<void>;
|
|
54
|
-
/**
|
|
55
|
-
* Headless CLI family (#645): role terminating-tool schema for host-native
|
|
56
|
-
* `--json-schema`. Present for every prepared turn; ACP ignores it.
|
|
57
|
-
*/
|
|
58
|
-
jsonSchema: Readonly<Record<string, unknown>>;
|
|
59
|
-
/** Terminating tool name whose schema is `jsonSchema`. */
|
|
60
|
-
terminatingToolName: string;
|
|
61
|
-
/**
|
|
62
|
-
* Headless CLI family: feed host-native `structured_output` through the same
|
|
63
|
-
* terminating-tool path the MCP relay uses (ledger + gates). ACP ignores it.
|
|
64
|
-
*/
|
|
65
|
-
ingestStructuredOutput(params: unknown): Promise<void>;
|
|
66
|
-
}>;
|
|
67
|
-
|
|
68
|
-
/** Fold structured system-prompt authority into the provider-visible ACP override. */
|
|
69
|
-
export function renderAcpSystemPromptOverride(authority: {
|
|
70
|
-
readonly body: string;
|
|
71
|
-
readonly materials: readonly unknown[];
|
|
72
|
-
}): string {
|
|
73
|
-
return renderAgentStartMaterials(authority.body, authority.materials);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export type AcpSessionIdentityAuthority = Readonly<{
|
|
77
|
-
load(principal: RoleTurnRequest["principal"]): Promise<string | undefined>;
|
|
78
|
-
bind(principal: RoleTurnRequest["principal"], sessionId: string): Promise<void>;
|
|
79
|
-
/** Durable principal session path for layout ownership / isAvailable — not a rebuild source (#617 DK-4). */
|
|
80
|
-
resolveSessionFile(principal: RoleTurnRequest["principal"]): string;
|
|
81
|
-
}>;
|
|
82
|
-
|
|
83
28
|
export type AcpRoleTurnHostConfig = Readonly<{
|
|
84
|
-
sessionIdentity:
|
|
29
|
+
sessionIdentity: SessionIdentityAuthority;
|
|
30
|
+
/** Seat-table host key (e.g. grok-build) for sitian host field. */
|
|
31
|
+
hostName: string;
|
|
85
32
|
/** Whether a bound resume reuses the native session or mints a fresh one. */
|
|
86
33
|
boundResume: AcpHostDescription["boundResume"];
|
|
87
34
|
/**
|
|
@@ -91,7 +38,7 @@ export type AcpRoleTurnHostConfig = Readonly<{
|
|
|
91
38
|
*/
|
|
92
39
|
modelPassing: AcpHostDescription["modelPassing"];
|
|
93
40
|
connect(request: RoleTurnRequest): Promise<AcpConnection>;
|
|
94
|
-
prepare(request: RoleTurnRequest): Promise<
|
|
41
|
+
prepare(request: RoleTurnRequest): Promise<PreparedRoleTurn>;
|
|
95
42
|
}>;
|
|
96
43
|
|
|
97
44
|
function failure(cause: "activation" | "session" | "output", name: string, code: string, details?: Readonly<Record<string, unknown>>): RoleTurnResult {
|
|
@@ -109,10 +56,6 @@ function failure(cause: "activation" | "session" | "output", name: string, code:
|
|
|
109
56
|
|
|
110
57
|
type RpcReply = { readonly id?: unknown; readonly method?: unknown; readonly params?: unknown; readonly result?: unknown; readonly error?: unknown };
|
|
111
58
|
|
|
112
|
-
function hostAbortedError(): Error & { readonly code: "host-aborted" } {
|
|
113
|
-
return Object.assign(new Error("ACP host aborted"), { code: "host-aborted" as const });
|
|
114
|
-
}
|
|
115
|
-
|
|
116
59
|
function acpError(code: string, message: string, cause?: unknown): Error & { readonly code: string } {
|
|
117
60
|
return Object.assign(new Error(message, cause === undefined ? undefined : { cause }), { code });
|
|
118
61
|
}
|
|
@@ -132,6 +75,7 @@ export function connectAcpStdio(options: {
|
|
|
132
75
|
let nextId = 0;
|
|
133
76
|
let closed = false;
|
|
134
77
|
let terminalError: Error | undefined;
|
|
78
|
+
// Rolling diagnostic tail only — not an unbounded transcript face (same class as headless).
|
|
135
79
|
let stderr = "";
|
|
136
80
|
const settleClosed = (error: Error): void => {
|
|
137
81
|
if (closed) return;
|
|
@@ -148,7 +92,9 @@ export function connectAcpStdio(options: {
|
|
|
148
92
|
child.stdin.end();
|
|
149
93
|
child.kill("SIGTERM");
|
|
150
94
|
};
|
|
151
|
-
child.stderr.setEncoding("utf8").on("data", (chunk: string) => {
|
|
95
|
+
child.stderr.setEncoding("utf8").on("data", (chunk: string) => {
|
|
96
|
+
stderr = retainDiagnosticTail(stderr + chunk);
|
|
97
|
+
});
|
|
152
98
|
child.on("error", (error) => settleClosed(acpError("acp-process-error", `ACP process error: ${error.message}`, error)));
|
|
153
99
|
createInterface({ input: child.stdout }).on("line", (line) => {
|
|
154
100
|
let message: RpcReply;
|
|
@@ -223,210 +169,179 @@ export function connectAcpStdio(options: {
|
|
|
223
169
|
});
|
|
224
170
|
}
|
|
225
171
|
|
|
226
|
-
/**
|
|
227
|
-
* Main-session ACP adapter. The injected composition callbacks are the shared
|
|
228
|
-
* envelope boundary: this module owns ACP lifecycle, never role policy.
|
|
229
|
-
*/
|
|
172
|
+
/** ACP last hop (#820): session open/load/close, prompt, MCP mount, capability/model. */
|
|
230
173
|
export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHost {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
174
|
+
return createSerializedRoleTurnHost(async (request): Promise<RoleTurnResult> => {
|
|
175
|
+
const prepared = await config.prepare(request);
|
|
176
|
+
const systemPromptOverride = renderSystemPromptOverride(prepared.systemPrompt);
|
|
177
|
+
let connection: AcpConnection | undefined;
|
|
178
|
+
let sessionId: string | undefined;
|
|
179
|
+
let accepted = false;
|
|
180
|
+
try {
|
|
181
|
+
if (prepared.mcpServers.length === 0) {
|
|
182
|
+
return failure("activation", "UncontrolledAcpSession", "ak-config-missing");
|
|
183
|
+
}
|
|
184
|
+
connection = await config.connect(request);
|
|
185
|
+
// Live host-session records: ACP session/update → sitian sole entry (#811).
|
|
186
|
+
// One abort + one race helper + one outer projection — write failure ends
|
|
187
|
+
// any in-flight RPC as typed session infrastructure failure.
|
|
188
|
+
const sessionParent = config.sessionIdentity.resolveSessionFile(request.principal);
|
|
189
|
+
const recordAbort = new AbortController();
|
|
190
|
+
let hostSessionRecordFailure: RoleTurnKnownFailure | undefined;
|
|
191
|
+
const hostSessionRecordResult = (): RoleTurnResult => ({
|
|
192
|
+
code: null,
|
|
193
|
+
stderr: "",
|
|
194
|
+
timedOut: false,
|
|
195
|
+
knownFailure: hostSessionRecordFailure!,
|
|
196
|
+
});
|
|
197
|
+
const noteHostSessionRecordFailure = (error: unknown): void => {
|
|
198
|
+
if (hostSessionRecordFailure !== undefined) return;
|
|
199
|
+
hostSessionRecordFailure = {
|
|
200
|
+
cause: "session",
|
|
201
|
+
identity: { name: "HostSessionRecordFailure", code: "host-session-record-failed" },
|
|
202
|
+
diagnostic: error instanceof Error ? error.message : String(error),
|
|
203
|
+
};
|
|
204
|
+
try { recordAbort.abort(); } catch { /* already aborted */ }
|
|
205
|
+
};
|
|
206
|
+
const rpc = (
|
|
207
|
+
method: string,
|
|
208
|
+
params: Readonly<Record<string, unknown>>,
|
|
209
|
+
): Promise<Readonly<Record<string, unknown>>> =>
|
|
210
|
+
raceAgainstHostAbort(connection!.request(method, params), recordAbort.signal, "host-session-record-failed");
|
|
211
|
+
|
|
212
|
+
connection.onNotification?.((method, params) => {
|
|
213
|
+
if (method !== "session/update" || hostSessionRecordFailure !== undefined) return;
|
|
241
214
|
try {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
protocolVersion: 1,
|
|
249
|
-
clientCapabilities: {},
|
|
215
|
+
reportHostSessionEvent({
|
|
216
|
+
host: config.hostName,
|
|
217
|
+
cwd: request.cwd,
|
|
218
|
+
sessionParent,
|
|
219
|
+
source: "acp-host",
|
|
220
|
+
event: { method, params },
|
|
250
221
|
});
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const availableModels = Array.isArray(modelState?.availableModels) ? modelState.availableModels : undefined;
|
|
256
|
-
if (request.model !== undefined && availableModels !== undefined && !availableModels.some((entry) =>
|
|
257
|
-
typeof entry === "object" && entry !== null
|
|
258
|
-
&& (entry as { modelId?: unknown }).modelId === acpModelId(config.modelPassing, request.model))) {
|
|
259
|
-
return failure("activation", "AcpHostModelMismatch", "host-model-mismatch", {
|
|
260
|
-
provider: request.model.provider,
|
|
261
|
-
model: request.model.model,
|
|
262
|
-
});
|
|
263
|
-
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
noteHostSessionRecordFailure(error);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
264
226
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
? loaded.sessionId
|
|
285
|
-
: bindSessionId;
|
|
286
|
-
};
|
|
287
|
-
if (continuation.kind === "resume" && config.boundResume === "session/load") {
|
|
288
|
-
// Same-host resume reuses the native ACP session via session/load.
|
|
289
|
-
const boundSessionId = await config.sessionIdentity.load(request.principal);
|
|
290
|
-
if (boundSessionId !== undefined && boundSessionId !== "") {
|
|
291
|
-
sessionId = await loadSession(boundSessionId);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
if (sessionId === undefined) {
|
|
295
|
-
// Initial run, unbound resume (cross-host / lost binding), or a host
|
|
296
|
-
// whose bound resume is session/new: mint the session and bind it.
|
|
297
|
-
const session = await connection.request("session/new", sessionBindParams);
|
|
298
|
-
sessionId = typeof session.sessionId === "string" ? session.sessionId : undefined;
|
|
299
|
-
if (sessionId === undefined || sessionId === "") {
|
|
300
|
-
return failure("session", "AcpSessionFailure", "session-id-missing");
|
|
301
|
-
}
|
|
302
|
-
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
303
|
-
}
|
|
227
|
+
try {
|
|
228
|
+
const initialized = await rpc("initialize", {
|
|
229
|
+
protocolVersion: 1,
|
|
230
|
+
clientCapabilities: {},
|
|
231
|
+
});
|
|
232
|
+
const initializeMeta = initialized._meta as {
|
|
233
|
+
modelState?: { availableModels?: unknown };
|
|
234
|
+
} | undefined;
|
|
235
|
+
const availableModels = Array.isArray(initializeMeta?.modelState?.availableModels)
|
|
236
|
+
? initializeMeta.modelState.availableModels
|
|
237
|
+
: undefined;
|
|
238
|
+
if (request.model !== undefined && availableModels !== undefined && !availableModels.some((entry) =>
|
|
239
|
+
typeof entry === "object" && entry !== null
|
|
240
|
+
&& (entry as { modelId?: unknown }).modelId === acpModelId(config.modelPassing, request.model))) {
|
|
241
|
+
return failure("activation", "AcpHostModelMismatch", "host-model-mismatch", {
|
|
242
|
+
provider: request.model.provider,
|
|
243
|
+
model: request.model.model,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
304
246
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
247
|
+
const sessionBindParams = {
|
|
248
|
+
cwd: request.cwd,
|
|
249
|
+
mcpServers: prepared.mcpServers,
|
|
250
|
+
_meta: { systemPromptOverride, yoloMode: false },
|
|
251
|
+
};
|
|
252
|
+
const loadSession = async (bindSessionId: string): Promise<string> => {
|
|
253
|
+
const loaded = await rpc("session/load", {
|
|
254
|
+
sessionId: bindSessionId,
|
|
255
|
+
...sessionBindParams,
|
|
256
|
+
});
|
|
257
|
+
return typeof loaded.sessionId === "string" && loaded.sessionId !== ""
|
|
258
|
+
? loaded.sessionId
|
|
259
|
+
: bindSessionId;
|
|
260
|
+
};
|
|
261
|
+
if (request.continuation.kind === "resume" && config.boundResume === "session/load") {
|
|
262
|
+
const boundSessionId = await config.sessionIdentity.load(request.principal);
|
|
263
|
+
if (boundSessionId !== undefined && boundSessionId !== "") {
|
|
264
|
+
sessionId = await loadSession(boundSessionId);
|
|
321
265
|
}
|
|
266
|
+
}
|
|
267
|
+
if (sessionId === undefined) {
|
|
268
|
+
const session = await rpc("session/new", sessionBindParams);
|
|
269
|
+
sessionId = typeof session.sessionId === "string" ? session.sessionId : undefined;
|
|
270
|
+
if (sessionId === undefined || sessionId === "") {
|
|
271
|
+
return failure("session", "AcpSessionFailure", "session-id-missing");
|
|
272
|
+
}
|
|
273
|
+
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
274
|
+
}
|
|
322
275
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
request.signal === undefined
|
|
332
|
-
? prepared.abortSignal
|
|
333
|
-
: prepared.abortSignal === undefined
|
|
334
|
-
? request.signal
|
|
335
|
-
: AbortSignal.any([prepared.abortSignal, request.signal]);
|
|
336
|
-
const activeConnection = connection;
|
|
276
|
+
// set_model seat provider:model (#778); may rebuild agent — re-bind via loadSession.
|
|
277
|
+
if (config.modelPassing === "set_model" && request.model !== undefined && sessionId !== undefined) {
|
|
278
|
+
await rpc("session/set_model", {
|
|
279
|
+
sessionId,
|
|
280
|
+
modelId: acpModelId(config.modelPassing, request.model),
|
|
281
|
+
});
|
|
282
|
+
sessionId = await loadSession(sessionId);
|
|
283
|
+
}
|
|
337
284
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const onAbort = (): void => {
|
|
351
|
-
if (settled) return;
|
|
352
|
-
settled = true;
|
|
353
|
-
promptRequest.catch(() => {});
|
|
354
|
-
reject(hostAbortedError());
|
|
355
|
-
};
|
|
356
|
-
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
357
|
-
promptRequest.then(
|
|
358
|
-
(value) => {
|
|
359
|
-
if (settled) return;
|
|
360
|
-
settled = true;
|
|
361
|
-
abortSignal.removeEventListener("abort", onAbort);
|
|
362
|
-
resolve(value);
|
|
363
|
-
},
|
|
364
|
-
(error) => {
|
|
365
|
-
if (settled) return;
|
|
366
|
-
settled = true;
|
|
367
|
-
abortSignal.removeEventListener("abort", onAbort);
|
|
368
|
-
reject(error);
|
|
369
|
-
},
|
|
370
|
-
);
|
|
371
|
-
});
|
|
372
|
-
};
|
|
373
|
-
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
285
|
+
const activeConnection = connection;
|
|
286
|
+
const activeSessionId = sessionId;
|
|
287
|
+
const turnResult = await driveExternalRoleTurnRounds(prepared, request, {
|
|
288
|
+
roundLimitName: "AcpRoundLimit",
|
|
289
|
+
currentSessionId: () => sessionId,
|
|
290
|
+
async runRound({ prompt, abortSignal }) {
|
|
291
|
+
if (hostSessionRecordFailure !== undefined) return { status: "terminal", result: hostSessionRecordResult() };
|
|
292
|
+
// Envelope infra abort, parent cancellation (#675), and host-session
|
|
293
|
+
// record abort (#811) share one race face for in-flight prompt.
|
|
294
|
+
const abortParts: AbortSignal[] = [recordAbort.signal];
|
|
295
|
+
if (abortSignal !== undefined) abortParts.push(abortSignal);
|
|
296
|
+
const combinedAbort = AbortSignal.any(abortParts);
|
|
374
297
|
let result: Readonly<Record<string, unknown>>;
|
|
375
298
|
try {
|
|
376
|
-
result = await
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
299
|
+
result = await raceAgainstHostAbort(
|
|
300
|
+
activeConnection.request("session/prompt", {
|
|
301
|
+
sessionId: activeSessionId,
|
|
302
|
+
prompt: [{ type: "text", text: prompt }],
|
|
303
|
+
}),
|
|
304
|
+
combinedAbort,
|
|
305
|
+
"ACP host aborted",
|
|
306
|
+
);
|
|
380
307
|
} catch (error) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
typeof error === "object"
|
|
384
|
-
&& error !== null
|
|
385
|
-
&& (error as { code?: unknown }).code === "host-aborted"
|
|
386
|
-
) {
|
|
387
|
-
const closure = await prepared.closeRound();
|
|
388
|
-
if ("failure" in closure) {
|
|
389
|
-
return { code: null, stderr: "", timedOut: false, knownFailure: closure.failure };
|
|
390
|
-
}
|
|
391
|
-
return failure("session", "HostAborted", "host-aborted", { sessionId });
|
|
308
|
+
if (hostSessionRecordFailure !== undefined) {
|
|
309
|
+
return { status: "terminal", result: hostSessionRecordResult() };
|
|
392
310
|
}
|
|
393
311
|
throw error;
|
|
394
312
|
}
|
|
395
313
|
if (result.stopReason === "refusal") {
|
|
396
|
-
return
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const closure = await prepared.closeRound();
|
|
401
|
-
if (closure.accepted) {
|
|
402
|
-
// Wait for ACP's typed close acknowledgement before tearing down the
|
|
403
|
-
// process; Stop hooks and fire-and-forget cancellation are not closure.
|
|
404
|
-
await connection.request("session/close", { sessionId });
|
|
405
|
-
accepted = true;
|
|
406
|
-
return { code: 0, stderr: "", timedOut: false };
|
|
407
|
-
}
|
|
408
|
-
if ("failure" in closure) {
|
|
409
|
-
return { code: null, stderr: "", timedOut: false, knownFailure: closure.failure };
|
|
410
|
-
}
|
|
411
|
-
// Shared envelope already owns the officer/correctable text (#813).
|
|
412
|
-
prompt = closure.retry.message;
|
|
413
|
-
}
|
|
414
|
-
return failure("output", "AcpRoundLimit", "round-retry-limit", { sessionId });
|
|
415
|
-
} finally {
|
|
416
|
-
if (connection !== undefined) {
|
|
417
|
-
if (sessionId !== undefined && !accepted) {
|
|
418
|
-
try { connection.notify("session/cancel", { sessionId }); }
|
|
419
|
-
catch { /* Preserve the original turn result or failure. */ }
|
|
314
|
+
return {
|
|
315
|
+
status: "terminal",
|
|
316
|
+
result: failure("output", "AcpRefusal", "refusal", { sessionId }),
|
|
317
|
+
};
|
|
420
318
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
319
|
+
return { status: "delivered" };
|
|
320
|
+
},
|
|
321
|
+
async afterAccepted() {
|
|
322
|
+
await rpc("session/close", { sessionId: activeSessionId });
|
|
323
|
+
accepted = true;
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
if (hostSessionRecordFailure !== undefined) return hostSessionRecordResult();
|
|
327
|
+
return turnResult;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
// recordAbort races setup RPCs via raceAgainstHostAbort (host-aborted code);
|
|
330
|
+
// noteHostSessionRecordFailure always sets the typed failure before aborting.
|
|
331
|
+
if (hostSessionRecordFailure !== undefined) return hostSessionRecordResult();
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
} finally {
|
|
335
|
+
if (connection !== undefined) {
|
|
336
|
+
if (sessionId !== undefined && !accepted) {
|
|
337
|
+
try { connection.notify("session/cancel", { sessionId }); }
|
|
338
|
+
catch { /* keep turn result */ }
|
|
426
339
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
340
|
+
try { await connection.close(); }
|
|
341
|
+
catch { /* keep turn result */ }
|
|
342
|
+
}
|
|
343
|
+
try { await prepared.dispose?.(); }
|
|
344
|
+
catch { /* keep turn result */ }
|
|
345
|
+
}
|
|
346
|
+
});
|
|
432
347
|
}
|
|
@@ -25,7 +25,10 @@ export type CanonicalSkillEvidence<Name extends CanonicalSkillName = CanonicalSk
|
|
|
25
25
|
export type CanonicalSkillBinding<Name extends CanonicalSkillName = CanonicalSkillName> = Readonly<{
|
|
26
26
|
name: Name;
|
|
27
27
|
snapshot: CanonicalSkillSnapshot;
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Accept host expansion evidence when name/location/content match the binding
|
|
30
|
+
* and userMessage equals the captured original request (ADR 0032).
|
|
31
|
+
*/
|
|
29
32
|
captureExpansion(
|
|
30
33
|
evidence: HostSkillExpansionEvidence | undefined,
|
|
31
34
|
originalRequest: string,
|
|
@@ -100,9 +103,6 @@ export async function loadCanonicalSkillBinding(
|
|
|
100
103
|
const binding: CanonicalSkillBinding<typeof name> = {
|
|
101
104
|
name,
|
|
102
105
|
snapshot,
|
|
103
|
-
invocation(originalRequest) {
|
|
104
|
-
return `/skill:${name} ${originalRequest}`;
|
|
105
|
-
},
|
|
106
106
|
captureExpansion(evidence, originalRequest) {
|
|
107
107
|
return captureCanonicalSkillExpansion(
|
|
108
108
|
name,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bound diagnostic text: rolling tail only, not a dossier/transcript face.
|
|
3
|
+
* One authority for host stderr retention (headless + ACP).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Fixed cap for retained diagnostic tails. */
|
|
7
|
+
export const DIAGNOSTIC_TAIL_CAP = 16 * 1024;
|
|
8
|
+
|
|
9
|
+
const CLIP_MARK = "…[stderr clipped]\n";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Retain at most DIAGNOSTIC_TAIL_CAP characters, keeping the newest bytes
|
|
13
|
+
* when over budget. Clip mark is included inside the fixed cap.
|
|
14
|
+
*/
|
|
15
|
+
export function retainDiagnosticTail(text: string): string {
|
|
16
|
+
if (text.length <= DIAGNOSTIC_TAIL_CAP) return text;
|
|
17
|
+
return CLIP_MARK + text.slice(text.length - (DIAGNOSTIC_TAIL_CAP - CLIP_MARK.length));
|
|
18
|
+
}
|
|
@@ -11,8 +11,8 @@ import type { HostContext, HostToolDefinition, HostToolResult, RoleHost } from "
|
|
|
11
11
|
import {
|
|
12
12
|
ENGINE_DETOUR_TOOL_NAME,
|
|
13
13
|
engineDetourFailureDiagnostic,
|
|
14
|
-
engineNameFromEnv,
|
|
15
14
|
isEngineDetourFailure,
|
|
15
|
+
resolveEngineName,
|
|
16
16
|
runEngineDetourOnce,
|
|
17
17
|
} from "./engine-detour.ts";
|
|
18
18
|
|
|
@@ -125,14 +125,15 @@ export function createEngineDetourToolDefinition(input: {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
|
-
* Register the engine-generic detour tool once
|
|
129
|
-
*
|
|
128
|
+
* Register the engine-generic detour tool once when any role has an engine
|
|
129
|
+
* activation signal. Signal is request-scoped via RoleHost flag, with pi
|
|
130
|
+
* child-process env as fallback (resolveEngineName). Returns whether registration occurred.
|
|
130
131
|
*/
|
|
131
132
|
export function registerEngineDetourTool(
|
|
132
133
|
roleHost: RoleHost,
|
|
133
134
|
hostActions: EngineDetourHostActions,
|
|
134
135
|
): boolean {
|
|
135
|
-
const engineName =
|
|
136
|
+
const engineName = resolveEngineName((name) => roleHost.getFlag(name));
|
|
136
137
|
if (engineName === undefined) {
|
|
137
138
|
return false;
|
|
138
139
|
}
|
package/src/engine-detour.ts
CHANGED
|
@@ -16,24 +16,42 @@ export const ENGINE_DETOUR_TOOL_NAME = "ak_engine_detour" as const;
|
|
|
16
16
|
/** Env presence/name signal injected by public role runs (registration gate only). */
|
|
17
17
|
export const AK_ROLE_ENGINE_ENV = "AK_ROLE_ENGINE" as const;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Request-scoped engine flag on RoleHost (#818 P1).
|
|
21
|
+
* Envelope projects RoleTurnRequest.engine here so concurrent in-process hosts
|
|
22
|
+
* do not share process.env. Empty string = explicitly no engine (blocks ambient).
|
|
23
|
+
* Pi never sets this flag — resolve falls through to child-process env.
|
|
24
|
+
*/
|
|
25
|
+
export const ENGINE_FLAG_NAME = "ak-engine" as const;
|
|
26
|
+
|
|
19
27
|
/**
|
|
20
28
|
* Argv placeholder replaced by a seam-owned temp prompt file path when
|
|
21
29
|
* `stagedPrompt` is set. Exactly one argv entry must equal this token.
|
|
22
30
|
*/
|
|
23
31
|
export const ENGINE_DETOUR_STAGED_PROMPT_TOKEN = "<<ak-engine-staged-prompt>>" as const;
|
|
24
32
|
|
|
33
|
+
/** Non-empty trimmed engine name, else undefined. */
|
|
34
|
+
export function normalizeEngineName(engine: string | undefined): string | undefined {
|
|
35
|
+
if (engine === undefined) return undefined;
|
|
36
|
+
const trimmed = engine.trim();
|
|
37
|
+
return trimmed === "" ? undefined : trimmed;
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
/**
|
|
26
|
-
* Sole AK_ROLE_ENGINE write seam
|
|
27
|
-
*
|
|
41
|
+
* Sole AK_ROLE_ENGINE write seam (#391 E2 / #818).
|
|
42
|
+
* Child-process env only (pi adapter). Delete ambient first. Child-env objects
|
|
43
|
+
* keep an own-key undefined mask so a later process.env re-merge cannot revive
|
|
44
|
+
* ambient; process.env itself only deletes (Node stringifies undefined assignments).
|
|
28
45
|
*/
|
|
29
46
|
export function applyEngineChildEnv(
|
|
30
47
|
childEnv: NodeJS.ProcessEnv,
|
|
31
48
|
engine?: string,
|
|
32
49
|
): void {
|
|
33
50
|
delete childEnv[AK_ROLE_ENGINE_ENV];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
const normalized = normalizeEngineName(engine);
|
|
52
|
+
if (normalized !== undefined) {
|
|
53
|
+
childEnv[AK_ROLE_ENGINE_ENV] = normalized;
|
|
54
|
+
} else if (childEnv !== process.env) {
|
|
37
55
|
childEnv[AK_ROLE_ENGINE_ENV] = undefined;
|
|
38
56
|
}
|
|
39
57
|
}
|
|
@@ -239,8 +257,25 @@ export function engineDetourFailureDiagnostic(result: {
|
|
|
239
257
|
|
|
240
258
|
/** Non-empty trimmed engine name from process.env, else undefined. */
|
|
241
259
|
export function engineNameFromEnv(): string | undefined {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
260
|
+
return normalizeEngineName(
|
|
261
|
+
typeof process.env[AK_ROLE_ENGINE_ENV] === "string"
|
|
262
|
+
? process.env[AK_ROLE_ENGINE_ENV]
|
|
263
|
+
: undefined,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* One activation-signal resolver (#818 P1 / ADR 0069 one gate).
|
|
269
|
+
* Request-scoped RoleHost flag wins when present (envelope always projects it,
|
|
270
|
+
* including "" for no-engine so ambient env cannot arm). Flag absent → pi
|
|
271
|
+
* child-process env via engineNameFromEnv.
|
|
272
|
+
*/
|
|
273
|
+
export function resolveEngineName(
|
|
274
|
+
getFlag?: (name: string) => boolean | string | undefined,
|
|
275
|
+
): string | undefined {
|
|
276
|
+
if (getFlag !== undefined) {
|
|
277
|
+
const flag = getFlag(ENGINE_FLAG_NAME);
|
|
278
|
+
if (typeof flag === "string") return normalizeEngineName(flag);
|
|
279
|
+
}
|
|
280
|
+
return engineNameFromEnv();
|
|
246
281
|
}
|