@akagilnc/pi-workflow-roles 0.1.4586 → 0.1.4620
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/acp-host/production-host.js +951 -497
- package/dist/archivist-record-topology.js +27 -35
- package/dist/headless-host/description.js +7 -4
- package/dist/headless-host/production-host.js +783 -393
- package/dist/migrate-book-topology.js +3 -6
- package/dist/navigator-attendance.js +146 -241
- package/dist/navigator-public-session.js +81 -15
- package/dist/package-contracts/navigator-output.js +30 -12
- package/dist/public-cli/judge-run.js +2 -2
- package/dist/public-cli/main.js +69 -94
- package/dist/public-cli/option-definitions.js +1 -1
- package/dist/public-cli/post-admission.js +2 -2
- package/dist/public-cli/run-lifecycle.js +29 -7
- package/dist/public-cli/settlement.js +46 -32
- package/dist/public-cli/terminal.js +8 -27
- package/dist/public-role-summons.js +22 -4
- package/dist/submission-ledger.js +56 -23
- package/package.json +1 -1
- package/resources/836-deleted-machine-instruction-inventory.md +2 -0
- package/resources/navigator-route-playbook.md +9 -0
- package/scripts/build-package.mjs +6 -1
- package/src/acp-host/role-turn-host.ts +129 -13
- package/src/headless-host/description.ts +17 -7
- package/src/headless-host/role-turn-host.ts +42 -11
- package/src/navigator-attendance.ts +290 -425
- package/src/navigator-public-session.ts +126 -25
- package/src/navigator-role.ts +2 -2
- package/src/package-contracts/navigator-output.ts +41 -25
- package/src/public-cli/coder-run.ts +2 -2
- package/src/public-cli/fixer-run.ts +2 -2
- package/src/public-cli/judge-run.ts +2 -2
- package/src/public-cli/merger-run.ts +2 -2
- package/src/public-cli/option-definitions.ts +1 -1
- package/src/public-cli/post-admission.ts +2 -1
- package/src/public-cli/run-lifecycle.ts +37 -7
- package/src/public-cli/settlement.ts +42 -33
- package/src/public-cli/terminal.ts +12 -37
- package/src/public-role-summons.ts +34 -5
- package/src/role-envelope.ts +89 -4
- package/src/role-runtime.ts +74 -14
- package/src/submission-ledger.ts +63 -21
- package/dist/public-cli/command-renderer.js +0 -5
- package/dist/public-command-renderer.js +0 -29
- package/src/public-cli/command-renderer.ts +0 -9
- package/src/public-command-renderer.ts +0 -55
|
@@ -157,7 +157,7 @@ async function closedLedgerOutcome(admitted, role, scope) {
|
|
|
157
157
|
function coordinatesFromAdmitted(authority, admitted) {
|
|
158
158
|
return authority.decode(admitted.principal);
|
|
159
159
|
}
|
|
160
|
-
import { exitCodeForTerminalOutcome, formatTerminalResult, isLawfulTypedTerminalOutcome,
|
|
160
|
+
import { exitCodeForTerminalOutcome, formatTerminalResult, isLawfulTypedTerminalOutcome, adviceNavigatorFact, } from "./terminal.js";
|
|
161
161
|
export { exitCodeForTerminalOutcome, formatTerminalResult, isLawfulTypedTerminalOutcome, };
|
|
162
162
|
/**
|
|
163
163
|
* Host stderr as recorded — full bytes, no flood filter, no char clip (#836).
|
|
@@ -457,7 +457,13 @@ export function explicitInternalKnownFailureClassificationInput(failure) {
|
|
|
457
457
|
...(failure.details === undefined ? {} : { knownDetails: failure.details }),
|
|
458
458
|
};
|
|
459
459
|
}
|
|
460
|
-
/**
|
|
460
|
+
/**
|
|
461
|
+
* Post-role Navigator delivery grace (Issue #11 / #101 / #106 / #159).
|
|
462
|
+
* After the parent finishes: wait at most this long for navigator output, then
|
|
463
|
+
* stop waiting. Navigator must already be running from parent start (prepare
|
|
464
|
+
* host round in parallel); this window is only the tail after parent end — not
|
|
465
|
+
* the budget to start a cold full host turn (owner 2026-09-17 #959).
|
|
466
|
+
*/
|
|
461
467
|
export const NAVIGATOR_POST_ROLE_GRACE_MS = 10_000;
|
|
462
468
|
function isMissingPathError(error) {
|
|
463
469
|
return (error instanceof Error &&
|
|
@@ -1334,11 +1340,6 @@ export async function appendRunAttemptHistory(source, outcome) {
|
|
|
1334
1340
|
}
|
|
1335
1341
|
catch { }
|
|
1336
1342
|
}
|
|
1337
|
-
function navigatorPhaseValue(value) {
|
|
1338
|
-
if (value === "plan" || value === "apply")
|
|
1339
|
-
return value;
|
|
1340
|
-
return null;
|
|
1341
|
-
}
|
|
1342
1343
|
/**
|
|
1343
1344
|
* Minimal attendance provenance against the bound marker (ADR 0043).
|
|
1344
1345
|
* Keep only invocationId + post-terminal ordering. Runtime-self-produced
|
|
@@ -1357,35 +1358,48 @@ function parseNavigatorAttendanceDetails(details) {
|
|
|
1357
1358
|
const advisoryDiagnostic = typeof details.routePlaybookReadFailure === "string"
|
|
1358
1359
|
? { advisoryDiagnostic: details.routePlaybookReadFailure }
|
|
1359
1360
|
: {};
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1361
|
+
// #959: advice prose is presented as-is. Legacy "recommendation" with next/reason/
|
|
1362
|
+
// command is projected into prose so historical sessions still render — never wash
|
|
1363
|
+
// a real recommendation into no-advice when any advice body is recoverable.
|
|
1364
|
+
if (disposition === "advice" || disposition === "recommendation") {
|
|
1365
|
+
let prose;
|
|
1366
|
+
if (typeof details.prose === "string" && details.prose.trim() !== "") {
|
|
1367
|
+
prose = details.prose;
|
|
1368
|
+
}
|
|
1369
|
+
else {
|
|
1370
|
+
const next = isRecord(details.next) && typeof details.next.role === "string"
|
|
1371
|
+
? details.next.role
|
|
1372
|
+
: undefined;
|
|
1373
|
+
const reason = typeof details.reason === "string" && details.reason.trim() !== ""
|
|
1374
|
+
? details.reason
|
|
1375
|
+
: undefined;
|
|
1376
|
+
const command = typeof details.command === "string" && details.command.trim() !== ""
|
|
1377
|
+
? details.command
|
|
1378
|
+
: undefined;
|
|
1379
|
+
if (reason !== undefined && next !== undefined) {
|
|
1380
|
+
prose = `${reason}(下一步:${next})`;
|
|
1381
|
+
}
|
|
1382
|
+
else if (reason !== undefined) {
|
|
1383
|
+
prose = reason;
|
|
1384
|
+
}
|
|
1385
|
+
else if (next !== undefined) {
|
|
1386
|
+
// Historical recommendation with only typed next — still real advice.
|
|
1387
|
+
prose = `下一步:${next}`;
|
|
1388
|
+
}
|
|
1389
|
+
else if (command !== undefined) {
|
|
1390
|
+
prose = command;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (prose === undefined || prose.trim() === "") {
|
|
1394
|
+
// Attended but empty body is affirmative no-advice, not unavailable (#959).
|
|
1363
1395
|
return {
|
|
1364
|
-
disposition: "
|
|
1365
|
-
|
|
1366
|
-
reason: "navigator recommendation missing typed next role",
|
|
1396
|
+
disposition: "no-advice",
|
|
1397
|
+
...advisoryDiagnostic,
|
|
1367
1398
|
};
|
|
1368
1399
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
? details.route
|
|
1372
|
-
.filter(isRecord)
|
|
1373
|
-
.map((target) => ({
|
|
1374
|
-
role: String(target.role),
|
|
1375
|
-
phase: navigatorPhaseValue(target.phase),
|
|
1376
|
-
}))
|
|
1377
|
-
: undefined;
|
|
1378
|
-
return recommendationNavigatorFact({
|
|
1400
|
+
return adviceNavigatorFact({
|
|
1401
|
+
prose,
|
|
1379
1402
|
...advisoryDiagnostic,
|
|
1380
|
-
next: {
|
|
1381
|
-
role: next.role,
|
|
1382
|
-
phase: navigatorPhaseValue(next.phase),
|
|
1383
|
-
},
|
|
1384
|
-
reason,
|
|
1385
|
-
...(route === undefined ? {} : { route }),
|
|
1386
|
-
...(typeof details.command === "string"
|
|
1387
|
-
? { modelCommand: details.command }
|
|
1388
|
-
: {}),
|
|
1389
1403
|
});
|
|
1390
1404
|
}
|
|
1391
1405
|
if (disposition === "unavailable") {
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Terminal result: typed semantic regions for one admitted Role run (ADR 0052 / #106).
|
|
3
|
-
* Presentation may rearrange labels/order; only regions and typed facts are stable.
|
|
4
|
-
*
|
|
5
|
-
* Free-text cells are JSON-string encoded so legitimate newlines/tabs cannot forge
|
|
6
|
-
* extra rows or shift column boundaries (note / fix.summary / decision question / reason).
|
|
7
|
-
*/
|
|
8
|
-
import { isPublicCallableRole, renderPublicAkRoleCommand, } from "./command-renderer.js";
|
|
9
1
|
/** Encode one free-text Terminal cell. JSON string form cannot embed raw tab/newline. */
|
|
10
2
|
export function encodeTerminalField(value) {
|
|
11
3
|
return JSON.stringify(value);
|
|
@@ -34,21 +26,13 @@ export function roleResultPayloads(outcome) {
|
|
|
34
26
|
return [];
|
|
35
27
|
}
|
|
36
28
|
/**
|
|
37
|
-
* Build
|
|
38
|
-
*
|
|
29
|
+
* Build an advice navigator fact (#959). Prose is presented as submitted;
|
|
30
|
+
* code does not parse route/next or judge usability.
|
|
39
31
|
*/
|
|
40
|
-
export function
|
|
41
|
-
const command = typeof input.modelCommand === "string" && input.modelCommand.trim() !== ""
|
|
42
|
-
? input.modelCommand
|
|
43
|
-
: isPublicCallableRole(input.next.role)
|
|
44
|
-
? renderPublicAkRoleCommand(input.next)
|
|
45
|
-
: undefined;
|
|
32
|
+
export function adviceNavigatorFact(input) {
|
|
46
33
|
return {
|
|
47
|
-
disposition: "
|
|
48
|
-
|
|
49
|
-
reason: input.reason,
|
|
50
|
-
...(command === undefined ? {} : { command }),
|
|
51
|
-
...(input.route === undefined ? {} : { route: input.route }),
|
|
34
|
+
disposition: "advice",
|
|
35
|
+
prose: input.prose,
|
|
52
36
|
...(input.advisoryDiagnostic === undefined ? {} : { advisoryDiagnostic: input.advisoryDiagnostic }),
|
|
53
37
|
};
|
|
54
38
|
}
|
|
@@ -82,12 +66,9 @@ export function formatTerminalResult(result) {
|
|
|
82
66
|
if (result.navigator.advisoryDiagnostic !== undefined) {
|
|
83
67
|
lines.push(`navigator-advisory\t${encodeTerminalField(result.navigator.advisoryDiagnostic)}`);
|
|
84
68
|
}
|
|
85
|
-
if (result.navigator.disposition === "
|
|
86
|
-
|
|
87
|
-
lines.push(`
|
|
88
|
-
if (result.navigator.command !== undefined) {
|
|
89
|
-
lines.push(`command\t${encodeTerminalField(result.navigator.command)}`);
|
|
90
|
-
}
|
|
69
|
+
if (result.navigator.disposition === "advice") {
|
|
70
|
+
// #959: present navigator prose as-is — no next/reason/command parsing.
|
|
71
|
+
lines.push(`prose\t${encodeTerminalField(result.navigator.prose)}`);
|
|
91
72
|
}
|
|
92
73
|
else if (result.navigator.disposition === "unavailable") {
|
|
93
74
|
lines.push(`unavailable\t${result.navigator.source}\t${encodeTerminalField(result.navigator.reason)}`);
|
|
@@ -269,14 +269,32 @@ async function summonPublicRole(options) {
|
|
|
269
269
|
case "navigator":
|
|
270
270
|
case "gatekeeper": {
|
|
271
271
|
const seat2 = options.role;
|
|
272
|
-
const [{ runPublicInstructionSeat }, invocation] = await Promise.all([
|
|
272
|
+
const [{ runPublicInstructionSeat, runPublicInstructionSeatResume }, invocation] = await Promise.all([
|
|
273
273
|
import("./public-cli/instruction-seat-run.js"),
|
|
274
274
|
import("./public-cli/invocation.js")
|
|
275
275
|
]);
|
|
276
276
|
const parse = seat2 === "auditor" ? invocation.parseAuditorArgv : seat2 === "navigator" ? invocation.parseNavigatorArgv : invocation.parseGatekeeperArgv;
|
|
277
|
-
const
|
|
278
|
-
if (
|
|
279
|
-
|
|
277
|
+
const resumeRunId = typeof options.resumeRunId === "string" && options.resumeRunId.trim() !== "" ? options.resumeRunId.trim() : void 0;
|
|
278
|
+
if (resumeRunId !== void 0) {
|
|
279
|
+
const instruction = options.argv[0] ?? "";
|
|
280
|
+
const stepped = await runPrepared(parse, (env) => runPublicInstructionSeatResume(
|
|
281
|
+
{
|
|
282
|
+
runId: resumeRunId,
|
|
283
|
+
summons: {
|
|
284
|
+
instruction,
|
|
285
|
+
instructionEmpty: instruction.trim() === ""
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
env,
|
|
289
|
+
io
|
|
290
|
+
));
|
|
291
|
+
if ("fail" in stepped) return stepped.fail;
|
|
292
|
+
result = stepped.ok;
|
|
293
|
+
} else {
|
|
294
|
+
const stepped = await runPrepared(parse, (env, once) => runPublicInstructionSeat(options.argv, env, io, seat2, once));
|
|
295
|
+
if ("fail" in stepped) return stepped.fail;
|
|
296
|
+
result = stepped.ok;
|
|
297
|
+
}
|
|
280
298
|
break;
|
|
281
299
|
}
|
|
282
300
|
case "judge": {
|
|
@@ -338,6 +338,59 @@ async function restoreState(cwd, runId, scope) {
|
|
|
338
338
|
}, 0),
|
|
339
339
|
};
|
|
340
340
|
}
|
|
341
|
+
/** Sole HostContext-derived session parent for ledger restore/append (never process.env). */
|
|
342
|
+
function sessionParentFromHostContext(context) {
|
|
343
|
+
const runDirectory = runDirectoryFromHostContext(context);
|
|
344
|
+
if (runDirectory !== undefined)
|
|
345
|
+
return join(runDirectory, "session", "session.jsonl");
|
|
346
|
+
const sessionFile = context.sessionManager.getSessionFile?.();
|
|
347
|
+
if (typeof sessionFile === "string" && sessionFile.length > 0)
|
|
348
|
+
return sessionFile;
|
|
349
|
+
const sessionDir = context.sessionManager.getSessionDir?.();
|
|
350
|
+
if (typeof sessionDir === "string" && sessionDir.length > 0) {
|
|
351
|
+
return join(sessionDir, "session.jsonl");
|
|
352
|
+
}
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
function homeFromHostContext(context, home) {
|
|
356
|
+
if (home !== undefined)
|
|
357
|
+
return home;
|
|
358
|
+
const sessionParent = sessionParentFromHostContext(context);
|
|
359
|
+
return sessionParent !== undefined ? tryHomeFromAkRolesPath(sessionParent) : undefined;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* #959: seal one accepted submission without a model tool call.
|
|
363
|
+
* Same ledger row shape and priorEventId chain as the terminating-tool wrap.
|
|
364
|
+
* Used when a prose-exit seat (navigator) harvests the final assistant text.
|
|
365
|
+
*/
|
|
366
|
+
export async function sealAcceptedSubmission(options) {
|
|
367
|
+
const runId = runIdentity(options.context);
|
|
368
|
+
const attemptId = attemptIdentity(options.context, runId);
|
|
369
|
+
const sessionParent = sessionParentFromHostContext(options.context);
|
|
370
|
+
const home = homeFromHostContext(options.context, options.home);
|
|
371
|
+
const state = await restoreState(options.context.cwd, runId, {
|
|
372
|
+
...(home === undefined ? {} : { home }),
|
|
373
|
+
...(sessionParent === undefined ? {} : { sessionParent }),
|
|
374
|
+
});
|
|
375
|
+
const pointer = sitianReport({
|
|
376
|
+
level: "event",
|
|
377
|
+
kind: "sealed",
|
|
378
|
+
subject: { runId, attemptId },
|
|
379
|
+
...(state.prior === undefined ? {} : { priorEventId: state.prior.identity }),
|
|
380
|
+
payload: {
|
|
381
|
+
type: "sealed",
|
|
382
|
+
attemptId,
|
|
383
|
+
toolCallId: options.toolCallId,
|
|
384
|
+
role: options.role,
|
|
385
|
+
accepted: options.accepted,
|
|
386
|
+
},
|
|
387
|
+
source: "role-runtime",
|
|
388
|
+
cwd: options.context.cwd,
|
|
389
|
+
...(home !== undefined ? { home } : {}),
|
|
390
|
+
...(sessionParent === undefined ? {} : { sessionParent }),
|
|
391
|
+
});
|
|
392
|
+
state.prior = pointer;
|
|
393
|
+
}
|
|
341
394
|
/**
|
|
342
395
|
* Submission ledger host — record only (#836).
|
|
343
396
|
* Each terminating submission is appended with the role's original payload.
|
|
@@ -346,30 +399,10 @@ async function restoreState(cwd, runId, scope) {
|
|
|
346
399
|
*/
|
|
347
400
|
export function createSubmissionLedgerHost(host, outputTools, failInfrastructure = (error) => { throw error; }, projectClosure = () => undefined, options) {
|
|
348
401
|
const states = new Map();
|
|
349
|
-
|
|
350
|
-
const sessionParentFromContext = (context) => {
|
|
351
|
-
const runDirectory = runDirectoryFromHostContext(context);
|
|
352
|
-
if (runDirectory !== undefined) {
|
|
353
|
-
return join(runDirectory, "session", "session.jsonl");
|
|
354
|
-
}
|
|
355
|
-
const sessionFile = context.sessionManager.getSessionFile?.();
|
|
356
|
-
if (typeof sessionFile === "string" && sessionFile.length > 0)
|
|
357
|
-
return sessionFile;
|
|
358
|
-
const sessionDir = context.sessionManager.getSessionDir?.();
|
|
359
|
-
if (typeof sessionDir === "string" && sessionDir.length > 0) {
|
|
360
|
-
return join(sessionDir, "session.jsonl");
|
|
361
|
-
}
|
|
362
|
-
return undefined;
|
|
363
|
-
};
|
|
364
|
-
const resolveHomeFromContext = (context) => {
|
|
365
|
-
if (options?.home !== undefined)
|
|
366
|
-
return options.home;
|
|
367
|
-
const sessionParent = sessionParentFromContext(context);
|
|
368
|
-
return sessionParent !== undefined ? tryHomeFromAkRolesPath(sessionParent) : undefined;
|
|
369
|
-
};
|
|
402
|
+
const resolveHomeFromContext = (context) => homeFromHostContext(context, options?.home);
|
|
370
403
|
const stateFor = (context, runId) => states.get(runId) ?? (() => {
|
|
371
404
|
const home = resolveHomeFromContext(context);
|
|
372
|
-
const sessionParent =
|
|
405
|
+
const sessionParent = sessionParentFromHostContext(context);
|
|
373
406
|
const pending = restoreState(context.cwd, runId, {
|
|
374
407
|
...(home === undefined ? {} : { home }),
|
|
375
408
|
...(sessionParent === undefined ? {} : { sessionParent }),
|
|
@@ -379,7 +412,7 @@ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure
|
|
|
379
412
|
})();
|
|
380
413
|
const appendFor = (state, context, runId, attemptId, event) => {
|
|
381
414
|
const home = resolveHomeFromContext(context);
|
|
382
|
-
const sessionParent =
|
|
415
|
+
const sessionParent = sessionParentFromHostContext(context);
|
|
383
416
|
const pointer = sitianReport({
|
|
384
417
|
level: "event",
|
|
385
418
|
kind: event.type,
|
package/package.json
CHANGED
|
@@ -14,5 +14,7 @@
|
|
|
14
14
|
| `run-lifecycle.ts` A4.2 | `重新读 <path>` | 前轮已删改写。 |
|
|
15
15
|
| `submission-correctable-error.ts` A4.5 | `终局交卷并非本轮唯一工具调用。` | 删。Pi/ACP `deliverSubmissionRejection` 不再注入该句。 |
|
|
16
16
|
| `role-runtime.ts` 催交 | `本会话尚无已接受的 typed 回执。` | **留**(轮次/预算,Q2)。 |
|
|
17
|
+
| `navigator-attendance.ts` early prepare 开场 | `父衙门进行中。请在本宿主会话熟悉下列材料并待命;结算结果送达后,再给出下一步建议。本轮不要提交最终路线建议。` | #959 迁入 `resources/navigator-route-playbook.md`「到场阶段」;代码只留中立指针句「本轮为待命轮。」 |
|
|
18
|
+
| `navigator-attendance.ts` settlement feed 开场 | `父衙门结算已送达。请根据下列材料给出下一步建议。` | #959 迁入同手册;代码只留中立指针句「本轮为结算投喂轮。」 |
|
|
17
19
|
|
|
18
20
|
催交句与 2.1–2.3 打回文、重问三句不在删除列。
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 到场阶段(#959 / ADR 0073)
|
|
2
|
+
|
|
3
|
+
代码只传中立阶段指针句与 typed 材料;阶段行为说明住本手册,随案附卷。
|
|
4
|
+
|
|
5
|
+
- **待命轮**:父衙门进行中。请在本宿主会话熟悉下列材料并待命;结算结果送达后,再给出下一步建议。本轮不要提交最终路线建议。
|
|
6
|
+
- **结算投喂轮**:父衙门结算已送达。请根据下列材料给出下一步建议。
|
|
7
|
+
|
|
8
|
+
Early 待命轮的输出由执行机制丢弃;最终路线建议只在结算投喂轮给出。
|
|
9
|
+
|
|
1
10
|
## 宿主轴(与角色命令面)
|
|
2
11
|
|
|
3
12
|
游奕使自动出席,亦可 `ak-role navigator` 直调(自动出席不变)。主会话宿主由公开角色的 host 轴决定:调用 `--host` → 席位 `config set-host` → 默认 `pi`。配置默认 host 后,调用者仍用与 Pi 相同的 `ak-role <role> …` 命令面;游奕使随该宿主下的角色跑次出席,席位模型走 run 目录 `institutional-resolution.json` 的 navigator 座(显式 `config set navigator` 优先,否则继承父席有效模型),不再依赖父宿主 ExtensionContext。
|
|
@@ -5,7 +5,6 @@ import { build } from "esbuild";
|
|
|
5
5
|
|
|
6
6
|
const entries = [
|
|
7
7
|
"packaged-role-registry",
|
|
8
|
-
"public-command-renderer",
|
|
9
8
|
"work-subject-identity",
|
|
10
9
|
"navigator-invocation-identity",
|
|
11
10
|
// Static import of navigator-invocation-identity (#603: non-bundle graph closure).
|
|
@@ -22,6 +21,10 @@ const entries = [
|
|
|
22
21
|
"activation-ledger-topology",
|
|
23
22
|
"activation-reconciliation",
|
|
24
23
|
"archivist-record-entry",
|
|
24
|
+
// Pure subject nest topology — static import of archivist-record-entry;
|
|
25
|
+
// cold discovery without SessionManager (#636). Keep listed while that
|
|
26
|
+
// relative edge remains (#857 removal left the import graph open).
|
|
27
|
+
"archivist-record-topology",
|
|
25
28
|
// Session material loaders used by published non-bundle roots.
|
|
26
29
|
"session-opening-materials",
|
|
27
30
|
// Value-import closure of published non-bundle roots (build-package-only loadable).
|
|
@@ -205,6 +208,8 @@ export async function buildPackageArtifacts() {
|
|
|
205
208
|
await mkdir(pluginDir, { recursive: true });
|
|
206
209
|
await cp("resources/method-host-plugin/.claude-plugin", join(pluginDir, ".claude-plugin"), { recursive: true });
|
|
207
210
|
await cp("resources/methods", join(pluginDir, "skills"), { recursive: true });
|
|
211
|
+
// Packaged handbook/playbook paths resolve via injected packageRoot (#962) —
|
|
212
|
+
// do not mirror resources/ into dist/resources (one authority, no parallel copy).
|
|
208
213
|
}
|
|
209
214
|
|
|
210
215
|
const isMain =
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
} from "../external-host-turn-loop.ts";
|
|
10
10
|
|
|
11
11
|
import { reportHostSessionEvent } from "../host-session-record.ts";
|
|
12
|
+
import {
|
|
13
|
+
NAVIGATOR_OUTPUT_TOOL_NAME,
|
|
14
|
+
navigatorProseFromUnknown,
|
|
15
|
+
} from "../package-contracts/navigator-output.ts";
|
|
12
16
|
import {
|
|
13
17
|
renderSystemPromptOverride,
|
|
14
18
|
type PreparedRoleTurn,
|
|
@@ -16,6 +20,53 @@ import {
|
|
|
16
20
|
} from "../prepared-role-turn.ts";
|
|
17
21
|
import { acpModelId, type AcpHostDescription } from "./description.ts";
|
|
18
22
|
|
|
23
|
+
/**
|
|
24
|
+
* #959: collect free-form agent text from ACP session/update stream.
|
|
25
|
+
* Used only when the navigator seat spoke prose without calling the output tool.
|
|
26
|
+
* ACP agent speech is only agent_message / agent_message_chunk — never user,
|
|
27
|
+
* thought, or other *_message kinds (load replay must not poison the bucket).
|
|
28
|
+
*
|
|
29
|
+
* Standard ACP nests under params.update: { sessionUpdate, content }.
|
|
30
|
+
* Flat params.sessionUpdate / string params.update remain accepted for host variants.
|
|
31
|
+
*/
|
|
32
|
+
function acpAgentTextChunk(params: Readonly<Record<string, unknown>>): string | undefined {
|
|
33
|
+
let kind: unknown;
|
|
34
|
+
let content: unknown;
|
|
35
|
+
let textFallback: unknown;
|
|
36
|
+
|
|
37
|
+
const nested = params.update;
|
|
38
|
+
if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) {
|
|
39
|
+
const record = nested as Record<string, unknown>;
|
|
40
|
+
kind = record.sessionUpdate;
|
|
41
|
+
content = record.content;
|
|
42
|
+
textFallback = record.text;
|
|
43
|
+
} else {
|
|
44
|
+
kind = params.sessionUpdate ?? nested;
|
|
45
|
+
content = params.content;
|
|
46
|
+
textFallback = params.text;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (kind !== "agent_message_chunk" && kind !== "agent_message") return undefined;
|
|
50
|
+
if (typeof content === "string" && content.length > 0) return content;
|
|
51
|
+
if (typeof content === "object" && content !== null && !Array.isArray(content)) {
|
|
52
|
+
const record = content as Record<string, unknown>;
|
|
53
|
+
if (typeof record.text === "string" && record.text.length > 0) return record.text;
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(content)) {
|
|
56
|
+
const parts: string[] = [];
|
|
57
|
+
for (const part of content) {
|
|
58
|
+
if (typeof part === "string" && part.length > 0) parts.push(part);
|
|
59
|
+
else if (typeof part === "object" && part !== null) {
|
|
60
|
+
const record = part as Record<string, unknown>;
|
|
61
|
+
if (typeof record.text === "string" && record.text.length > 0) parts.push(record.text);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (parts.length > 0) return parts.join("");
|
|
65
|
+
}
|
|
66
|
+
if (typeof textFallback === "string" && textFallback.length > 0) return textFallback;
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
19
70
|
/** ACP v1 surface used by the generic ACP adapter. Protocol details stay in this module. */
|
|
20
71
|
export interface AcpConnection {
|
|
21
72
|
request(method: string, params: Readonly<Record<string, unknown>>): Promise<Readonly<Record<string, unknown>>>;
|
|
@@ -43,7 +94,13 @@ export type AcpRoleTurnHostConfig = Readonly<{
|
|
|
43
94
|
prepare(request: RoleTurnRequest): Promise<PreparedRoleTurn>;
|
|
44
95
|
}>;
|
|
45
96
|
|
|
46
|
-
function failure(
|
|
97
|
+
function failure(
|
|
98
|
+
cause: "activation" | "session" | "output",
|
|
99
|
+
name: string,
|
|
100
|
+
code: string,
|
|
101
|
+
details?: Readonly<Record<string, unknown>>,
|
|
102
|
+
diagnostic?: string,
|
|
103
|
+
): RoleTurnResult {
|
|
47
104
|
return {
|
|
48
105
|
code: null,
|
|
49
106
|
stderr: "",
|
|
@@ -51,11 +108,27 @@ function failure(cause: "activation" | "session" | "output", name: string, code:
|
|
|
51
108
|
knownFailure: {
|
|
52
109
|
cause,
|
|
53
110
|
identity: { name, code },
|
|
111
|
+
...(diagnostic === undefined ? {} : { diagnostic }),
|
|
54
112
|
...(details === undefined ? {} : { details }),
|
|
55
113
|
},
|
|
56
114
|
};
|
|
57
115
|
}
|
|
58
116
|
|
|
117
|
+
/** Success→dispose failure; existing failure keeps primary cause + cleanup detail. */
|
|
118
|
+
function withCleanupFailure(outcome: RoleTurnResult, cleanupError: unknown): RoleTurnResult {
|
|
119
|
+
const message = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
120
|
+
if (outcome.knownFailure === undefined) {
|
|
121
|
+
return failure("session", "AcpDisposeFailure", "dispose-failed", { cleanupError: message }, message);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
...outcome,
|
|
125
|
+
knownFailure: {
|
|
126
|
+
...outcome.knownFailure,
|
|
127
|
+
details: { ...(outcome.knownFailure.details ?? {}), cleanupError: message },
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
59
132
|
type RpcReply = { readonly id?: unknown; readonly method?: unknown; readonly params?: unknown; readonly result?: unknown; readonly error?: unknown };
|
|
60
133
|
|
|
61
134
|
function acpError(code: string, message: string, cause?: unknown): Error & { readonly code: string } {
|
|
@@ -182,10 +255,12 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
182
255
|
let connection: AcpConnection | undefined;
|
|
183
256
|
let sessionId: string | undefined;
|
|
184
257
|
let accepted = false;
|
|
258
|
+
// Mutable so dispose failure can outrank a clean turn (headless withCleanupFailure face).
|
|
259
|
+
let outcome: RoleTurnResult = failure("session", "AcpNoOutcome", "no-outcome");
|
|
185
260
|
try {
|
|
186
261
|
if (prepared.mcpServers.length === 0) {
|
|
187
|
-
|
|
188
|
-
}
|
|
262
|
+
outcome = failure("activation", "UncontrolledAcpSession", "ak-config-missing");
|
|
263
|
+
} else {
|
|
189
264
|
connection = await config.connect(request);
|
|
190
265
|
// Live host-session records: ACP session/update → sitian sole entry (#811).
|
|
191
266
|
// One abort + one race helper + one outer projection — write failure ends
|
|
@@ -214,8 +289,17 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
214
289
|
): Promise<Readonly<Record<string, unknown>>> =>
|
|
215
290
|
raceAgainstHostAbort(connection!.request(method, params), recordAbort.signal, "host-session-record-failed");
|
|
216
291
|
|
|
292
|
+
// #959: navigator free-form agent text — only while session/prompt is in flight.
|
|
293
|
+
// session/load replays history via session/update; those must not enter the bucket
|
|
294
|
+
// (resume / set_model load would otherwise prepend prior turns as "this turn" prose).
|
|
295
|
+
const agentProseChunks: string[] = [];
|
|
296
|
+
let collectAgentProse = false;
|
|
217
297
|
connection.onNotification?.((method, params) => {
|
|
218
298
|
if (method !== "session/update" || hostSessionRecordFailure !== undefined) return;
|
|
299
|
+
if (collectAgentProse) {
|
|
300
|
+
const chunk = acpAgentTextChunk(params);
|
|
301
|
+
if (chunk !== undefined) agentProseChunks.push(chunk);
|
|
302
|
+
}
|
|
219
303
|
try {
|
|
220
304
|
reportHostSessionEvent({
|
|
221
305
|
host: config.hostName,
|
|
@@ -243,11 +327,11 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
243
327
|
if (request.model !== undefined && availableModels !== undefined && !availableModels.some((entry) =>
|
|
244
328
|
typeof entry === "object" && entry !== null
|
|
245
329
|
&& (entry as { modelId?: unknown }).modelId === acpModelId(config.modelPassing, request.model))) {
|
|
246
|
-
|
|
330
|
+
outcome = failure("activation", "AcpHostModelMismatch", "host-model-mismatch", {
|
|
247
331
|
provider: request.model.provider,
|
|
248
332
|
model: request.model.model,
|
|
249
333
|
});
|
|
250
|
-
}
|
|
334
|
+
} else {
|
|
251
335
|
|
|
252
336
|
const sessionBindParams = {
|
|
253
337
|
cwd: request.cwd,
|
|
@@ -269,15 +353,19 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
269
353
|
sessionId = await loadSession(boundSessionId);
|
|
270
354
|
}
|
|
271
355
|
}
|
|
356
|
+
let sessionReady = true;
|
|
272
357
|
if (sessionId === undefined) {
|
|
273
358
|
const session = await rpc("session/new", sessionBindParams);
|
|
274
359
|
sessionId = typeof session.sessionId === "string" ? session.sessionId : undefined;
|
|
275
360
|
if (sessionId === undefined || sessionId === "") {
|
|
276
|
-
|
|
361
|
+
outcome = failure("session", "AcpSessionFailure", "session-id-missing");
|
|
362
|
+
sessionReady = false;
|
|
363
|
+
} else {
|
|
364
|
+
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
277
365
|
}
|
|
278
|
-
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
279
366
|
}
|
|
280
367
|
|
|
368
|
+
if (sessionReady) {
|
|
281
369
|
// set_model seat provider:model (#778); may rebuild agent — re-bind via loadSession.
|
|
282
370
|
if (config.modelPassing === "set_model" && request.model !== undefined && sessionId !== undefined) {
|
|
283
371
|
await rpc("session/set_model", {
|
|
@@ -300,6 +388,9 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
300
388
|
if (abortSignal !== undefined) abortParts.push(abortSignal);
|
|
301
389
|
const combinedAbort = AbortSignal.any(abortParts);
|
|
302
390
|
let result: Readonly<Record<string, unknown>>;
|
|
391
|
+
// Open the prose gate only for this prompt round; clear any stale chunks first.
|
|
392
|
+
agentProseChunks.length = 0;
|
|
393
|
+
collectAgentProse = prepared.terminatingToolName === NAVIGATOR_OUTPUT_TOOL_NAME;
|
|
303
394
|
try {
|
|
304
395
|
result = await raceAgainstHostAbort(
|
|
305
396
|
activeConnection.request("session/prompt", {
|
|
@@ -310,17 +401,33 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
310
401
|
"ACP host aborted",
|
|
311
402
|
);
|
|
312
403
|
} catch (error) {
|
|
404
|
+
collectAgentProse = false;
|
|
405
|
+
agentProseChunks.length = 0;
|
|
313
406
|
if (hostSessionRecordFailure !== undefined) {
|
|
314
407
|
return { status: "terminal", result: hostSessionRecordResult() };
|
|
315
408
|
}
|
|
316
409
|
throw error;
|
|
317
410
|
}
|
|
411
|
+
collectAgentProse = false;
|
|
318
412
|
if (result.stopReason === "refusal") {
|
|
413
|
+
agentProseChunks.length = 0;
|
|
319
414
|
return {
|
|
320
415
|
status: "terminal",
|
|
321
416
|
result: failure("output", "AcpRefusal", "refusal", { sessionId }),
|
|
322
417
|
};
|
|
323
418
|
}
|
|
419
|
+
// #959: navigator prose exit when the model spoke without the output tool.
|
|
420
|
+
// Tool path still wins via MCP; ingest is a no-op once the tool already sealed.
|
|
421
|
+
// Emptiness via shared projector; payload keeps original bytes (LLM 原话过手).
|
|
422
|
+
if (prepared.terminatingToolName === NAVIGATOR_OUTPUT_TOOL_NAME) {
|
|
423
|
+
const prose = agentProseChunks.join("");
|
|
424
|
+
agentProseChunks.length = 0;
|
|
425
|
+
if (navigatorProseFromUnknown(prose) !== undefined) {
|
|
426
|
+
await prepared.ingestStructuredOutput({ prose });
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
agentProseChunks.length = 0;
|
|
430
|
+
}
|
|
324
431
|
return { status: "delivered", stderr: activeConnection.stderr?.() ?? "" };
|
|
325
432
|
},
|
|
326
433
|
async afterAccepted() {
|
|
@@ -328,14 +435,19 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
328
435
|
accepted = true;
|
|
329
436
|
},
|
|
330
437
|
});
|
|
331
|
-
|
|
332
|
-
|
|
438
|
+
outcome = hostSessionRecordFailure !== undefined ? hostSessionRecordResult() : turnResult;
|
|
439
|
+
} // sessionReady
|
|
440
|
+
} // model match else
|
|
333
441
|
} catch (error) {
|
|
334
442
|
// recordAbort races setup RPCs via raceAgainstHostAbort (host-aborted code);
|
|
335
443
|
// noteHostSessionRecordFailure always sets the typed failure before aborting.
|
|
336
|
-
if (hostSessionRecordFailure !== undefined)
|
|
337
|
-
|
|
444
|
+
if (hostSessionRecordFailure !== undefined) {
|
|
445
|
+
outcome = hostSessionRecordResult();
|
|
446
|
+
} else {
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
338
449
|
}
|
|
450
|
+
} // mcpServers else
|
|
339
451
|
} finally {
|
|
340
452
|
if (connection !== undefined) {
|
|
341
453
|
if (sessionId !== undefined && !accepted) {
|
|
@@ -345,8 +457,12 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
345
457
|
try { await connection.close(); }
|
|
346
458
|
catch { /* keep turn result */ }
|
|
347
459
|
}
|
|
348
|
-
try {
|
|
349
|
-
|
|
460
|
+
try {
|
|
461
|
+
await prepared.dispose?.();
|
|
462
|
+
} catch (cleanupError) {
|
|
463
|
+
outcome = withCleanupFailure(outcome, cleanupError);
|
|
464
|
+
}
|
|
350
465
|
}
|
|
466
|
+
return outcome;
|
|
351
467
|
});
|
|
352
468
|
}
|