@akagilnc/pi-workflow-roles 0.1.3771 → 0.1.3789
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/acp-host/production-host.js +793 -393
- package/dist/atomic-write.js +23 -0
- package/dist/collector-handbook.js +142 -0
- package/dist/collector-ledger.js +39 -47
- package/dist/collector-role.js +103 -14
- package/dist/collector-tool-schemas.js +25 -1
- package/dist/headless-host/description.js +77 -0
- package/dist/headless-host/mcp-relay.mjs +119 -0
- package/dist/headless-host/production-host.js +26841 -0
- package/dist/host-descriptions.js +44 -3
- package/dist/public-cli/load-production-external-host.js +19 -0
- package/dist/public-cli/load-production-headless-host.js +36 -0
- package/dist/public-cli/main.js +244 -234
- package/dist/public-cli/option-definitions.js +2 -2
- package/dist/public-role-summons.js +40 -5
- package/dist/role-runtime.js +3 -0
- package/extensions/role-runtime.ts +2 -0
- package/package.json +1 -1
- package/resources/collector-bot-handbook.md +33 -0
- package/scripts/build-package.mjs +28 -0
- package/src/acp-host/production-host.ts +4 -0
- package/src/acp-host/role-envelope.ts +141 -84
- package/src/acp-host/role-turn-host.ts +12 -0
- package/src/collector-handbook.ts +194 -0
- package/src/collector-ledger.ts +42 -62
- package/src/collector-role.ts +117 -11
- package/src/collector-tool-schemas.ts +25 -1
- package/src/headless-host/description.ts +123 -0
- package/src/headless-host/production-host.ts +75 -0
- package/src/headless-host/role-turn-host.ts +424 -0
- package/src/host-descriptions.ts +53 -6
- package/src/public-cli/cli.ts +2 -2
- package/src/public-cli/load-production-external-host.ts +29 -0
- package/src/public-cli/load-production-headless-host.ts +48 -0
- package/src/public-cli/main.ts +5 -0
- package/src/public-cli/option-definitions.ts +2 -2
- package/src/public-role-summons.ts +62 -9
- package/src/role-runtime.ts +5 -0
|
@@ -483,8 +483,8 @@ const COLLECTOR_OPTIONS = [
|
|
|
483
483
|
repeatable: false,
|
|
484
484
|
form: "option",
|
|
485
485
|
description: {
|
|
486
|
-
en: "Optional request manifest JSON path ({requests:[{id,body}]}).",
|
|
487
|
-
zh: "可选 request manifest JSON 路径({requests:[{id,body}]}
|
|
486
|
+
en: "Optional request manifest JSON path ({requests:[{id,body}]}). Role may also decide request bodies from handbook/field activity without this file.",
|
|
487
|
+
zh: "可选 request manifest JSON 路径({requests:[{id,body}]})。角色亦可依手册/现场自行判定请求正文,不必依赖本文件。",
|
|
488
488
|
},
|
|
489
489
|
},
|
|
490
490
|
];
|
|
@@ -99,9 +99,9 @@ async function createSummonEnv(options) {
|
|
|
99
99
|
const hostName = options.seat.host ?? "pi";
|
|
100
100
|
let roleTurnHost = piHost;
|
|
101
101
|
if (hostName !== "pi") {
|
|
102
|
-
const {
|
|
103
|
-
const {
|
|
104
|
-
if (
|
|
102
|
+
const { lookupHostFamily } = await import("./host-descriptions.js");
|
|
103
|
+
const { loadProductionExternalHostFactory } = await import("./public-cli/load-production-external-host.js");
|
|
104
|
+
if (lookupHostFamily(hostName) === void 0) {
|
|
105
105
|
throw new Error(
|
|
106
106
|
`public role summons host unregistered: host=${hostName} seat=${options.role}`
|
|
107
107
|
);
|
|
@@ -109,7 +109,7 @@ async function createSummonEnv(options) {
|
|
|
109
109
|
let hostPromise;
|
|
110
110
|
roleTurnHost = {
|
|
111
111
|
executeTurn: async (request) => {
|
|
112
|
-
hostPromise ??=
|
|
112
|
+
hostPromise ??= loadProductionExternalHostFactory(options.packageRoot, hostName).then(
|
|
113
113
|
(create) => create({
|
|
114
114
|
packageRoot: options.packageRoot,
|
|
115
115
|
principalAuthority
|
|
@@ -151,7 +151,8 @@ async function summonPublicRole(options) {
|
|
|
151
151
|
} = await import("./public-cli/config.js");
|
|
152
152
|
const credentials = options.credentials ?? await loadCredentialProviders(agentDir);
|
|
153
153
|
const config = await loadPublicCliConfig(home);
|
|
154
|
-
const
|
|
154
|
+
const invocation = options.host === void 0 ? void 0 : { host: options.host };
|
|
155
|
+
const seat = resolveEffectiveSeat(config, options.role, credentials, invocation);
|
|
155
156
|
const env = {
|
|
156
157
|
...await createSummonEnv({
|
|
157
158
|
role: options.role,
|
|
@@ -263,6 +264,38 @@ async function summonPublicRole(options) {
|
|
|
263
264
|
...stderr === void 0 || stderr === "" ? {} : { stderr }
|
|
264
265
|
};
|
|
265
266
|
}
|
|
267
|
+
async function parentInvocationHost(sourceRunDirectory) {
|
|
268
|
+
const { readFile } = await import("node:fs/promises");
|
|
269
|
+
const path = join(sourceRunDirectory, "invocation.json");
|
|
270
|
+
let text;
|
|
271
|
+
try {
|
|
272
|
+
text = await readFile(path, "utf8");
|
|
273
|
+
} catch (error) {
|
|
274
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
275
|
+
throw new Error(
|
|
276
|
+
`parent invocation.json required for gate source run at ${path}: ${code ?? (error instanceof Error ? error.message : String(error))}`,
|
|
277
|
+
{ cause: error }
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
let raw;
|
|
281
|
+
try {
|
|
282
|
+
raw = JSON.parse(text);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`parent invocation.json unreadable at ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
286
|
+
{ cause: error }
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
290
|
+
throw new Error(`parent invocation.json has non-object shape at ${path}`);
|
|
291
|
+
}
|
|
292
|
+
const host = raw.host;
|
|
293
|
+
if (host === void 0) return void 0;
|
|
294
|
+
if (typeof host !== "string" || host.trim() === "") {
|
|
295
|
+
throw new Error(`parent invocation.json host must be a non-empty string at ${path}`);
|
|
296
|
+
}
|
|
297
|
+
return host;
|
|
298
|
+
}
|
|
266
299
|
async function summonGateOfficer(options) {
|
|
267
300
|
let home = options.home;
|
|
268
301
|
if (home === void 0) {
|
|
@@ -276,6 +309,7 @@ async function summonGateOfficer(options) {
|
|
|
276
309
|
submission: options.submission
|
|
277
310
|
});
|
|
278
311
|
}
|
|
312
|
+
const parentHost = await parentInvocationHost(options.sourceRunDirectory);
|
|
279
313
|
const common = {
|
|
280
314
|
cwd: options.cwd,
|
|
281
315
|
...home === void 0 ? {} : { home },
|
|
@@ -284,6 +318,7 @@ async function summonGateOfficer(options) {
|
|
|
284
318
|
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
285
319
|
...options.reask === void 0 ? {} : { reviewReask: options.reask },
|
|
286
320
|
...gateReviewInstruction === void 0 ? {} : { gateReviewInstruction },
|
|
321
|
+
...parentHost === void 0 ? {} : { host: parentHost },
|
|
287
322
|
...options.roleTurnHost === void 0 ? {} : { roleTurnHost: options.roleTurnHost },
|
|
288
323
|
...options.createRunId === void 0 ? {} : { createRunId: options.createRunId }
|
|
289
324
|
};
|
package/dist/role-runtime.js
CHANGED
|
@@ -1223,6 +1223,9 @@ export function createRoleRuntimeExtension(dependencies) {
|
|
|
1223
1223
|
...(dependencies.createCollectorClock === undefined
|
|
1224
1224
|
? {}
|
|
1225
1225
|
: { createClock: dependencies.createCollectorClock }),
|
|
1226
|
+
...(dependencies.loadCollectorHandbookSeed === undefined
|
|
1227
|
+
? {}
|
|
1228
|
+
: { loadHandbookSeed: dependencies.loadCollectorHandbookSeed }),
|
|
1226
1229
|
}, hostActions);
|
|
1227
1230
|
// #676 J1: business tools + tool_call gate register only behind admission/activation.
|
|
1228
1231
|
// before_agent_start / tool_result collector branches live on the shared envelope hooks above.
|
|
@@ -40,6 +40,7 @@ import { loadGatekeeperSessionMaterials, loadMainRoleSessionMaterials } from "..
|
|
|
40
40
|
const extensionPath = fileURLToPath(import.meta.url);
|
|
41
41
|
const packageRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
42
42
|
const navigatorRoutePlaybookPath = fileURLToPath(new URL("../resources/navigator-route-playbook.md", import.meta.url));
|
|
43
|
+
const collectorHandbookSeedPath = fileURLToPath(new URL("../resources/collector-bot-handbook.md", import.meta.url));
|
|
43
44
|
// #675: nested public summons (gate/auditor) resolve root via env under jiti.
|
|
44
45
|
if (process.env.AK_ROLE_PACKAGE_ROOT === undefined || process.env.AK_ROLE_PACKAGE_ROOT.trim() === "") {
|
|
45
46
|
process.env.AK_ROLE_PACKAGE_ROOT = packageRoot;
|
|
@@ -122,6 +123,7 @@ export default function roleRuntime(pi: ExtensionAPI): void {
|
|
|
122
123
|
createReviewerPinnedGitReader: () => createReviewerPinnedGitReader(),
|
|
123
124
|
createReviewerIssueFetcher: () => createGhIssueSoftFetcher(),
|
|
124
125
|
loadCollectorSoul: () => loadMainRoleSessionMaterials("collector"),
|
|
126
|
+
loadCollectorHandbookSeed: () => readFile(collectorHandbookSeedPath, "utf8"),
|
|
125
127
|
createCollectorTransport: () => createGhCollectorGitHubTransport(),
|
|
126
128
|
loadDoctorSoul: () => loadMainRoleSessionMaterials("doctor"),
|
|
127
129
|
loadDoctorCase,
|
package/package.json
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# 通进司 bot 工作记忆(通用)
|
|
2
|
+
|
|
3
|
+
薄手册:触发方式、自动/手动条件、完成与不可继续的识别线索、官方出处。
|
|
4
|
+
内容是角色阅读与修正的工作记忆,不是代码状态规则;临时额度/故障不得固化为永久行为。
|
|
5
|
+
参与者依本仓使用与现场活动辨认,不把下列名称当作全仓固定名单。
|
|
6
|
+
|
|
7
|
+
## Codex(hosted GitHub connector)
|
|
8
|
+
|
|
9
|
+
- 官方:https://learn.chatgpt.com/docs/third-party/github
|
|
10
|
+
- 常见主动请求:`@codex review`
|
|
11
|
+
- 新建 PR 常有自动评审;是否已在审当前提交以现场 reviews/comments 为准
|
|
12
|
+
|
|
13
|
+
## CodeRabbit
|
|
14
|
+
|
|
15
|
+
- 官方:https://docs.coderabbit.ai/configuration/auto-review
|
|
16
|
+
- 命令:https://docs.coderabbit.ai/guides/commands
|
|
17
|
+
- 常见主动请求:`@coderabbitai review`;全量:`@coderabbitai full review`
|
|
18
|
+
- 默认常为增量;本仓实际自动/暂停以现场与仓库差异条为准
|
|
19
|
+
|
|
20
|
+
## Sourcery
|
|
21
|
+
|
|
22
|
+
- 官方:https://docs.sourcery.ai/reviews/ 与 https://docs.sourcery.ai/reviews/commands/
|
|
23
|
+
- 新建/更新与主动请求依文档与本仓现场
|
|
24
|
+
|
|
25
|
+
## Cursor Bugbot / 自动化
|
|
26
|
+
|
|
27
|
+
- 官方:https://prod.cursor.com/help/ai-features/bugbot 与 automations 文档
|
|
28
|
+
- 内置 Bugbot 与同账号自定义任务须区分;某任务额度耗尽不推断其他任务失效
|
|
29
|
+
|
|
30
|
+
## 修正规则
|
|
31
|
+
|
|
32
|
+
- 优先引用官方文档;现场差异记入仓库差异条并附 PR/证据指针
|
|
33
|
+
- 未知或暂未响应 ≠ 通过或确定死亡
|
|
@@ -114,6 +114,33 @@ export async function buildAcpProductionHost(
|
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Deferred generic headless production host artifact (#645). Same peer-free
|
|
119
|
+
* selection face as ACP: loaded only when a headless description-table row is
|
|
120
|
+
* selected.
|
|
121
|
+
*/
|
|
122
|
+
export async function buildHeadlessProductionHost(
|
|
123
|
+
outfile = "dist/headless-host/production-host.js",
|
|
124
|
+
) {
|
|
125
|
+
await mkdir(dirname(outfile), { recursive: true });
|
|
126
|
+
await build({
|
|
127
|
+
entryPoints: ["src/headless-host/production-host.ts"],
|
|
128
|
+
outfile,
|
|
129
|
+
format: "esm",
|
|
130
|
+
platform: "node",
|
|
131
|
+
target: "node20",
|
|
132
|
+
bundle: true,
|
|
133
|
+
packages: "external",
|
|
134
|
+
logLevel: "silent",
|
|
135
|
+
});
|
|
136
|
+
// Shared envelope resolves ./mcp-relay.mjs from import.meta.url of the bundle
|
|
137
|
+
// (#645 headless reuses the ACP relay for AK tools under --mcp-config).
|
|
138
|
+
await copyFile(
|
|
139
|
+
resolve("src/acp-host/mcp-relay.mjs"),
|
|
140
|
+
join(dirname(outfile), "mcp-relay.mjs"),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
117
144
|
export async function buildPackageArtifacts() {
|
|
118
145
|
await build({
|
|
119
146
|
entryPoints: entries.map((name) => `src/${name}.ts`),
|
|
@@ -139,6 +166,7 @@ export async function buildPackageArtifacts() {
|
|
|
139
166
|
}
|
|
140
167
|
await buildPublicAkRoleBin();
|
|
141
168
|
await buildAcpProductionHost();
|
|
169
|
+
await buildHeadlessProductionHost();
|
|
142
170
|
}
|
|
143
171
|
|
|
144
172
|
const isMain =
|
|
@@ -39,6 +39,9 @@ export type ProductionAcpHostOptions = Readonly<{
|
|
|
39
39
|
const navigatorRoutePlaybookPath = fileURLToPath(
|
|
40
40
|
new URL("../../resources/navigator-route-playbook.md", import.meta.url),
|
|
41
41
|
);
|
|
42
|
+
const collectorHandbookSeedPath = fileURLToPath(
|
|
43
|
+
new URL("../../resources/collector-bot-handbook.md", import.meta.url),
|
|
44
|
+
);
|
|
42
45
|
|
|
43
46
|
/** Host-neutral packaged role runtime deps for the ACP parent-process envelope. */
|
|
44
47
|
export function createAcpRoleRuntimeDependencies(packageRoot: string): RoleRuntimeDependencies {
|
|
@@ -55,6 +58,7 @@ export function createAcpRoleRuntimeDependencies(packageRoot: string): RoleRunti
|
|
|
55
58
|
createReviewerPinnedGitReader: () => createReviewerPinnedGitReader(),
|
|
56
59
|
createReviewerIssueFetcher: () => createGhIssueSoftFetcher(),
|
|
57
60
|
loadCollectorSoul: () => loadMainRoleSessionMaterials("collector"),
|
|
61
|
+
loadCollectorHandbookSeed: () => readFile(collectorHandbookSeedPath, "utf8"),
|
|
58
62
|
createCollectorTransport: () => createGhCollectorGitHubTransport(),
|
|
59
63
|
loadDoctorSoul: () => loadMainRoleSessionMaterials("doctor"),
|
|
60
64
|
loadDoctorCase,
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
RoleTurnKnownFailure,
|
|
16
16
|
RoleTurnRequest,
|
|
17
17
|
} from "../host-contracts.ts";
|
|
18
|
-
import { packagedRoleInputFlag, packagedRolePhaseFlag } from "../packaged-role-registry.ts";
|
|
18
|
+
import { packagedRoleInputFlag, packagedRoleOutputTool, packagedRolePhaseFlag } from "../packaged-role-registry.ts";
|
|
19
19
|
import { stripSkillFrontmatter } from "../package-resources/method-skill.ts";
|
|
20
20
|
import {
|
|
21
21
|
createRoleRuntimeExtension,
|
|
@@ -131,10 +131,33 @@ export function createComposedAcpRoleTurnHost(
|
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/** JSON Schema draft-07 document for host-native `--json-schema` (headless). */
|
|
135
|
+
export function terminatingToolJsonSchema(parameters: unknown): Readonly<Record<string, unknown>> {
|
|
136
|
+
const cloned = JSON.parse(JSON.stringify(parameters)) as Record<string, unknown>;
|
|
137
|
+
// $schema last so a newer declaration on the tool parameters cannot override draft-07.
|
|
138
|
+
return Object.freeze({
|
|
139
|
+
...cloned,
|
|
140
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
134
144
|
export async function prepareAcpRoleEnvelope(options: {
|
|
135
145
|
readonly request: RoleTurnRequest;
|
|
136
146
|
readonly dependencies: RoleRuntimeDependencies;
|
|
147
|
+
/**
|
|
148
|
+
* MCP unix socket path for the protocol-relay child. Always required — AK tools
|
|
149
|
+
* ride this single MCP path; headless structured_output reuses the same
|
|
150
|
+
* terminating-tool ledger path without listing the terminating tool on MCP.
|
|
151
|
+
*/
|
|
137
152
|
readonly socketPath: string;
|
|
153
|
+
/**
|
|
154
|
+
* Whether MCP `tools/list` advertises the role terminating tool.
|
|
155
|
+
* ACP keeps it listed (session-tool receipt). Headless hides it so the host
|
|
156
|
+
* native `--json-schema` / structured_output is the sole schema channel
|
|
157
|
+
* (#750 submission-tool-is-schema-channel) and empty MCP probes cannot
|
|
158
|
+
* pre-empt a later structured_output.
|
|
159
|
+
*/
|
|
160
|
+
readonly listTerminatingToolOnMcp?: boolean;
|
|
138
161
|
/**
|
|
139
162
|
* Durable principal session path (header layout only).
|
|
140
163
|
* Production passes DurablePrincipalAuthority.decode(principal).sessionFile so
|
|
@@ -143,6 +166,14 @@ export async function prepareAcpRoleEnvelope(options: {
|
|
|
143
166
|
readonly sessionFile?: string;
|
|
144
167
|
}): Promise<AcpPreparedTurn> {
|
|
145
168
|
const { request } = options;
|
|
169
|
+
if (options.socketPath === "") {
|
|
170
|
+
throw new Error("prepareAcpRoleEnvelope requires socketPath");
|
|
171
|
+
}
|
|
172
|
+
const listTerminatingToolOnMcp = options.listTerminatingToolOnMcp !== false;
|
|
173
|
+
const earlyTerminatingTool = packagedRoleOutputTool(request.activation.role);
|
|
174
|
+
if (earlyTerminatingTool === undefined) {
|
|
175
|
+
throw new Error(`role has no terminating tool: ${request.activation.role}`);
|
|
176
|
+
}
|
|
146
177
|
const flags = projectAcpActivationFlags(request);
|
|
147
178
|
const tools = new Map<string, HostToolDefinition>();
|
|
148
179
|
const handlers = new Map<string, Handler[]>();
|
|
@@ -403,6 +434,87 @@ export async function prepareAcpRoleEnvelope(options: {
|
|
|
403
434
|
await emit("tool_execution_end", { toolCallId, toolName, isError: projected.isError });
|
|
404
435
|
return projected;
|
|
405
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* Shared terminating/support tool path for MCP relay and headless structured_output.
|
|
439
|
+
* Books the call, runs execute, projects tool_result; does not emit turn_end (closeRound).
|
|
440
|
+
*/
|
|
441
|
+
async function invokeAkTool(name: string, args: unknown): Promise<{
|
|
442
|
+
content: ContentPart[];
|
|
443
|
+
isError: boolean;
|
|
444
|
+
blocked?: true;
|
|
445
|
+
}> {
|
|
446
|
+
const tool = tools.get(name);
|
|
447
|
+
if (tool === undefined) throw new Error(`Unknown AK tool: ${name}`);
|
|
448
|
+
const toolCallId = randomUUID();
|
|
449
|
+
calls.push({ toolCallId, toolName: name });
|
|
450
|
+
// First-record-then-audit: book the tool-call leaf in memory before execute so
|
|
451
|
+
// judge/doctor subject gates see the candidate on parent session books.
|
|
452
|
+
sessionEntries.push({
|
|
453
|
+
type: "message",
|
|
454
|
+
message: {
|
|
455
|
+
role: "assistant" as const,
|
|
456
|
+
content: [{
|
|
457
|
+
type: "toolCall",
|
|
458
|
+
id: toolCallId,
|
|
459
|
+
name,
|
|
460
|
+
arguments: args ?? {},
|
|
461
|
+
}],
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
try {
|
|
465
|
+
await emit("tool_execution_start", { toolCallId, toolName: name });
|
|
466
|
+
const blocked = (await emit("tool_call", { toolCallId, toolName: name, input: (args ?? {}) as Record<string, unknown> }))
|
|
467
|
+
.some((value) => typeof value === "object" && value !== null && "block" in value && value.block === true);
|
|
468
|
+
if (blocked) {
|
|
469
|
+
// Lawful seatbelt/block stays bare rejection — not infrastructure (#593 r3).
|
|
470
|
+
return { content: [{ type: "text", text: `AK tool blocked: ${name}` }], isError: true, blocked: true };
|
|
471
|
+
}
|
|
472
|
+
} catch (error) {
|
|
473
|
+
// Pre-execution emit failure shares the non-correctable infra pathway (#593 r3).
|
|
474
|
+
const declared = declareRoundInfrastructureFailure(error);
|
|
475
|
+
try {
|
|
476
|
+
const projected = await projectToolResult(toolCallId, name, {
|
|
477
|
+
content: declared.content,
|
|
478
|
+
details: declared.details,
|
|
479
|
+
isError: true,
|
|
480
|
+
});
|
|
481
|
+
return { content: projected.content, isError: true };
|
|
482
|
+
} catch {
|
|
483
|
+
return { content: declared.content, isError: true };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const result = await tool.execute(toolCallId, (args ?? {}) as never, undefined, undefined, context);
|
|
488
|
+
const projected = await projectToolResult(toolCallId, name, {
|
|
489
|
+
content: result.content,
|
|
490
|
+
details: result.details,
|
|
491
|
+
isError: false,
|
|
492
|
+
});
|
|
493
|
+
// Candidate only: seal waits for closeRound after the host round boundary.
|
|
494
|
+
return { content: projected.content, isError: projected.isError };
|
|
495
|
+
} catch (error) {
|
|
496
|
+
let content: ContentPart[];
|
|
497
|
+
let details: Record<string, unknown>;
|
|
498
|
+
if (isCorrectableExecuteError(error)) {
|
|
499
|
+
const projected = projectCorrectableExecuteRejection(error);
|
|
500
|
+
content = [{ type: "text", text: projected.diagnostic }];
|
|
501
|
+
details = projected.details;
|
|
502
|
+
} else {
|
|
503
|
+
// Slot-before-abort; projectToolResult may still project durable details.
|
|
504
|
+
({ content, details } = declareRoundInfrastructureFailure(error));
|
|
505
|
+
}
|
|
506
|
+
// The shared envelope's tool_result handler is the sole classifier:
|
|
507
|
+
// it projects either the structured submission non-pass (correctable
|
|
508
|
+
// rejection) or the typed infrastructure fact onto the reply.
|
|
509
|
+
const projected = await projectToolResult(toolCallId, name, {
|
|
510
|
+
content,
|
|
511
|
+
details,
|
|
512
|
+
isError: true,
|
|
513
|
+
});
|
|
514
|
+
return { content: projected.content, isError: projected.isError };
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
406
518
|
function reply(socket: Socket, id: number, result?: unknown, error?: unknown): void {
|
|
407
519
|
const rpcError = error instanceof Error
|
|
408
520
|
? { code: "ak-relay-failure", name: error.name, message: error.message }
|
|
@@ -424,7 +536,9 @@ export async function prepareAcpRoleEnvelope(options: {
|
|
|
424
536
|
if (rpc.token !== token) { reply(socket, rpc.id, undefined, "unauthorized relay"); return; }
|
|
425
537
|
try {
|
|
426
538
|
if (rpc.method === "tools/list") {
|
|
427
|
-
|
|
539
|
+
const listed = [...tools.values()].filter((tool) =>
|
|
540
|
+
listTerminatingToolOnMcp || tool.name !== earlyTerminatingTool);
|
|
541
|
+
reply(socket, rpc.id, { tools: listed.map((tool) => {
|
|
428
542
|
return { name: tool.name, description: tool.description, inputSchema: tool.parameters };
|
|
429
543
|
}) });
|
|
430
544
|
return;
|
|
@@ -433,95 +547,24 @@ export async function prepareAcpRoleEnvelope(options: {
|
|
|
433
547
|
const params = rpc.params as ToolCallParams | undefined;
|
|
434
548
|
const name = params?.name;
|
|
435
549
|
if (typeof name !== "string") throw new Error("MCP tool name is missing");
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
// First-record-then-audit: book the tool-call leaf in memory before execute so
|
|
441
|
-
// judge/doctor subject gates see the candidate on parent session books.
|
|
442
|
-
{
|
|
443
|
-
const message = {
|
|
444
|
-
role: "assistant" as const,
|
|
445
|
-
content: [{
|
|
446
|
-
type: "toolCall",
|
|
447
|
-
id: toolCallId,
|
|
448
|
-
name,
|
|
449
|
-
arguments: params?.arguments ?? {},
|
|
450
|
-
}],
|
|
451
|
-
};
|
|
452
|
-
sessionEntries.push({ type: "message", message });
|
|
550
|
+
// Headless schema channel owns the terminating receipt — refuse MCP
|
|
551
|
+
// terminating calls so an empty probe cannot book a non-sealable candidate.
|
|
552
|
+
if (!listTerminatingToolOnMcp && name === earlyTerminatingTool) {
|
|
553
|
+
throw new Error(`terminating tool ${name} is schema-channel only on this host`);
|
|
453
554
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
.some((value) => typeof value === "object" && value !== null && "block" in value && value.block === true);
|
|
458
|
-
if (blocked) {
|
|
459
|
-
// Lawful seatbelt/block stays bare RPC rejection — not infrastructure (#593 r3).
|
|
460
|
-
reply(socket, rpc.id, undefined, new Error(`AK tool blocked: ${name}`));
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
} catch (error) {
|
|
464
|
-
// Pre-execution emit failure (e.g. observation writer) shares the same
|
|
465
|
-
// non-correctable infra pathway as execute throws (#593 r3).
|
|
466
|
-
const declared = declareRoundInfrastructureFailure(error);
|
|
467
|
-
try {
|
|
468
|
-
const projected = await projectToolResult(toolCallId, name, {
|
|
469
|
-
content: declared.content,
|
|
470
|
-
details: declared.details,
|
|
471
|
-
isError: true,
|
|
472
|
-
});
|
|
473
|
-
reply(socket, rpc.id, {
|
|
474
|
-
content: projected.content,
|
|
475
|
-
isError: true,
|
|
476
|
-
});
|
|
477
|
-
} catch {
|
|
478
|
-
// Slot already filled; durable projection may have partially failed.
|
|
479
|
-
reply(socket, rpc.id, {
|
|
480
|
-
content: declared.content,
|
|
481
|
-
isError: true,
|
|
482
|
-
});
|
|
483
|
-
}
|
|
555
|
+
const outcome = await invokeAkTool(name, params?.arguments ?? {});
|
|
556
|
+
if (outcome.blocked === true) {
|
|
557
|
+
reply(socket, rpc.id, undefined, new Error(outcome.content.map((p) => p.type === "text" ? p.text : "").join("")));
|
|
484
558
|
return;
|
|
485
559
|
}
|
|
486
|
-
|
|
487
|
-
const result = await tool.execute(toolCallId, (params?.arguments ?? {}) as never, undefined, undefined, context);
|
|
488
|
-
const projected = await projectToolResult(toolCallId, name, {
|
|
489
|
-
content: result.content,
|
|
490
|
-
details: result.details,
|
|
491
|
-
isError: false,
|
|
492
|
-
});
|
|
493
|
-
// Candidate only: do not emit turn_end here. Seal waits for the typed ACP
|
|
494
|
-
// round boundary (closeRound after session/prompt), so delayed siblings stay
|
|
495
|
-
// in the same round instead of becoming silent post-seal anomalies.
|
|
496
|
-
reply(socket, rpc.id, { content: projected.content, ...(projected.isError ? { isError: true } : {}) });
|
|
497
|
-
} catch (error) {
|
|
498
|
-
let content: ContentPart[];
|
|
499
|
-
let details: Record<string, unknown>;
|
|
500
|
-
if (isCorrectableExecuteError(error)) {
|
|
501
|
-
const projected = projectCorrectableExecuteRejection(error);
|
|
502
|
-
content = [{ type: "text", text: projected.diagnostic }];
|
|
503
|
-
details = projected.details;
|
|
504
|
-
} else {
|
|
505
|
-
// Slot-before-abort; projectToolResult may still project durable details.
|
|
506
|
-
({ content, details } = declareRoundInfrastructureFailure(error));
|
|
507
|
-
}
|
|
508
|
-
// The shared envelope's tool_result handler is the sole classifier:
|
|
509
|
-
// it projects either the structured submission non-pass (correctable
|
|
510
|
-
// rejection) or the typed infrastructure fact onto the reply.
|
|
511
|
-
const projected = await projectToolResult(toolCallId, name, {
|
|
512
|
-
content,
|
|
513
|
-
details,
|
|
514
|
-
isError: true,
|
|
515
|
-
});
|
|
516
|
-
reply(socket, rpc.id, { content: projected.content, ...(projected.isError ? { isError: true } : {}) });
|
|
517
|
-
}
|
|
560
|
+
reply(socket, rpc.id, { content: outcome.content, ...(outcome.isError ? { isError: true } : {}) });
|
|
518
561
|
} catch (error) { reply(socket, rpc.id, undefined, error); }
|
|
519
562
|
})();
|
|
520
563
|
}
|
|
521
564
|
});
|
|
522
565
|
}
|
|
523
|
-
await listen(server, options.socketPath);
|
|
524
566
|
const relay = fileURLToPath(new URL("./mcp-relay.mjs", import.meta.url));
|
|
567
|
+
await listen(server, options.socketPath);
|
|
525
568
|
let disposed = false;
|
|
526
569
|
// Tools execute in this process (relay is protocol-only). Mirror Pi's child-env
|
|
527
570
|
// AK_ROLE_RUN_DIR / AK_ROLE_COURT_ATTEMPT injection onto the parent so ledger
|
|
@@ -552,8 +595,6 @@ export async function prepareAcpRoleEnvelope(options: {
|
|
|
552
595
|
} catch (error) {
|
|
553
596
|
cleanupFailures.push(error);
|
|
554
597
|
}
|
|
555
|
-
// Server closure is unconditional: a shutdown-handler failure must not leave
|
|
556
|
-
// the listening MCP server keeping the completed invocation alive.
|
|
557
598
|
try {
|
|
558
599
|
const closeAll = (server as unknown as { closeAllConnections?: () => void }).closeAllConnections;
|
|
559
600
|
if (typeof closeAll === "function") closeAll.call(server);
|
|
@@ -571,6 +612,14 @@ export async function prepareAcpRoleEnvelope(options: {
|
|
|
571
612
|
}
|
|
572
613
|
};
|
|
573
614
|
|
|
615
|
+
const terminatingToolName: string = earlyTerminatingTool;
|
|
616
|
+
async function ingestStructuredOutput(params: unknown): Promise<void> {
|
|
617
|
+
// MCP path may already have invoked the terminating tool this round; skip the
|
|
618
|
+
// duplicate so structured_output + tool-call does not arm non-sole.
|
|
619
|
+
if (calls.some((call) => call.toolName === terminatingToolName)) return;
|
|
620
|
+
await invokeAkTool(terminatingToolName, params ?? {});
|
|
621
|
+
}
|
|
622
|
+
|
|
574
623
|
const closeRound: AcpPreparedTurn["closeRound"] = async () => {
|
|
575
624
|
// Typed round boundary: hand the complete call list to the shared ledger once.
|
|
576
625
|
if (calls.length > 0) {
|
|
@@ -654,6 +703,11 @@ export async function prepareAcpRoleEnvelope(options: {
|
|
|
654
703
|
else process.env.AK_ROLE_COURT_ATTEMPT = request.courtAttemptId;
|
|
655
704
|
runDirInjected = true;
|
|
656
705
|
|
|
706
|
+
const terminating = tools.get(terminatingToolName);
|
|
707
|
+
if (terminating === undefined) {
|
|
708
|
+
throw new Error(`terminating tool not registered after activation: ${terminatingToolName}`);
|
|
709
|
+
}
|
|
710
|
+
const jsonSchema = terminatingToolJsonSchema(terminating.parameters);
|
|
657
711
|
return {
|
|
658
712
|
mcpServers: [{
|
|
659
713
|
name: `ak-${request.activation.role}`,
|
|
@@ -669,6 +723,9 @@ export async function prepareAcpRoleEnvelope(options: {
|
|
|
669
723
|
abortSignal: hostAbort.signal,
|
|
670
724
|
closeRound,
|
|
671
725
|
dispose,
|
|
726
|
+
jsonSchema,
|
|
727
|
+
terminatingToolName,
|
|
728
|
+
ingestStructuredOutput,
|
|
672
729
|
};
|
|
673
730
|
} catch (error) {
|
|
674
731
|
// listen already succeeded; dispose is not yet caller-owned. Release the
|
|
@@ -43,6 +43,18 @@ export type AcpPreparedTurn = Readonly<{
|
|
|
43
43
|
| { readonly accepted: false; readonly failure: RoleTurnKnownFailure }
|
|
44
44
|
>;
|
|
45
45
|
dispose?(): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Headless CLI family (#645): role terminating-tool schema for host-native
|
|
48
|
+
* `--json-schema`. Present for every prepared turn; ACP ignores it.
|
|
49
|
+
*/
|
|
50
|
+
jsonSchema: Readonly<Record<string, unknown>>;
|
|
51
|
+
/** Terminating tool name whose schema is `jsonSchema`. */
|
|
52
|
+
terminatingToolName: string;
|
|
53
|
+
/**
|
|
54
|
+
* Headless CLI family: feed host-native `structured_output` through the same
|
|
55
|
+
* terminating-tool path the MCP relay uses (ledger + gates). ACP ignores it.
|
|
56
|
+
*/
|
|
57
|
+
ingestStructuredOutput(params: unknown): Promise<void>;
|
|
46
58
|
}>;
|
|
47
59
|
|
|
48
60
|
/** Fold structured system-prompt authority into the provider-visible ACP override. */
|