@akagilnc/pi-workflow-roles 0.1.3722 → 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 +157 -50
- package/dist/acp-host/seat-profile-soul.js +78 -0
- package/dist/host-descriptions.js +26 -0
- package/dist/public-cli/main.js +26 -0
- 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/host-descriptions.ts +26 -0
|
@@ -1,21 +1,35 @@
|
|
|
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
|
/** Absolute agent binary for one operator home. */
|
|
8
9
|
export function resolveAcpBinary(description, operatorHome) {
|
|
9
10
|
return join(operatorHome, ...description.binaryFromHome);
|
|
10
11
|
}
|
|
11
|
-
/** Stdio argv:
|
|
12
|
-
|
|
12
|
+
/** Stdio argv: optional profile flag, thinking flag (before the subcommand),
|
|
13
|
+
* prefix, optional model flag pair, suffix. */
|
|
14
|
+
export function acpStdioArgs(description, model, seat) {
|
|
13
15
|
const { prefix, suffix, modelFlag, thinkingFlag } = description.argv;
|
|
14
16
|
const pair = (flag, value) => flag === undefined || value === undefined ? [] : [flag, value];
|
|
15
17
|
return [
|
|
18
|
+
...pair(description.seatProfileSoul?.flag, seat?.profileName),
|
|
19
|
+
...pair(thinkingFlag, model?.thinking),
|
|
16
20
|
...prefix,
|
|
17
21
|
...pair(modelFlag, model?.model),
|
|
18
|
-
...pair(thinkingFlag, model?.thinking),
|
|
19
22
|
...suffix,
|
|
20
23
|
];
|
|
21
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The modelId the host addresses the seat model by.
|
|
27
|
+
* "argv" hosts address by bare model name; "set_model" hosts address by the
|
|
28
|
+
* `provider:model` modelId the ACP catalog exposes (seat provider after host
|
|
29
|
+
* alias projection, concatenated — never a package provider map).
|
|
30
|
+
*/
|
|
31
|
+
export function acpModelId(modelPassing, model) {
|
|
32
|
+
if (model?.model === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
return modelPassing === "set_model" ? `${model.provider}:${model.model}` : model.model;
|
|
35
|
+
}
|
|
@@ -164,7 +164,7 @@ function normalizeReviewComment(raw) {
|
|
|
164
164
|
function createGhApiRunner(options = {}) {
|
|
165
165
|
const spawnImpl = options.spawnImpl ?? spawn;
|
|
166
166
|
return async (args, runOptions = {}) => {
|
|
167
|
-
return await new Promise((
|
|
167
|
+
return await new Promise((resolve20, reject) => {
|
|
168
168
|
const signal = runOptions.signal;
|
|
169
169
|
if (signal?.aborted) {
|
|
170
170
|
reject(signal.reason ?? new Error("aborted"));
|
|
@@ -237,11 +237,11 @@ function createGhApiRunner(options = {}) {
|
|
|
237
237
|
const value = line2.slice(idx + 1).trim();
|
|
238
238
|
headers[name] = value;
|
|
239
239
|
}
|
|
240
|
-
|
|
240
|
+
resolve20({ status, headers, bodyText });
|
|
241
241
|
return;
|
|
242
242
|
}
|
|
243
243
|
if (code === 0) {
|
|
244
|
-
|
|
244
|
+
resolve20({ status: 200, headers: {}, bodyText: stdout });
|
|
245
245
|
return;
|
|
246
246
|
}
|
|
247
247
|
const failure2 = new Error(
|
|
@@ -1227,7 +1227,7 @@ async function spawnEngineDetourOnce(input) {
|
|
|
1227
1227
|
}
|
|
1228
1228
|
const command = input.argv[0];
|
|
1229
1229
|
const args = input.argv.slice(1);
|
|
1230
|
-
return await new Promise((
|
|
1230
|
+
return await new Promise((resolve20, reject) => {
|
|
1231
1231
|
let settled = false;
|
|
1232
1232
|
const signal = input.signal;
|
|
1233
1233
|
const child = spawn2(command, args, {
|
|
@@ -1257,7 +1257,7 @@ async function spawnEngineDetourOnce(input) {
|
|
|
1257
1257
|
if (signal !== void 0) {
|
|
1258
1258
|
signal.removeEventListener("abort", onAbort);
|
|
1259
1259
|
}
|
|
1260
|
-
|
|
1260
|
+
resolve20(result);
|
|
1261
1261
|
};
|
|
1262
1262
|
const onAbort = () => {
|
|
1263
1263
|
fail5(signal !== void 0 ? abortReasonError(signal) : new Error("aborted"));
|
|
@@ -8398,6 +8398,7 @@ var init_host_descriptions = __esm({
|
|
|
8398
8398
|
suffix: Object.freeze(["stdio"]),
|
|
8399
8399
|
modelFlag: "--model"
|
|
8400
8400
|
}),
|
|
8401
|
+
modelPassing: "argv",
|
|
8401
8402
|
boundResume: "session/load",
|
|
8402
8403
|
sessionBindingFile: "grok-acp-session.json",
|
|
8403
8404
|
childEnv: Object.freeze({
|
|
@@ -8405,6 +8406,31 @@ var init_host_descriptions = __esm({
|
|
|
8405
8406
|
GROK_MEMORY: "0",
|
|
8406
8407
|
GROK_SUBAGENTS: "0"
|
|
8407
8408
|
})
|
|
8409
|
+
}),
|
|
8410
|
+
/**
|
|
8411
|
+
* Operator home `~/.hermes`, native session/load resume, `acp` subcommand.
|
|
8412
|
+
* Model arrives as an ACP `session/set_model` RPC with modelId `provider:model`
|
|
8413
|
+
* (seat table provider + model concatenated). Reasoning is the global
|
|
8414
|
+
* `--reasoning` flag before `acp`. Soul is the seat profile SOUL.md symlink
|
|
8415
|
+
* (`hermes -p ak-<role> …`); package `souls/<role>.md` is the sole source.
|
|
8416
|
+
*/
|
|
8417
|
+
"hermes": Object.freeze({
|
|
8418
|
+
binaryFromHome: Object.freeze([".local", "bin", "hermes"]),
|
|
8419
|
+
argv: Object.freeze({
|
|
8420
|
+
prefix: Object.freeze(["acp"]),
|
|
8421
|
+
suffix: Object.freeze([]),
|
|
8422
|
+
thinkingFlag: "--reasoning"
|
|
8423
|
+
}),
|
|
8424
|
+
modelPassing: "set_model",
|
|
8425
|
+
boundResume: "session/load",
|
|
8426
|
+
sessionBindingFile: "hermes-acp-session.json",
|
|
8427
|
+
childEnv: Object.freeze({}),
|
|
8428
|
+
seatProfileSoul: Object.freeze({
|
|
8429
|
+
flag: "-p",
|
|
8430
|
+
namePrefix: "ak-",
|
|
8431
|
+
profilesRootFromHome: Object.freeze([".hermes", "profiles"]),
|
|
8432
|
+
soulFileName: "SOUL.md"
|
|
8433
|
+
})
|
|
8408
8434
|
})
|
|
8409
8435
|
});
|
|
8410
8436
|
}
|
|
@@ -11451,12 +11477,12 @@ function createSystemCollectorClock() {
|
|
|
11451
11477
|
return {
|
|
11452
11478
|
wallNow: () => /* @__PURE__ */ new Date(),
|
|
11453
11479
|
monoNow: () => Number(process.hrtime.bigint() - start) / 1e6,
|
|
11454
|
-
sleep: (ms, signal) => new Promise((
|
|
11480
|
+
sleep: (ms, signal) => new Promise((resolve20, reject) => {
|
|
11455
11481
|
if (signal?.aborted) {
|
|
11456
11482
|
reject(signal.reason ?? new Error("aborted"));
|
|
11457
11483
|
return;
|
|
11458
11484
|
}
|
|
11459
|
-
const timer = setTimeout(
|
|
11485
|
+
const timer = setTimeout(resolve20, ms);
|
|
11460
11486
|
const onAbort = () => {
|
|
11461
11487
|
clearTimeout(timer);
|
|
11462
11488
|
reject(signal?.reason ?? new Error("aborted"));
|
|
@@ -14877,10 +14903,10 @@ function presentFailureTerminal(terminal, io) {
|
|
|
14877
14903
|
}
|
|
14878
14904
|
function defaultNavigatorGraceSleep() {
|
|
14879
14905
|
let timer;
|
|
14880
|
-
const sleep = ((ms) => new Promise((
|
|
14906
|
+
const sleep = ((ms) => new Promise((resolve20) => {
|
|
14881
14907
|
timer = setTimeout(() => {
|
|
14882
14908
|
timer = void 0;
|
|
14883
|
-
|
|
14909
|
+
resolve20();
|
|
14884
14910
|
}, ms);
|
|
14885
14911
|
}));
|
|
14886
14912
|
sleep.cancel = () => {
|
|
@@ -14892,7 +14918,7 @@ function defaultNavigatorGraceSleep() {
|
|
|
14892
14918
|
return sleep;
|
|
14893
14919
|
}
|
|
14894
14920
|
function raceNavigatorGrace(work, graceMs = NAVIGATOR_POST_ROLE_GRACE_MS, sleep = defaultNavigatorGraceSleep()) {
|
|
14895
|
-
return new Promise((
|
|
14921
|
+
return new Promise((resolve20, reject) => {
|
|
14896
14922
|
let settled = false;
|
|
14897
14923
|
const finish = (action) => {
|
|
14898
14924
|
if (settled) return;
|
|
@@ -14901,11 +14927,11 @@ function raceNavigatorGrace(work, graceMs = NAVIGATOR_POST_ROLE_GRACE_MS, sleep
|
|
|
14901
14927
|
action();
|
|
14902
14928
|
};
|
|
14903
14929
|
void work.then(
|
|
14904
|
-
(value) => finish(() =>
|
|
14930
|
+
(value) => finish(() => resolve20({ status: "done", value })),
|
|
14905
14931
|
(error) => finish(() => reject(error))
|
|
14906
14932
|
);
|
|
14907
14933
|
void sleep(graceMs).then(() => {
|
|
14908
|
-
finish(() =>
|
|
14934
|
+
finish(() => resolve20({ status: "timeout" }));
|
|
14909
14935
|
});
|
|
14910
14936
|
});
|
|
14911
14937
|
}
|
|
@@ -24155,7 +24181,7 @@ import { mkdtemp as mkdtemp3, rm as rm3 } from "node:fs/promises";
|
|
|
24155
24181
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
24156
24182
|
import { join as join32 } from "node:path";
|
|
24157
24183
|
async function runCommand(command, args, options = {}) {
|
|
24158
|
-
return await new Promise((
|
|
24184
|
+
return await new Promise((resolve20, reject) => {
|
|
24159
24185
|
const child = spawn4(command, args, { ...options.cwd === void 0 ? {} : { cwd: options.cwd }, stdio: ["ignore", "pipe", "pipe"], signal: options.signal });
|
|
24160
24186
|
let stdout = "", stderr = "";
|
|
24161
24187
|
child.stdout.setEncoding("utf8").on("data", (chunk) => {
|
|
@@ -24175,7 +24201,7 @@ async function runCommand(command, args, options = {}) {
|
|
|
24175
24201
|
const actual = code ?? 1;
|
|
24176
24202
|
if ((options.allowedCodes ?? [0]).includes(actual)) {
|
|
24177
24203
|
settled = true;
|
|
24178
|
-
|
|
24204
|
+
resolve20({ stdout, stderr, code: actual });
|
|
24179
24205
|
} else fail5(void 0, code, signal);
|
|
24180
24206
|
});
|
|
24181
24207
|
});
|
|
@@ -24639,16 +24665,21 @@ import { join as join33 } from "node:path";
|
|
|
24639
24665
|
function resolveAcpBinary(description, operatorHome) {
|
|
24640
24666
|
return join33(operatorHome, ...description.binaryFromHome);
|
|
24641
24667
|
}
|
|
24642
|
-
function acpStdioArgs(description, model) {
|
|
24668
|
+
function acpStdioArgs(description, model, seat) {
|
|
24643
24669
|
const { prefix, suffix, modelFlag, thinkingFlag } = description.argv;
|
|
24644
24670
|
const pair = (flag, value) => flag === void 0 || value === void 0 ? [] : [flag, value];
|
|
24645
24671
|
return [
|
|
24672
|
+
...pair(description.seatProfileSoul?.flag, seat?.profileName),
|
|
24673
|
+
...pair(thinkingFlag, model?.thinking),
|
|
24646
24674
|
...prefix,
|
|
24647
24675
|
...pair(modelFlag, model?.model),
|
|
24648
|
-
...pair(thinkingFlag, model?.thinking),
|
|
24649
24676
|
...suffix
|
|
24650
24677
|
];
|
|
24651
24678
|
}
|
|
24679
|
+
function acpModelId(modelPassing, model) {
|
|
24680
|
+
if (model?.model === void 0) return void 0;
|
|
24681
|
+
return modelPassing === "set_model" ? `${model.provider}:${model.model}` : model.model;
|
|
24682
|
+
}
|
|
24652
24683
|
|
|
24653
24684
|
// src/acp-host/role-envelope.ts
|
|
24654
24685
|
init_gatekeeper_pass_envelope();
|
|
@@ -24750,8 +24781,8 @@ function connectAcpStdio(options) {
|
|
|
24750
24781
|
request(method, params) {
|
|
24751
24782
|
if (closed) return Promise.reject(terminalError ?? acpError("acp-connection-closed", "ACP connection is closed"));
|
|
24752
24783
|
const id = ++nextId;
|
|
24753
|
-
return new Promise((
|
|
24754
|
-
pending.set(id, { resolve:
|
|
24784
|
+
return new Promise((resolve20, reject) => {
|
|
24785
|
+
pending.set(id, { resolve: resolve20, reject });
|
|
24755
24786
|
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
|
|
24756
24787
|
`, (error) => {
|
|
24757
24788
|
if (error === null || error === void 0) return;
|
|
@@ -24775,7 +24806,7 @@ function connectAcpStdio(options) {
|
|
|
24775
24806
|
settleClosed(acpError("acp-connection-closed", "ACP connection is closed"));
|
|
24776
24807
|
child.stdin.end();
|
|
24777
24808
|
child.kill("SIGTERM");
|
|
24778
|
-
await new Promise((
|
|
24809
|
+
await new Promise((resolve20) => child.once("close", () => resolve20()));
|
|
24779
24810
|
}
|
|
24780
24811
|
});
|
|
24781
24812
|
}
|
|
@@ -24786,6 +24817,7 @@ function createAcpRoleTurnHost(config) {
|
|
|
24786
24817
|
const execution = serial.then(async () => {
|
|
24787
24818
|
const continuation = request.continuation;
|
|
24788
24819
|
const prepared = await config.prepare(request);
|
|
24820
|
+
const systemPromptOverride = renderAcpSystemPromptOverride(prepared.systemPrompt);
|
|
24789
24821
|
let connection;
|
|
24790
24822
|
let sessionId;
|
|
24791
24823
|
let accepted = false;
|
|
@@ -24801,40 +24833,47 @@ function createAcpRoleTurnHost(config) {
|
|
|
24801
24833
|
const initializeMeta = initialized._meta;
|
|
24802
24834
|
const modelState = initializeMeta?.modelState;
|
|
24803
24835
|
const availableModels = Array.isArray(modelState?.availableModels) ? modelState.availableModels : void 0;
|
|
24804
|
-
if (request.model !== void 0 && availableModels !== void 0 && !availableModels.some((entry) => typeof entry === "object" && entry !== null && entry.modelId === request.model
|
|
24836
|
+
if (request.model !== void 0 && availableModels !== void 0 && !availableModels.some((entry) => typeof entry === "object" && entry !== null && entry.modelId === acpModelId(config.modelPassing, request.model))) {
|
|
24805
24837
|
return failure("activation", "AcpHostModelMismatch", "host-model-mismatch", {
|
|
24806
24838
|
provider: request.model.provider,
|
|
24807
24839
|
model: request.model.model
|
|
24808
24840
|
});
|
|
24809
24841
|
}
|
|
24810
24842
|
const priorNativePaths = continuation.kind === "resume" ? request.hostTransition?.priorNativePaths : void 0;
|
|
24843
|
+
const loadConnection = connection;
|
|
24844
|
+
const sessionBindParams = {
|
|
24845
|
+
cwd: request.cwd,
|
|
24846
|
+
mcpServers: prepared.mcpServers,
|
|
24847
|
+
_meta: { systemPromptOverride, yoloMode: false }
|
|
24848
|
+
};
|
|
24849
|
+
const loadSession = async (bindSessionId) => {
|
|
24850
|
+
const loaded = await loadConnection.request("session/load", {
|
|
24851
|
+
sessionId: bindSessionId,
|
|
24852
|
+
...sessionBindParams
|
|
24853
|
+
});
|
|
24854
|
+
return typeof loaded.sessionId === "string" && loaded.sessionId !== "" ? loaded.sessionId : bindSessionId;
|
|
24855
|
+
};
|
|
24811
24856
|
if (continuation.kind === "resume" && config.boundResume === "session/load") {
|
|
24812
24857
|
const boundSessionId = await config.sessionIdentity.load(request.principal);
|
|
24813
24858
|
if (boundSessionId !== void 0 && boundSessionId !== "") {
|
|
24814
|
-
|
|
24815
|
-
sessionId: boundSessionId,
|
|
24816
|
-
cwd: request.cwd,
|
|
24817
|
-
mcpServers: prepared.mcpServers,
|
|
24818
|
-
_meta: { systemPromptOverride: renderAcpSystemPromptOverride(prepared.systemPrompt), yoloMode: false }
|
|
24819
|
-
});
|
|
24820
|
-
sessionId = typeof loaded.sessionId === "string" && loaded.sessionId !== "" ? loaded.sessionId : boundSessionId;
|
|
24859
|
+
sessionId = await loadSession(boundSessionId);
|
|
24821
24860
|
}
|
|
24822
24861
|
}
|
|
24823
24862
|
if (sessionId === void 0) {
|
|
24824
|
-
const session = await connection.request(
|
|
24825
|
-
"session/new",
|
|
24826
|
-
{
|
|
24827
|
-
cwd: request.cwd,
|
|
24828
|
-
mcpServers: prepared.mcpServers,
|
|
24829
|
-
_meta: { systemPromptOverride: renderAcpSystemPromptOverride(prepared.systemPrompt), yoloMode: false }
|
|
24830
|
-
}
|
|
24831
|
-
);
|
|
24863
|
+
const session = await connection.request("session/new", sessionBindParams);
|
|
24832
24864
|
sessionId = typeof session.sessionId === "string" ? session.sessionId : void 0;
|
|
24833
24865
|
if (sessionId === void 0 || sessionId === "") {
|
|
24834
24866
|
return failure("session", "AcpSessionFailure", "session-id-missing");
|
|
24835
24867
|
}
|
|
24836
24868
|
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
24837
24869
|
}
|
|
24870
|
+
if (config.modelPassing === "set_model" && request.model !== void 0 && sessionId !== void 0) {
|
|
24871
|
+
await connection.request("session/set_model", {
|
|
24872
|
+
sessionId,
|
|
24873
|
+
modelId: acpModelId(config.modelPassing, request.model)
|
|
24874
|
+
});
|
|
24875
|
+
sessionId = await loadSession(sessionId);
|
|
24876
|
+
}
|
|
24838
24877
|
let prompt = priorNativePaths !== void 0 && priorNativePaths.length > 0 ? `${prepared.prompt}
|
|
24839
24878
|
${priorNativePaths.join("\n")}` : prepared.prompt;
|
|
24840
24879
|
const abortSignal = request.signal === void 0 ? prepared.abortSignal : prepared.abortSignal === void 0 ? request.signal : AbortSignal.any([prepared.abortSignal, request.signal]);
|
|
@@ -24845,7 +24884,7 @@ ${priorNativePaths.join("\n")}` : prepared.prompt;
|
|
|
24845
24884
|
}
|
|
24846
24885
|
const promptRequest = activeConnection.request("session/prompt", params);
|
|
24847
24886
|
if (abortSignal === void 0) return promptRequest;
|
|
24848
|
-
return new Promise((
|
|
24887
|
+
return new Promise((resolve20, reject) => {
|
|
24849
24888
|
let settled = false;
|
|
24850
24889
|
const onAbort = () => {
|
|
24851
24890
|
if (settled) return;
|
|
@@ -24860,7 +24899,7 @@ ${priorNativePaths.join("\n")}` : prepared.prompt;
|
|
|
24860
24899
|
if (settled) return;
|
|
24861
24900
|
settled = true;
|
|
24862
24901
|
abortSignal.removeEventListener("abort", onAbort);
|
|
24863
|
-
|
|
24902
|
+
resolve20(value);
|
|
24864
24903
|
},
|
|
24865
24904
|
(error) => {
|
|
24866
24905
|
if (settled) return;
|
|
@@ -24874,12 +24913,9 @@ ${priorNativePaths.join("\n")}` : prepared.prompt;
|
|
|
24874
24913
|
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
24875
24914
|
let result;
|
|
24876
24915
|
try {
|
|
24877
|
-
const promptParts = [
|
|
24878
|
-
{ type: "text", text: prompt }
|
|
24879
|
-
];
|
|
24880
24916
|
result = await promptOrAbort({
|
|
24881
24917
|
sessionId,
|
|
24882
|
-
prompt:
|
|
24918
|
+
prompt: [{ type: "text", text: prompt }]
|
|
24883
24919
|
});
|
|
24884
24920
|
} catch (error) {
|
|
24885
24921
|
if (typeof error === "object" && error !== null && error.code === "host-aborted") {
|
|
@@ -24984,11 +25020,11 @@ function projectAcpActivationFlags(request) {
|
|
|
24984
25020
|
return flags;
|
|
24985
25021
|
}
|
|
24986
25022
|
async function listen(server, path) {
|
|
24987
|
-
await new Promise((
|
|
25023
|
+
await new Promise((resolve20, reject) => {
|
|
24988
25024
|
server.once("error", reject);
|
|
24989
25025
|
server.listen(path, () => {
|
|
24990
25026
|
server.off("error", reject);
|
|
24991
|
-
|
|
25027
|
+
resolve20();
|
|
24992
25028
|
});
|
|
24993
25029
|
});
|
|
24994
25030
|
}
|
|
@@ -25379,8 +25415,8 @@ async function prepareAcpRoleEnvelope(options) {
|
|
|
25379
25415
|
try {
|
|
25380
25416
|
const closeAll = server.closeAllConnections;
|
|
25381
25417
|
if (typeof closeAll === "function") closeAll.call(server);
|
|
25382
|
-
await new Promise((
|
|
25383
|
-
server.close((error) => error ? reject(error) :
|
|
25418
|
+
await new Promise((resolve20, reject) => {
|
|
25419
|
+
server.close((error) => error ? reject(error) : resolve20());
|
|
25384
25420
|
});
|
|
25385
25421
|
} catch (error) {
|
|
25386
25422
|
cleanupFailures.push(error);
|
|
@@ -25488,11 +25524,70 @@ async function prepareAcpRoleEnvelope(options) {
|
|
|
25488
25524
|
}
|
|
25489
25525
|
}
|
|
25490
25526
|
|
|
25527
|
+
// src/acp-host/seat-profile-soul.ts
|
|
25528
|
+
import { constants as constants3 } from "node:fs";
|
|
25529
|
+
import { access as access4, copyFile, lstat as lstat6, mkdir as mkdir6, readlink, symlink, unlink as unlink4 } from "node:fs/promises";
|
|
25530
|
+
import { dirname as dirname18, join as join35, relative as relative3, resolve as resolve19 } from "node:path";
|
|
25531
|
+
function seatProfileName(spec, role) {
|
|
25532
|
+
return `${spec.namePrefix}${role}`;
|
|
25533
|
+
}
|
|
25534
|
+
function packageRoleSoulPath(packageRoot, role) {
|
|
25535
|
+
return join35(packageRoot, "souls", `${role}.md`);
|
|
25536
|
+
}
|
|
25537
|
+
async function pathExists(path) {
|
|
25538
|
+
try {
|
|
25539
|
+
await access4(path, constants3.F_OK);
|
|
25540
|
+
return true;
|
|
25541
|
+
} catch {
|
|
25542
|
+
return false;
|
|
25543
|
+
}
|
|
25544
|
+
}
|
|
25545
|
+
async function ensureSeatProfileSoul(options) {
|
|
25546
|
+
const { spec, operatorHome, packageRoot, role } = options;
|
|
25547
|
+
const profileName = seatProfileName(spec, role);
|
|
25548
|
+
const soulTarget = resolve19(packageRoleSoulPath(packageRoot, role));
|
|
25549
|
+
if (!await pathExists(soulTarget)) {
|
|
25550
|
+
throw new Error(`packaged role soul missing: ${soulTarget}`);
|
|
25551
|
+
}
|
|
25552
|
+
const profilesRoot = join35(operatorHome, ...spec.profilesRootFromHome);
|
|
25553
|
+
const profileDir = join35(profilesRoot, profileName);
|
|
25554
|
+
const hostRoot = dirname18(profilesRoot);
|
|
25555
|
+
const soulPath = join35(profileDir, spec.soulFileName);
|
|
25556
|
+
if (!await pathExists(profileDir)) {
|
|
25557
|
+
await mkdir6(profileDir, { recursive: true });
|
|
25558
|
+
for (const name of ["auth.json", ".env", "config.yaml"]) {
|
|
25559
|
+
const source = join35(hostRoot, name);
|
|
25560
|
+
if (!await pathExists(source)) continue;
|
|
25561
|
+
await copyFile(source, join35(profileDir, name));
|
|
25562
|
+
}
|
|
25563
|
+
} else {
|
|
25564
|
+
await mkdir6(profileDir, { recursive: true });
|
|
25565
|
+
}
|
|
25566
|
+
const desiredLink = relative3(profileDir, soulTarget);
|
|
25567
|
+
let current;
|
|
25568
|
+
try {
|
|
25569
|
+
const st = await lstat6(soulPath);
|
|
25570
|
+
if (st.isSymbolicLink()) {
|
|
25571
|
+
current = await readlink(soulPath);
|
|
25572
|
+
}
|
|
25573
|
+
} catch {
|
|
25574
|
+
current = void 0;
|
|
25575
|
+
}
|
|
25576
|
+
if (current === desiredLink || current === soulTarget) {
|
|
25577
|
+
return profileName;
|
|
25578
|
+
}
|
|
25579
|
+
if (await pathExists(soulPath) || current !== void 0) {
|
|
25580
|
+
await unlink4(soulPath);
|
|
25581
|
+
}
|
|
25582
|
+
await symlink(desiredLink, soulPath);
|
|
25583
|
+
return profileName;
|
|
25584
|
+
}
|
|
25585
|
+
|
|
25491
25586
|
// src/acp-host/session-identity.ts
|
|
25492
|
-
import { mkdir as
|
|
25493
|
-
import { dirname as
|
|
25587
|
+
import { mkdir as mkdir7, readFile as readFile19, rename, writeFile as writeFile9 } from "node:fs/promises";
|
|
25588
|
+
import { dirname as dirname19, join as join36 } from "node:path";
|
|
25494
25589
|
function createAcpSessionIdentityAuthority(authority, sessionBindingFile) {
|
|
25495
|
-
const bindingPath = (principal) =>
|
|
25590
|
+
const bindingPath = (principal) => join36(authority.decode(principal).sessionDirectory, sessionBindingFile);
|
|
25496
25591
|
return {
|
|
25497
25592
|
resolveSessionFile(principal) {
|
|
25498
25593
|
return authority.decode(principal).sessionFile;
|
|
@@ -25511,7 +25606,7 @@ function createAcpSessionIdentityAuthority(authority, sessionBindingFile) {
|
|
|
25511
25606
|
},
|
|
25512
25607
|
async bind(principal, sessionId) {
|
|
25513
25608
|
const target = bindingPath(principal);
|
|
25514
|
-
await
|
|
25609
|
+
await mkdir7(dirname19(target), { recursive: true });
|
|
25515
25610
|
const temporary = `${target}.${process.pid}.tmp`;
|
|
25516
25611
|
await writeFile9(temporary, `${JSON.stringify({ sessionId })}
|
|
25517
25612
|
`, { encoding: "utf8", mode: 384 });
|
|
@@ -25598,11 +25693,23 @@ function createProductionAcpRoleTurnHost(options) {
|
|
|
25598
25693
|
return createComposedAcpRoleTurnHost({
|
|
25599
25694
|
sessionIdentity: createAcpSessionIdentityAuthority(principalAuthority, description.sessionBindingFile),
|
|
25600
25695
|
boundResume: description.boundResume,
|
|
25696
|
+
modelPassing: description.modelPassing,
|
|
25601
25697
|
roleRuntimeDependencies: createAcpRoleRuntimeDependencies(packageRoot),
|
|
25602
25698
|
async connect(request) {
|
|
25699
|
+
const seatProfile = description.seatProfileSoul;
|
|
25700
|
+
const profileName = seatProfile === void 0 ? void 0 : await ensureSeatProfileSoul({
|
|
25701
|
+
spec: seatProfile,
|
|
25702
|
+
operatorHome: request.home,
|
|
25703
|
+
packageRoot,
|
|
25704
|
+
role: request.activation.role
|
|
25705
|
+
});
|
|
25603
25706
|
return connectAcpStdio({
|
|
25604
25707
|
binary: resolveAcpBinary(description, request.home),
|
|
25605
|
-
args: acpStdioArgs(
|
|
25708
|
+
args: acpStdioArgs(
|
|
25709
|
+
description,
|
|
25710
|
+
request.model,
|
|
25711
|
+
profileName === void 0 ? void 0 : { profileName }
|
|
25712
|
+
),
|
|
25606
25713
|
cwd: request.cwd,
|
|
25607
25714
|
env
|
|
25608
25715
|
});
|
|
@@ -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
|
+
}
|
|
@@ -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;
|
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
|
}
|
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.
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
|
|
12
|
+
export type SeatProfileSoul = Readonly<{
|
|
13
|
+
/** Argv flag selecting the profile (hermes pre-argparse `-p`). */
|
|
14
|
+
flag: string;
|
|
15
|
+
/** Profile id = `${namePrefix}${role}` (e.g. `ak-judge`). */
|
|
16
|
+
namePrefix: string;
|
|
17
|
+
/** Profile directory root relative to the operator home (`.hermes/profiles`). */
|
|
18
|
+
profilesRootFromHome: readonly string[];
|
|
19
|
+
/** Soul filename inside the profile directory. */
|
|
20
|
+
soulFileName: string;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
/** Profile id for one seat role. */
|
|
24
|
+
export function seatProfileName(spec: SeatProfileSoul, role: string): string {
|
|
25
|
+
return `${spec.namePrefix}${role}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Packaged soul path for one role (`souls/<role>.md`). */
|
|
29
|
+
export function packageRoleSoulPath(packageRoot: string, role: string): string {
|
|
30
|
+
return join(packageRoot, "souls", `${role}.md`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
34
|
+
try {
|
|
35
|
+
await access(path, constants.F_OK);
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Ensure the seat profile directory exists and SOUL.md is a symlink to the
|
|
44
|
+
* packaged role soul. On first create, copy credential surfaces from the host
|
|
45
|
+
* root (parent of the profiles root) so the profile can authenticate without
|
|
46
|
+
* touching the default profile in place. Returns the profile id for argv.
|
|
47
|
+
*/
|
|
48
|
+
export async function ensureSeatProfileSoul(options: {
|
|
49
|
+
readonly spec: SeatProfileSoul;
|
|
50
|
+
readonly operatorHome: string;
|
|
51
|
+
readonly packageRoot: string;
|
|
52
|
+
readonly role: string;
|
|
53
|
+
}): Promise<string> {
|
|
54
|
+
const { spec, operatorHome, packageRoot, role } = options;
|
|
55
|
+
const profileName = seatProfileName(spec, role);
|
|
56
|
+
const soulTarget = resolve(packageRoleSoulPath(packageRoot, role));
|
|
57
|
+
if (!(await pathExists(soulTarget))) {
|
|
58
|
+
throw new Error(`packaged role soul missing: ${soulTarget}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const profilesRoot = join(operatorHome, ...spec.profilesRootFromHome);
|
|
62
|
+
const profileDir = join(profilesRoot, profileName);
|
|
63
|
+
const hostRoot = dirname(profilesRoot);
|
|
64
|
+
const soulPath = join(profileDir, spec.soulFileName);
|
|
65
|
+
|
|
66
|
+
if (!(await pathExists(profileDir))) {
|
|
67
|
+
await mkdir(profileDir, { recursive: true });
|
|
68
|
+
// First-create credential bootstrap only. Never rewrite an existing profile's
|
|
69
|
+
// auth/config; never write into the host root / default profile.
|
|
70
|
+
for (const name of ["auth.json", ".env", "config.yaml"] as const) {
|
|
71
|
+
const source = join(hostRoot, name);
|
|
72
|
+
if (!(await pathExists(source))) continue;
|
|
73
|
+
await copyFile(source, join(profileDir, name));
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
await mkdir(profileDir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const desiredLink = relative(profileDir, soulTarget);
|
|
80
|
+
let current: string | undefined;
|
|
81
|
+
try {
|
|
82
|
+
const st = await lstat(soulPath);
|
|
83
|
+
if (st.isSymbolicLink()) {
|
|
84
|
+
current = await readlink(soulPath);
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
current = undefined;
|
|
88
|
+
}
|
|
89
|
+
if (current === desiredLink || current === soulTarget) {
|
|
90
|
+
return profileName;
|
|
91
|
+
}
|
|
92
|
+
if (await pathExists(soulPath) || current !== undefined) {
|
|
93
|
+
await unlink(soulPath);
|
|
94
|
+
}
|
|
95
|
+
await symlink(desiredLink, soulPath);
|
|
96
|
+
return profileName;
|
|
97
|
+
}
|
package/src/host-descriptions.ts
CHANGED
|
@@ -24,6 +24,7 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
|
|
|
24
24
|
suffix: Object.freeze(["stdio"]),
|
|
25
25
|
modelFlag: "--model",
|
|
26
26
|
}),
|
|
27
|
+
modelPassing: "argv",
|
|
27
28
|
boundResume: "session/load",
|
|
28
29
|
sessionBindingFile: "grok-acp-session.json",
|
|
29
30
|
childEnv: Object.freeze({
|
|
@@ -32,6 +33,31 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
|
|
|
32
33
|
GROK_SUBAGENTS: "0",
|
|
33
34
|
}),
|
|
34
35
|
}),
|
|
36
|
+
/**
|
|
37
|
+
* Operator home `~/.hermes`, native session/load resume, `acp` subcommand.
|
|
38
|
+
* Model arrives as an ACP `session/set_model` RPC with modelId `provider:model`
|
|
39
|
+
* (seat table provider + model concatenated). Reasoning is the global
|
|
40
|
+
* `--reasoning` flag before `acp`. Soul is the seat profile SOUL.md symlink
|
|
41
|
+
* (`hermes -p ak-<role> …`); package `souls/<role>.md` is the sole source.
|
|
42
|
+
*/
|
|
43
|
+
"hermes": Object.freeze({
|
|
44
|
+
binaryFromHome: Object.freeze([".local", "bin", "hermes"]),
|
|
45
|
+
argv: Object.freeze({
|
|
46
|
+
prefix: Object.freeze(["acp"]),
|
|
47
|
+
suffix: Object.freeze([]),
|
|
48
|
+
thinkingFlag: "--reasoning",
|
|
49
|
+
}),
|
|
50
|
+
modelPassing: "set_model",
|
|
51
|
+
boundResume: "session/load",
|
|
52
|
+
sessionBindingFile: "hermes-acp-session.json",
|
|
53
|
+
childEnv: Object.freeze({}),
|
|
54
|
+
seatProfileSoul: Object.freeze({
|
|
55
|
+
flag: "-p",
|
|
56
|
+
namePrefix: "ak-",
|
|
57
|
+
profilesRootFromHome: Object.freeze([".hermes", "profiles"]),
|
|
58
|
+
soulFileName: "SOUL.md",
|
|
59
|
+
}),
|
|
60
|
+
}),
|
|
35
61
|
});
|
|
36
62
|
|
|
37
63
|
export function lookupHostDescription(host: string): AcpHostDescription | undefined {
|