@akagilnc/pi-workflow-roles 0.1.3718 → 0.1.3733
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/description.js +19 -5
- package/dist/acp-host/production-host.js +210 -85
- package/dist/acp-host/seat-profile-soul.js +78 -0
- package/dist/auditor-dossier-tool.js +15 -9
- package/dist/gatekeeper-role.js +13 -10
- package/dist/host-descriptions.js +26 -0
- package/dist/public-cli/inspector-run.js +2 -2
- package/dist/public-cli/instruction-seat-run.js +1 -1
- package/dist/public-cli/main.js +27 -0
- package/dist/public-cli/notary-run.js +2 -2
- package/dist/public-role-summons.js +10 -6
- package/package.json +1 -1
- package/src/acp-host/description.ts +39 -4
- package/src/acp-host/production-host.ts +16 -1
- package/src/acp-host/role-envelope.ts +2 -1
- package/src/acp-host/role-turn-host.ts +47 -23
- package/src/acp-host/seat-profile-soul.ts +97 -0
- package/src/auditor-dossier-tool.ts +26 -14
- package/src/gatekeeper-role.ts +32 -12
- package/src/host-descriptions.ts +26 -0
- package/src/public-cli/inspector-run.ts +3 -3
- package/src/public-cli/instruction-seat-run.ts +2 -2
- package/src/public-cli/notary-run.ts +3 -3
- package/src/public-role-summons.ts +26 -14
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seat-scoped host profile soul delivery (#644 / 拍 2).
|
|
3
|
+
*
|
|
4
|
+
* Hermes has no per-session systemPrompt channel; identity is the profile's
|
|
5
|
+
* SOUL.md. Each seat owns `profiles/<namePrefix><role>/`, and SOUL.md is a
|
|
6
|
+
* symlink to the packaged `souls/<role>.md` (package is the sole soul source).
|
|
7
|
+
*/
|
|
8
|
+
import { constants } from "node:fs";
|
|
9
|
+
import { access, copyFile, lstat, mkdir, readlink, symlink, unlink } from "node:fs/promises";
|
|
10
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
11
|
+
/** Profile id for one seat role. */
|
|
12
|
+
export function seatProfileName(spec, role) {
|
|
13
|
+
return `${spec.namePrefix}${role}`;
|
|
14
|
+
}
|
|
15
|
+
/** Packaged soul path for one role (`souls/<role>.md`). */
|
|
16
|
+
export function packageRoleSoulPath(packageRoot, role) {
|
|
17
|
+
return join(packageRoot, "souls", `${role}.md`);
|
|
18
|
+
}
|
|
19
|
+
async function pathExists(path) {
|
|
20
|
+
try {
|
|
21
|
+
await access(path, constants.F_OK);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Ensure the seat profile directory exists and SOUL.md is a symlink to the
|
|
30
|
+
* packaged role soul. On first create, copy credential surfaces from the host
|
|
31
|
+
* root (parent of the profiles root) so the profile can authenticate without
|
|
32
|
+
* touching the default profile in place. Returns the profile id for argv.
|
|
33
|
+
*/
|
|
34
|
+
export async function ensureSeatProfileSoul(options) {
|
|
35
|
+
const { spec, operatorHome, packageRoot, role } = options;
|
|
36
|
+
const profileName = seatProfileName(spec, role);
|
|
37
|
+
const soulTarget = resolve(packageRoleSoulPath(packageRoot, role));
|
|
38
|
+
if (!(await pathExists(soulTarget))) {
|
|
39
|
+
throw new Error(`packaged role soul missing: ${soulTarget}`);
|
|
40
|
+
}
|
|
41
|
+
const profilesRoot = join(operatorHome, ...spec.profilesRootFromHome);
|
|
42
|
+
const profileDir = join(profilesRoot, profileName);
|
|
43
|
+
const hostRoot = dirname(profilesRoot);
|
|
44
|
+
const soulPath = join(profileDir, spec.soulFileName);
|
|
45
|
+
if (!(await pathExists(profileDir))) {
|
|
46
|
+
await mkdir(profileDir, { recursive: true });
|
|
47
|
+
// First-create credential bootstrap only. Never rewrite an existing profile's
|
|
48
|
+
// auth/config; never write into the host root / default profile.
|
|
49
|
+
for (const name of ["auth.json", ".env", "config.yaml"]) {
|
|
50
|
+
const source = join(hostRoot, name);
|
|
51
|
+
if (!(await pathExists(source)))
|
|
52
|
+
continue;
|
|
53
|
+
await copyFile(source, join(profileDir, name));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
await mkdir(profileDir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
const desiredLink = relative(profileDir, soulTarget);
|
|
60
|
+
let current;
|
|
61
|
+
try {
|
|
62
|
+
const st = await lstat(soulPath);
|
|
63
|
+
if (st.isSymbolicLink()) {
|
|
64
|
+
current = await readlink(soulPath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
current = undefined;
|
|
69
|
+
}
|
|
70
|
+
if (current === desiredLink || current === soulTarget) {
|
|
71
|
+
return profileName;
|
|
72
|
+
}
|
|
73
|
+
if (await pathExists(soulPath) || current !== undefined) {
|
|
74
|
+
await unlink(soulPath);
|
|
75
|
+
}
|
|
76
|
+
await symlink(desiredLink, soulPath);
|
|
77
|
+
return profileName;
|
|
78
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { readableGateItem } from "./readable-gate-item.js";
|
|
3
4
|
import { Type } from "typebox";
|
|
4
5
|
const AUDITOR_DOSSIER_TOOL_NAME = "ak_get_run_dossier";
|
|
5
6
|
const GATE_SUBMISSION_CANDIDATE_FILE = "gate-submission-candidate.json";
|
|
@@ -29,16 +30,20 @@ function readLatestToolCallLeaf(context) {
|
|
|
29
30
|
function gateSubmissionCandidatePath(runDirectory) {
|
|
30
31
|
return join(runDirectory, "artifacts", GATE_SUBMISSION_CANDIDATE_FILE);
|
|
31
32
|
}
|
|
32
|
-
function
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
function readLatestSubmissionArguments(context) {
|
|
34
|
+
const leaf = readLatestToolCallLeaf(context);
|
|
35
|
+
if (!isRecord(leaf) || !isRecord(leaf.message) || !Array.isArray(leaf.message.content)) {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
for (const part of leaf.message.content) {
|
|
39
|
+
if (isRecord(part) && part.type === "toolCall" && "arguments" in part) {
|
|
40
|
+
return part.arguments;
|
|
41
|
+
}
|
|
40
42
|
}
|
|
41
|
-
return
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
function buildGateOfficerReviewInstruction(input) {
|
|
46
|
+
return readableGateItem(input.submission);
|
|
42
47
|
}
|
|
43
48
|
function persistGateSubmissionCandidate(runDirectory, context) {
|
|
44
49
|
const leaf = readLatestToolCallLeaf(context);
|
|
@@ -86,5 +91,6 @@ export {
|
|
|
86
91
|
createAuditorDossierTool,
|
|
87
92
|
gateSubmissionCandidatePath,
|
|
88
93
|
persistGateSubmissionCandidate,
|
|
94
|
+
readLatestSubmissionArguments,
|
|
89
95
|
readLatestToolCallLeaf
|
|
90
96
|
};
|
package/dist/gatekeeper-role.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { auditorRunDirectory, persistGateSubmissionCandidate, } from "./auditor-dossier-tool.js";
|
|
1
|
+
import { auditorRunDirectory, persistGateSubmissionCandidate, readLatestSubmissionArguments, } from "./auditor-dossier-tool.js";
|
|
2
2
|
import { GatekeeperDecisionError } from "./submission-errors.js";
|
|
3
3
|
import { INSPECTOR_OUTPUT_TOOL_NAME } from "./inspector-contracts.js";
|
|
4
4
|
import { GATEKEEPER_OUTPUT_TOOL_NAME, gatekeeperDecisionSchema, gatekeeperOutputSchema, } from "./package-contracts/gatekeeper-output.js";
|
|
@@ -173,14 +173,15 @@ export async function projectGatekeeperRun(options) {
|
|
|
173
173
|
},
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
176
|
+
// #632: Grok session.jsonl is header-only — freeze the in-memory tool-call leaf
|
|
177
|
+
// as a run artifact so dossier exploration still resolves. LLM→LLM resume does
|
|
178
|
+
// not ride this path: submission body goes verbatim on gateReviewInstruction (#786).
|
|
179
|
+
persistGateSubmissionCandidate(runDirectory, options.context);
|
|
180
|
+
const submission = readLatestSubmissionArguments(options.context);
|
|
180
181
|
let summoned;
|
|
181
182
|
try {
|
|
182
183
|
const summon = options.summonOfficer
|
|
183
|
-
?? (async (nextOfficer, sourceRunDirectory, officerSignal, reask) => {
|
|
184
|
+
?? (async (nextOfficer, sourceRunDirectory, officerSignal, reask, nextSubmission) => {
|
|
184
185
|
const { summonGateOfficer } = await import("./public-role-summons.js");
|
|
185
186
|
return summonGateOfficer({
|
|
186
187
|
officer: nextOfficer,
|
|
@@ -188,12 +189,14 @@ export async function projectGatekeeperRun(options) {
|
|
|
188
189
|
cwd: options.context.cwd ?? process.cwd(),
|
|
189
190
|
...(officerSignal === undefined ? {} : { signal: officerSignal }),
|
|
190
191
|
...(reask === undefined ? {} : { reask }),
|
|
191
|
-
...(
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
...(nextSubmission === undefined ? {} : { submission: nextSubmission }),
|
|
193
|
+
...(options.home === undefined ? {} : { home: options.home }),
|
|
194
|
+
...(options.packageRoot === undefined ? {} : { packageRoot: options.packageRoot }),
|
|
195
|
+
...(options.roleTurnHost === undefined ? {} : { roleTurnHost: options.roleTurnHost }),
|
|
196
|
+
...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
|
|
194
197
|
});
|
|
195
198
|
});
|
|
196
|
-
summoned = await summon(officer, runDirectory, options.signal, options.reask);
|
|
199
|
+
summoned = await summon(officer, runDirectory, options.signal, options.reask, submission);
|
|
197
200
|
}
|
|
198
201
|
catch (error) {
|
|
199
202
|
return {
|
|
@@ -10,6 +10,7 @@ export const HOST_DESCRIPTIONS = Object.freeze({
|
|
|
10
10
|
suffix: Object.freeze(["stdio"]),
|
|
11
11
|
modelFlag: "--model",
|
|
12
12
|
}),
|
|
13
|
+
modelPassing: "argv",
|
|
13
14
|
boundResume: "session/load",
|
|
14
15
|
sessionBindingFile: "grok-acp-session.json",
|
|
15
16
|
childEnv: Object.freeze({
|
|
@@ -18,6 +19,31 @@ export const HOST_DESCRIPTIONS = Object.freeze({
|
|
|
18
19
|
GROK_SUBAGENTS: "0",
|
|
19
20
|
}),
|
|
20
21
|
}),
|
|
22
|
+
/**
|
|
23
|
+
* Operator home `~/.hermes`, native session/load resume, `acp` subcommand.
|
|
24
|
+
* Model arrives as an ACP `session/set_model` RPC with modelId `provider:model`
|
|
25
|
+
* (seat table provider + model concatenated). Reasoning is the global
|
|
26
|
+
* `--reasoning` flag before `acp`. Soul is the seat profile SOUL.md symlink
|
|
27
|
+
* (`hermes -p ak-<role> …`); package `souls/<role>.md` is the sole source.
|
|
28
|
+
*/
|
|
29
|
+
"hermes": Object.freeze({
|
|
30
|
+
binaryFromHome: Object.freeze([".local", "bin", "hermes"]),
|
|
31
|
+
argv: Object.freeze({
|
|
32
|
+
prefix: Object.freeze(["acp"]),
|
|
33
|
+
suffix: Object.freeze([]),
|
|
34
|
+
thinkingFlag: "--reasoning",
|
|
35
|
+
}),
|
|
36
|
+
modelPassing: "set_model",
|
|
37
|
+
boundResume: "session/load",
|
|
38
|
+
sessionBindingFile: "hermes-acp-session.json",
|
|
39
|
+
childEnv: Object.freeze({}),
|
|
40
|
+
seatProfileSoul: Object.freeze({
|
|
41
|
+
flag: "-p",
|
|
42
|
+
namePrefix: "ak-",
|
|
43
|
+
profilesRootFromHome: Object.freeze([".hermes", "profiles"]),
|
|
44
|
+
soulFileName: "SOUL.md",
|
|
45
|
+
}),
|
|
46
|
+
}),
|
|
21
47
|
});
|
|
22
48
|
export function lookupHostDescription(host) {
|
|
23
49
|
return Object.hasOwn(HOST_DESCRIPTIONS, host) ? HOST_DESCRIPTIONS[host] : undefined;
|
|
@@ -31,7 +31,7 @@ export async function runPublicInspector(argv, env, io, parseInspectorArgv) {
|
|
|
31
31
|
// No bare catch→fresh: lookup/resume failures surface; only true absence mints new.
|
|
32
32
|
const projectRoot = parsed.project ?? env.cwd;
|
|
33
33
|
// #747: parentRunPath is the pure 卷宗指针 path only — never reask/materials text.
|
|
34
|
-
// #753: gate re-ask /
|
|
34
|
+
// #753/#786: gate re-ask / verbatim submission body ride summons.instruction on resume.
|
|
35
35
|
const parentRunPath = parentRunPathFromGatePointerInstruction(parsed.instruction);
|
|
36
36
|
if (parentRunPath !== undefined) {
|
|
37
37
|
const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction;
|
|
@@ -57,7 +57,7 @@ export async function runPublicInspector(argv, env, io, parseInspectorArgv) {
|
|
|
57
57
|
return resumed;
|
|
58
58
|
// Reask without a prior same-parent run cannot deliver the plain-language ask
|
|
59
59
|
// on a fresh mint without inventing a second prompt path — fail loud (#753).
|
|
60
|
-
// gateReviewInstruction alone is resume-only; fresh mint keeps argv 卷宗指针.
|
|
60
|
+
// gateReviewInstruction (verbatim body) alone is resume-only; fresh mint keeps argv 卷宗指针.
|
|
61
61
|
if (env.reviewReask !== undefined) {
|
|
62
62
|
presentStructuralRejection(new CliUsageError("inspector review reask requires a prior same-parent run to resume"), io);
|
|
63
63
|
return { exitCode: 2 };
|
|
@@ -150,7 +150,7 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
|
|
|
150
150
|
return { exitCode: 2 };
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
-
// #756: auditor reask /
|
|
153
|
+
// #756/#786: auditor reask / verbatim submission body ride summons.instruction on resume.
|
|
154
154
|
const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction;
|
|
155
155
|
const summons = {
|
|
156
156
|
...(resumeInstruction === undefined
|
package/dist/public-cli/main.js
CHANGED
|
@@ -15446,6 +15446,7 @@ var init_host_descriptions = __esm({
|
|
|
15446
15446
|
suffix: Object.freeze(["stdio"]),
|
|
15447
15447
|
modelFlag: "--model"
|
|
15448
15448
|
}),
|
|
15449
|
+
modelPassing: "argv",
|
|
15449
15450
|
boundResume: "session/load",
|
|
15450
15451
|
sessionBindingFile: "grok-acp-session.json",
|
|
15451
15452
|
childEnv: Object.freeze({
|
|
@@ -15453,6 +15454,31 @@ var init_host_descriptions = __esm({
|
|
|
15453
15454
|
GROK_MEMORY: "0",
|
|
15454
15455
|
GROK_SUBAGENTS: "0"
|
|
15455
15456
|
})
|
|
15457
|
+
}),
|
|
15458
|
+
/**
|
|
15459
|
+
* Operator home `~/.hermes`, native session/load resume, `acp` subcommand.
|
|
15460
|
+
* Model arrives as an ACP `session/set_model` RPC with modelId `provider:model`
|
|
15461
|
+
* (seat table provider + model concatenated). Reasoning is the global
|
|
15462
|
+
* `--reasoning` flag before `acp`. Soul is the seat profile SOUL.md symlink
|
|
15463
|
+
* (`hermes -p ak-<role> …`); package `souls/<role>.md` is the sole source.
|
|
15464
|
+
*/
|
|
15465
|
+
"hermes": Object.freeze({
|
|
15466
|
+
binaryFromHome: Object.freeze([".local", "bin", "hermes"]),
|
|
15467
|
+
argv: Object.freeze({
|
|
15468
|
+
prefix: Object.freeze(["acp"]),
|
|
15469
|
+
suffix: Object.freeze([]),
|
|
15470
|
+
thinkingFlag: "--reasoning"
|
|
15471
|
+
}),
|
|
15472
|
+
modelPassing: "set_model",
|
|
15473
|
+
boundResume: "session/load",
|
|
15474
|
+
sessionBindingFile: "hermes-acp-session.json",
|
|
15475
|
+
childEnv: Object.freeze({}),
|
|
15476
|
+
seatProfileSoul: Object.freeze({
|
|
15477
|
+
flag: "-p",
|
|
15478
|
+
namePrefix: "ak-",
|
|
15479
|
+
profilesRootFromHome: Object.freeze([".hermes", "profiles"]),
|
|
15480
|
+
soulFileName: "SOUL.md"
|
|
15481
|
+
})
|
|
15456
15482
|
})
|
|
15457
15483
|
});
|
|
15458
15484
|
}
|
|
@@ -21973,6 +21999,7 @@ var init_method_skill = __esm({
|
|
|
21973
21999
|
var init_auditor_dossier_tool = __esm({
|
|
21974
22000
|
"src/auditor-dossier-tool.ts"() {
|
|
21975
22001
|
"use strict";
|
|
22002
|
+
init_readable_gate_item();
|
|
21976
22003
|
init_build();
|
|
21977
22004
|
}
|
|
21978
22005
|
});
|
|
@@ -52,7 +52,7 @@ export async function runPublicNotary(argv, env, io, parseNotaryArgv) {
|
|
|
52
52
|
throw error;
|
|
53
53
|
}
|
|
54
54
|
// #747: officer resume key is this parent source-run path (not ticket number).
|
|
55
|
-
// #753: gate re-ask and
|
|
55
|
+
// #753/#786: gate re-ask and verbatim submission body share summons.instruction
|
|
56
56
|
// (reask wins when both present; no parallel stack).
|
|
57
57
|
{
|
|
58
58
|
const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction;
|
|
@@ -76,7 +76,7 @@ export async function runPublicNotary(argv, env, io, parseNotaryArgv) {
|
|
|
76
76
|
return resumed;
|
|
77
77
|
// Reask without a prior same-parent run cannot deliver the plain-language ask
|
|
78
78
|
// on a fresh mint without inventing a second prompt path — fail loud (#753).
|
|
79
|
-
// gateReviewInstruction alone is resume-only
|
|
79
|
+
// gateReviewInstruction (verbatim body) alone is resume-only; fresh mint ignores it.
|
|
80
80
|
if (env.reviewReask !== undefined) {
|
|
81
81
|
presentStructuralRejection(new CliUsageError("notary review reask requires a prior same-parent run to resume"), io);
|
|
82
82
|
return { exitCode: 2 };
|
|
@@ -165,8 +165,11 @@ async function summonPublicRole(options) {
|
|
|
165
165
|
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
166
166
|
// #753: reask rides the existing notary same-ticket resume summons.instruction.
|
|
167
167
|
...options.reviewReask === void 0 ? {} : { reviewReask: options.reviewReask },
|
|
168
|
-
// #
|
|
169
|
-
...options.gateReviewInstruction === void 0 ? {} : { gateReviewInstruction: options.gateReviewInstruction }
|
|
168
|
+
// #786: verbatim submission body on same-parent resume (not conclusion re-ask).
|
|
169
|
+
...options.gateReviewInstruction === void 0 ? {} : { gateReviewInstruction: options.gateReviewInstruction },
|
|
170
|
+
// Offline test injects — same faces as public CLI env (production leaves unset).
|
|
171
|
+
...options.roleTurnHost === void 0 ? {} : { roleTurnHost: options.roleTurnHost },
|
|
172
|
+
...options.createRunId === void 0 ? {} : { createRunId: options.createRunId }
|
|
170
173
|
};
|
|
171
174
|
const captured = options.io === void 0 ? createCapturingIo() : void 0;
|
|
172
175
|
const io = options.io ?? captured.io;
|
|
@@ -263,11 +266,10 @@ async function summonGateOfficer(options) {
|
|
|
263
266
|
home = homeFromRunDirectory(options.sourceRunDirectory);
|
|
264
267
|
}
|
|
265
268
|
let gateReviewInstruction;
|
|
266
|
-
if (options.reask === void 0) {
|
|
269
|
+
if (options.reask === void 0 && options.submission !== void 0) {
|
|
267
270
|
const { buildGateOfficerReviewInstruction } = await import("./auditor-dossier-tool.js");
|
|
268
271
|
gateReviewInstruction = buildGateOfficerReviewInstruction({
|
|
269
|
-
|
|
270
|
-
...options.submissionCandidatePath === void 0 ? {} : { submissionCandidatePath: options.submissionCandidatePath }
|
|
272
|
+
submission: options.submission
|
|
271
273
|
});
|
|
272
274
|
}
|
|
273
275
|
const common = {
|
|
@@ -277,7 +279,9 @@ async function summonGateOfficer(options) {
|
|
|
277
279
|
...options.io === void 0 ? {} : { io: options.io },
|
|
278
280
|
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
279
281
|
...options.reask === void 0 ? {} : { reviewReask: options.reask },
|
|
280
|
-
...gateReviewInstruction === void 0 ? {} : { gateReviewInstruction }
|
|
282
|
+
...gateReviewInstruction === void 0 ? {} : { gateReviewInstruction },
|
|
283
|
+
...options.roleTurnHost === void 0 ? {} : { roleTurnHost: options.roleTurnHost },
|
|
284
|
+
...options.createRunId === void 0 ? {} : { createRunId: options.createRunId }
|
|
281
285
|
};
|
|
282
286
|
if (options.officer === "notary") {
|
|
283
287
|
return summonPublicRole({
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One ACP host description. Every host-specific value the generic ACP adapter
|
|
3
|
-
* needs — binary location, argv shape, resume verb, binding filename, child env
|
|
4
|
-
* — is data here; the lifecycle in role-turn-host.ts
|
|
3
|
+
* needs — binary location, argv shape, resume verb, binding filename, child env,
|
|
4
|
+
* optional seat-profile soul — is data here; the lifecycle in role-turn-host.ts
|
|
5
|
+
* stays one copy (#732).
|
|
5
6
|
*/
|
|
6
7
|
import { join } from "node:path";
|
|
7
8
|
|
|
9
|
+
import type { SeatProfileSoul } from "./seat-profile-soul.ts";
|
|
10
|
+
|
|
8
11
|
export type AcpHostDescription = Readonly<{
|
|
9
12
|
/** Binary path segments relative to the operator home. */
|
|
10
13
|
binaryFromHome: readonly string[];
|
|
@@ -12,13 +15,28 @@ export type AcpHostDescription = Readonly<{
|
|
|
12
15
|
prefix: readonly string[];
|
|
13
16
|
suffix: readonly string[];
|
|
14
17
|
modelFlag?: string;
|
|
18
|
+
/** CLI flag whose value is the seat thinking level; placed before `prefix`
|
|
19
|
+
* so it lands ahead of the subcommand (hermes global `--reasoning`). */
|
|
15
20
|
thinkingFlag?: string;
|
|
16
21
|
}>;
|
|
22
|
+
/**
|
|
23
|
+
* How the seat model reaches the agent:
|
|
24
|
+
* - "argv": passed as the CLI `--model` flag (grok);
|
|
25
|
+
* - "set_model": sent as an ACP `session/set_model` RPC with modelId
|
|
26
|
+
* `provider:model` (hermes).
|
|
27
|
+
*/
|
|
28
|
+
modelPassing: "argv" | "set_model";
|
|
17
29
|
/** Which verb a bound resume uses; "session/new" hosts always mint + bind. */
|
|
18
30
|
boundResume: "session/load" | "session/new";
|
|
19
31
|
/** Durable ACP binding filename written beside the session principal. */
|
|
20
32
|
sessionBindingFile: string;
|
|
21
33
|
childEnv: Readonly<Record<string, string>>;
|
|
34
|
+
/**
|
|
35
|
+
* When set, the production factory ensures a seat profile whose SOUL.md is a
|
|
36
|
+
* symlink to the packaged role soul, and prefixes argv with `flag <name>`.
|
|
37
|
+
* Used by hosts that have no per-session systemPrompt channel (hermes).
|
|
38
|
+
*/
|
|
39
|
+
seatProfileSoul?: SeatProfileSoul;
|
|
22
40
|
}>;
|
|
23
41
|
|
|
24
42
|
/** Absolute agent binary for one operator home. */
|
|
@@ -26,18 +44,35 @@ export function resolveAcpBinary(description: AcpHostDescription, operatorHome:
|
|
|
26
44
|
return join(operatorHome, ...description.binaryFromHome);
|
|
27
45
|
}
|
|
28
46
|
|
|
29
|
-
/** Stdio argv:
|
|
47
|
+
/** Stdio argv: optional profile flag, thinking flag (before the subcommand),
|
|
48
|
+
* prefix, optional model flag pair, suffix. */
|
|
30
49
|
export function acpStdioArgs(
|
|
31
50
|
description: AcpHostDescription,
|
|
32
51
|
model?: { readonly model?: string; readonly thinking?: string },
|
|
52
|
+
seat?: { readonly profileName?: string },
|
|
33
53
|
): string[] {
|
|
34
54
|
const { prefix, suffix, modelFlag, thinkingFlag } = description.argv;
|
|
35
55
|
const pair = (flag: string | undefined, value: string | undefined): string[] =>
|
|
36
56
|
flag === undefined || value === undefined ? [] : [flag, value];
|
|
37
57
|
return [
|
|
58
|
+
...pair(description.seatProfileSoul?.flag, seat?.profileName),
|
|
59
|
+
...pair(thinkingFlag, model?.thinking),
|
|
38
60
|
...prefix,
|
|
39
61
|
...pair(modelFlag, model?.model),
|
|
40
|
-
...pair(thinkingFlag, model?.thinking),
|
|
41
62
|
...suffix,
|
|
42
63
|
];
|
|
43
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The modelId the host addresses the seat model by.
|
|
68
|
+
* "argv" hosts address by bare model name; "set_model" hosts address by the
|
|
69
|
+
* `provider:model` modelId the ACP catalog exposes (seat provider after host
|
|
70
|
+
* alias projection, concatenated — never a package provider map).
|
|
71
|
+
*/
|
|
72
|
+
export function acpModelId(
|
|
73
|
+
modelPassing: AcpHostDescription["modelPassing"],
|
|
74
|
+
model?: { readonly model?: string; readonly provider?: string },
|
|
75
|
+
): string | undefined {
|
|
76
|
+
if (model?.model === undefined) return undefined;
|
|
77
|
+
return modelPassing === "set_model" ? `${model.provider}:${model.model}` : model.model;
|
|
78
|
+
}
|
|
@@ -27,6 +27,7 @@ import { loadGatekeeperSessionMaterials, loadMainRoleSessionMaterials } from "..
|
|
|
27
27
|
import { acpStdioArgs, resolveAcpBinary, type AcpHostDescription } from "./description.ts";
|
|
28
28
|
import { createComposedAcpRoleTurnHost } from "./role-envelope.ts";
|
|
29
29
|
import { connectAcpStdio } from "./role-turn-host.ts";
|
|
30
|
+
import { ensureSeatProfileSoul } from "./seat-profile-soul.ts";
|
|
30
31
|
import { createAcpSessionIdentityAuthority } from "./session-identity.ts";
|
|
31
32
|
|
|
32
33
|
export type ProductionAcpHostOptions = Readonly<{
|
|
@@ -121,11 +122,25 @@ export function createProductionAcpRoleTurnHost(options: ProductionAcpHostOption
|
|
|
121
122
|
return createComposedAcpRoleTurnHost({
|
|
122
123
|
sessionIdentity: createAcpSessionIdentityAuthority(principalAuthority, description.sessionBindingFile),
|
|
123
124
|
boundResume: description.boundResume,
|
|
125
|
+
modelPassing: description.modelPassing,
|
|
124
126
|
roleRuntimeDependencies: createAcpRoleRuntimeDependencies(packageRoot),
|
|
125
127
|
async connect(request) {
|
|
128
|
+
const seatProfile = description.seatProfileSoul;
|
|
129
|
+
const profileName = seatProfile === undefined
|
|
130
|
+
? undefined
|
|
131
|
+
: await ensureSeatProfileSoul({
|
|
132
|
+
spec: seatProfile,
|
|
133
|
+
operatorHome: request.home,
|
|
134
|
+
packageRoot,
|
|
135
|
+
role: request.activation.role,
|
|
136
|
+
});
|
|
126
137
|
return connectAcpStdio({
|
|
127
138
|
binary: resolveAcpBinary(description, request.home),
|
|
128
|
-
args: acpStdioArgs(
|
|
139
|
+
args: acpStdioArgs(
|
|
140
|
+
description,
|
|
141
|
+
request.model,
|
|
142
|
+
profileName === undefined ? undefined : { profileName },
|
|
143
|
+
),
|
|
129
144
|
cwd: request.cwd,
|
|
130
145
|
env,
|
|
131
146
|
});
|
|
@@ -622,7 +622,8 @@ export async function prepareAcpRoleEnvelope(options: {
|
|
|
622
622
|
};
|
|
623
623
|
|
|
624
624
|
// Shared envelope activation. systemPrompt must be ready before session/new
|
|
625
|
-
// (
|
|
625
|
+
// (delivered via _meta.systemPromptOverride where the host honors it), so
|
|
626
|
+
// activation runs during prepare.
|
|
626
627
|
try {
|
|
627
628
|
await emit("session_start", { reason: request.continuation.kind });
|
|
628
629
|
const inputResults = await emit("input", { text: request.continuation.prompt, source: "interactive" });
|
|
@@ -3,7 +3,7 @@ import { createInterface } from "node:readline";
|
|
|
3
3
|
|
|
4
4
|
import type { RoleTurnHost, RoleTurnKnownFailure, RoleTurnRequest, RoleTurnResult } from "../host-contracts.ts";
|
|
5
5
|
import { renderAgentStartMaterials } from "../agent-start-materials.ts";
|
|
6
|
-
import type
|
|
6
|
+
import { acpModelId, type AcpHostDescription } from "./description.ts";
|
|
7
7
|
|
|
8
8
|
/** ACP v1 surface used by the generic ACP adapter. Protocol details stay in this module. */
|
|
9
9
|
export interface AcpConnection {
|
|
@@ -64,6 +64,12 @@ export type AcpRoleTurnHostConfig = Readonly<{
|
|
|
64
64
|
sessionIdentity: AcpSessionIdentityAuthority;
|
|
65
65
|
/** Whether a bound resume reuses the native session or mints a fresh one. */
|
|
66
66
|
boundResume: AcpHostDescription["boundResume"];
|
|
67
|
+
/**
|
|
68
|
+
* How the seat model reaches the agent: "set_model" sends an ACP
|
|
69
|
+
* `session/set_model` RPC with modelId `provider:model` once the session
|
|
70
|
+
* exists (new or loaded); "argv" leaves it to the connect argv (--model).
|
|
71
|
+
*/
|
|
72
|
+
modelPassing: AcpHostDescription["modelPassing"];
|
|
67
73
|
connect(request: RoleTurnRequest): Promise<AcpConnection>;
|
|
68
74
|
prepare(request: RoleTurnRequest): Promise<AcpPreparedTurn>;
|
|
69
75
|
}>;
|
|
@@ -201,6 +207,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
201
207
|
const execution = serial.then(async (): Promise<RoleTurnResult> => {
|
|
202
208
|
const continuation = request.continuation;
|
|
203
209
|
const prepared = await config.prepare(request);
|
|
210
|
+
const systemPromptOverride = renderAcpSystemPromptOverride(prepared.systemPrompt);
|
|
204
211
|
let connection: AcpConnection | undefined;
|
|
205
212
|
let sessionId: string | undefined;
|
|
206
213
|
let accepted = false;
|
|
@@ -220,7 +227,8 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
220
227
|
const modelState = initializeMeta?.modelState;
|
|
221
228
|
const availableModels = Array.isArray(modelState?.availableModels) ? modelState.availableModels : undefined;
|
|
222
229
|
if (request.model !== undefined && availableModels !== undefined && !availableModels.some((entry) =>
|
|
223
|
-
typeof entry === "object" && entry !== null
|
|
230
|
+
typeof entry === "object" && entry !== null
|
|
231
|
+
&& (entry as { modelId?: unknown }).modelId === acpModelId(config.modelPassing, request.model))) {
|
|
224
232
|
return failure("activation", "AcpHostModelMismatch", "host-model-mismatch", {
|
|
225
233
|
provider: request.model.provider,
|
|
226
234
|
model: request.model.model,
|
|
@@ -233,32 +241,33 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
233
241
|
continuation.kind === "resume"
|
|
234
242
|
? request.hostTransition?.priorNativePaths
|
|
235
243
|
: undefined;
|
|
244
|
+
const loadConnection = connection;
|
|
245
|
+
// Shared session/new + session/load body (cwd/mcpServers/_meta).
|
|
246
|
+
const sessionBindParams = {
|
|
247
|
+
cwd: request.cwd,
|
|
248
|
+
mcpServers: prepared.mcpServers,
|
|
249
|
+
_meta: { systemPromptOverride, yoloMode: false },
|
|
250
|
+
};
|
|
251
|
+
const loadSession = async (bindSessionId: string): Promise<string> => {
|
|
252
|
+
const loaded = await loadConnection.request("session/load", {
|
|
253
|
+
sessionId: bindSessionId,
|
|
254
|
+
...sessionBindParams,
|
|
255
|
+
});
|
|
256
|
+
return typeof loaded.sessionId === "string" && loaded.sessionId !== ""
|
|
257
|
+
? loaded.sessionId
|
|
258
|
+
: bindSessionId;
|
|
259
|
+
};
|
|
236
260
|
if (continuation.kind === "resume" && config.boundResume === "session/load") {
|
|
237
261
|
// Same-host resume reuses the native ACP session via session/load.
|
|
238
262
|
const boundSessionId = await config.sessionIdentity.load(request.principal);
|
|
239
263
|
if (boundSessionId !== undefined && boundSessionId !== "") {
|
|
240
|
-
|
|
241
|
-
sessionId: boundSessionId,
|
|
242
|
-
cwd: request.cwd,
|
|
243
|
-
mcpServers: prepared.mcpServers,
|
|
244
|
-
_meta: { systemPromptOverride: renderAcpSystemPromptOverride(prepared.systemPrompt), yoloMode: false },
|
|
245
|
-
});
|
|
246
|
-
sessionId = typeof loaded.sessionId === "string" && loaded.sessionId !== ""
|
|
247
|
-
? loaded.sessionId
|
|
248
|
-
: boundSessionId;
|
|
264
|
+
sessionId = await loadSession(boundSessionId);
|
|
249
265
|
}
|
|
250
266
|
}
|
|
251
267
|
if (sessionId === undefined) {
|
|
252
268
|
// Initial run, unbound resume (cross-host / lost binding), or a host
|
|
253
269
|
// whose bound resume is session/new: mint the session and bind it.
|
|
254
|
-
const session = await connection.request(
|
|
255
|
-
"session/new",
|
|
256
|
-
{
|
|
257
|
-
cwd: request.cwd,
|
|
258
|
-
mcpServers: prepared.mcpServers,
|
|
259
|
-
_meta: { systemPromptOverride: renderAcpSystemPromptOverride(prepared.systemPrompt), yoloMode: false },
|
|
260
|
-
},
|
|
261
|
-
);
|
|
270
|
+
const session = await connection.request("session/new", sessionBindParams);
|
|
262
271
|
sessionId = typeof session.sessionId === "string" ? session.sessionId : undefined;
|
|
263
272
|
if (sessionId === undefined || sessionId === "") {
|
|
264
273
|
return failure("session", "AcpSessionFailure", "session-id-missing");
|
|
@@ -266,6 +275,24 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
266
275
|
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
267
276
|
}
|
|
268
277
|
|
|
278
|
+
// set_model hosts address the seat model by `provider:model` once the
|
|
279
|
+
// session exists; argv hosts never reach this RPC. Provider is the seat
|
|
280
|
+
// table value after owner host-alias projection (#778) — concatenated
|
|
281
|
+
// here, never dropped, never remapped in package code. set_model may
|
|
282
|
+
// rebuild the session agent and drop ACP-injected mcpServers, so re-bind
|
|
283
|
+
// via the same loadSession authority (return value is the live session id).
|
|
284
|
+
if (
|
|
285
|
+
config.modelPassing === "set_model"
|
|
286
|
+
&& request.model !== undefined
|
|
287
|
+
&& sessionId !== undefined
|
|
288
|
+
) {
|
|
289
|
+
await connection.request("session/set_model", {
|
|
290
|
+
sessionId,
|
|
291
|
+
modelId: acpModelId(config.modelPassing, request.model),
|
|
292
|
+
});
|
|
293
|
+
sessionId = await loadSession(sessionId);
|
|
294
|
+
}
|
|
295
|
+
|
|
269
296
|
let prompt =
|
|
270
297
|
priorNativePaths !== undefined && priorNativePaths.length > 0
|
|
271
298
|
? `${prepared.prompt}\n${priorNativePaths.join("\n")}`
|
|
@@ -319,12 +346,9 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
319
346
|
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
320
347
|
let result: Readonly<Record<string, unknown>>;
|
|
321
348
|
try {
|
|
322
|
-
const promptParts: Array<Record<string, unknown>> = [
|
|
323
|
-
{ type: "text", text: prompt },
|
|
324
|
-
];
|
|
325
349
|
result = await promptOrAbort({
|
|
326
350
|
sessionId,
|
|
327
|
-
prompt:
|
|
351
|
+
prompt: [{ type: "text", text: prompt }],
|
|
328
352
|
});
|
|
329
353
|
} catch (error) {
|
|
330
354
|
// Envelope abort (typed infra declaration): closeRound owns the failure record.
|