@akagilnc/pi-workflow-roles 0.1.2521 → 0.1.2653-next.71811ca
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 +129 -39
- package/README.zh-CN.md +128 -39
- package/dist/activation-ledger-topology.js +1 -1
- package/dist/archivist-record-entry.js +47 -26
- package/dist/auditor-dossier-tool.js +3 -1
- package/dist/compliance-transport.js +17 -26
- package/dist/doctor-contracts.js +1 -2
- package/dist/dossier-resolution.js +4 -8
- package/dist/engine-detour-tool.js +33 -8
- package/dist/engine-detour.js +1 -0
- package/dist/evidence-child-executor.js +277 -424
- package/dist/host-contracts.js +22 -0
- package/dist/institutional-resolution.js +88 -0
- package/dist/merger-contracts.js +1 -2
- package/dist/navigator-attendance.js +16 -0
- package/dist/notary-contracts.js +2 -3
- package/dist/package-contracts/fixer-output.js +1 -2
- package/dist/packaged-role-registry.js +102 -9
- package/dist/pi/in-process-session.js +671 -0
- package/dist/public-cli/config.js +7 -5
- package/dist/public-cli/main.js +3767 -4113
- package/dist/reviewer-child-executor.js +6 -1
- package/dist/session-opening-materials.js +10 -38
- package/dist/sitian-appender.js +146 -0
- package/dist/sitian-contracts.js +21 -0
- package/dist/sitian-facade.js +15 -0
- package/dist/sitian-reader.js +52 -0
- package/extensions/role-runtime.ts +9 -7
- package/package.json +5 -5
- package/resources/engines/cursor.md +12 -26
- package/resources/engines/hermes.md +2 -23
- package/resources/engines/opus.md +1 -16
- package/resources/navigator-route-playbook.md +2 -2
- package/scripts/build-package.mjs +0 -1
- package/souls/quality-law.md +1 -1
- package/src/activation-ledger-topology.ts +1 -1
- package/src/archivist-record-entry.ts +57 -45
- package/src/auditor-dossier-tool.ts +4 -2
- package/src/canonical-skill-binding.ts +37 -27
- package/src/collector-role.ts +11 -20
- package/src/collector-tool-schemas.ts +1 -4
- package/src/compliance-transport.ts +19 -44
- package/src/doctor-auditor.ts +1 -5
- package/src/doctor-contracts.ts +1 -4
- package/src/doctor-role.ts +31 -8
- package/src/dossier-resolution.ts +8 -8
- package/src/engine-detour-tool.ts +48 -19
- package/src/engine-detour.ts +3 -0
- package/src/evidence-child-executor.ts +277 -485
- package/src/factory-board.ts +3 -3
- package/src/gatekeeper-role.ts +21 -10
- package/src/host-contracts.ts +386 -0
- package/src/in-process-session.ts +1 -115
- package/src/institutional-resolution.ts +135 -0
- package/src/judge-auditor.ts +1 -5
- package/src/judge-role.ts +37 -51
- package/src/merger-contracts.ts +1 -4
- package/src/merger-role.ts +7 -11
- package/src/navigator-attendance.ts +20 -1
- package/src/notary-contracts.ts +8 -11
- package/src/notary-role.ts +7 -23
- package/src/notary-source-run.ts +4 -3
- package/src/package-contracts/fixer-output.ts +1 -4
- package/src/package-resources/method-skill-binding.ts +10 -34
- package/src/packaged-role-registry.ts +122 -12
- package/src/pi/adapter.ts +192 -0
- package/src/pi/durable-principal.ts +98 -0
- package/src/pi/in-process-session.ts +819 -0
- package/src/pi/known-failure.ts +77 -0
- package/src/pi/pi-normalization.ts +284 -0
- package/src/pi/role-turn-host.ts +459 -0
- package/src/public-cli/auto-resume.ts +42 -44
- package/src/public-cli/cli.ts +171 -257
- package/src/public-cli/coder-run.ts +115 -433
- package/src/public-cli/collector-run.ts +62 -314
- package/src/public-cli/config.ts +7 -5
- package/src/public-cli/doctor-run.ts +41 -52
- package/src/public-cli/fixer-run.ts +122 -427
- package/src/public-cli/invocation.ts +287 -96
- package/src/public-cli/judge-run.ts +92 -422
- package/src/public-cli/merger-run.ts +167 -429
- package/src/public-cli/notary-run.ts +41 -47
- package/src/public-cli/option-definitions.ts +136 -3
- package/src/public-cli/post-admission.ts +497 -0
- package/src/public-cli/public-run-credentials.ts +4 -4
- package/src/public-cli/reviewer-dispatch-rejection.ts +106 -0
- package/src/public-cli/reviewer-run.ts +132 -449
- package/src/public-cli/run-lifecycle.ts +339 -327
- package/src/public-cli/settlement.ts +251 -134
- package/src/public-cli/terminal.ts +3 -9
- package/src/public-cli/turn-request.ts +87 -0
- package/src/reviewer-child-executor.ts +9 -1
- package/src/reviewer-role.ts +11 -20
- package/src/role-runtime.ts +129 -78
- package/src/session-opening-materials.ts +25 -39
- package/src/sitian-appender.ts +171 -0
- package/src/sitian-contracts.ts +106 -0
- package/src/sitian-facade.ts +18 -0
- package/src/sitian-reader.ts +62 -0
- package/src/worker-role.ts +42 -40
- package/src/worker-submission-gates.ts +33 -0
- package/dist/archivist-role-run-coordinates.js +0 -28
- package/dist/package-contracts/terminating-infrastructure.js +0 -90
- package/resources/engines/sonnet.md +0 -27
- package/resources/engines/zcode.md +0 -63
- package/src/archivist-role-run-coordinates.ts +0 -45
- package/src/package-contracts/terminating-infrastructure.ts +0 -132
- package/src/public-cli/explicit-internal.ts +0 -427
- package/src/public-cli/one-shot-dispatch.ts +0 -300
|
@@ -4,15 +4,11 @@
|
|
|
4
4
|
* 「谁调了谁」复用 Pi parentSession + ADR 0047 correlation,不新增 caller 字段。
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
|
-
import { existsSync, realpathSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { dirname,
|
|
7
|
+
import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, resolve, join } from "node:path";
|
|
9
9
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
|
|
11
11
|
import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
|
|
12
|
-
export {
|
|
13
|
-
roleRunSessionCoordinates,
|
|
14
|
-
type RoleRunSessionCoordinates,
|
|
15
|
-
} from "./archivist-role-run-coordinates.ts";
|
|
16
12
|
import {
|
|
17
13
|
ActivationLedgerError,
|
|
18
14
|
activationBookDirectory,
|
|
@@ -23,12 +19,42 @@ import {
|
|
|
23
19
|
resolveActivationLedgerHome,
|
|
24
20
|
} from "./activation-ledger-topology.ts";
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
const CURRENT_SESSION_LEDGER = "current-session.json";
|
|
23
|
+
|
|
24
|
+
type CurrentSessionRecord = { readonly sessionFile: string };
|
|
25
|
+
|
|
26
|
+
function readCurrentSession(sessionDir: string): string {
|
|
27
|
+
const ledger = join(sessionDir, CURRENT_SESSION_LEDGER);
|
|
28
|
+
try {
|
|
29
|
+
const value: unknown = JSON.parse(readFileSync(ledger, "utf8"));
|
|
30
|
+
if (
|
|
31
|
+
typeof value !== "object"
|
|
32
|
+
|| value === null
|
|
33
|
+
|| typeof (value as CurrentSessionRecord).sessionFile !== "string"
|
|
34
|
+
|| (value as CurrentSessionRecord).sessionFile.length === 0
|
|
35
|
+
) {
|
|
36
|
+
throw new Error("sessionFile is missing");
|
|
37
|
+
}
|
|
38
|
+
return (value as CurrentSessionRecord).sessionFile;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new ActivationLedgerError(
|
|
41
|
+
`archivist current-session ledger is unavailable or invalid (${ledger}): ${errorText(error)}`,
|
|
42
|
+
{ cause: error },
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeCurrentSession(sessionDir: string, sessionFile: string): void {
|
|
48
|
+
const ledger = join(sessionDir, CURRENT_SESSION_LEDGER);
|
|
49
|
+
try {
|
|
50
|
+
writeFileSync(ledger, `${JSON.stringify({ sessionFile })}\n`, { flag: "wx" });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
throw new ActivationLedgerError(
|
|
53
|
+
`archivist current-session ledger cannot be created (${ledger}): ${errorText(error)}`,
|
|
54
|
+
{ cause: error },
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
32
58
|
|
|
33
59
|
/** Parent session surface needed to link and (when already under home) nest the record. */
|
|
34
60
|
export type RecordSessionParent = {
|
|
@@ -53,44 +79,28 @@ export const WORKER_SUBMISSION_GATE_KIND = "worker-submission-gate";
|
|
|
53
79
|
* Sole file-level placement lock for a resumed same-nest principal (ADR 0065 / #221).
|
|
54
80
|
* ensureRealDirectoryTree already owns the sessionDir chain; a final .jsonl symlink is
|
|
55
81
|
* invisible to that directory walk, so this runs once before SessionManager.open.
|
|
56
|
-
* Circle is the
|
|
57
|
-
*
|
|
82
|
+
* Circle is the authorized nest (sessionDir) itself — lexical path and realpath must both
|
|
83
|
+
* stay inside it. Same-book cross-nest pointers and cross-book symlinks are refused alike.
|
|
58
84
|
* realpath/stat failures stay typed ActivationLedgerError with original cause — never
|
|
59
85
|
* wash through physicalPathIdentity's non-ENOENT lexical fallback.
|
|
60
86
|
*/
|
|
61
|
-
function
|
|
62
|
-
ledgerHome: string,
|
|
87
|
+
function assertRecentFinalFileUnderSessionDir(
|
|
63
88
|
sessionDir: string,
|
|
64
89
|
recentFile: string,
|
|
65
90
|
): void {
|
|
66
|
-
const absoluteHome = resolve(ledgerHome);
|
|
67
91
|
const absoluteSessionDir = resolve(sessionDir);
|
|
68
92
|
const absoluteFile = resolve(recentFile);
|
|
69
|
-
|
|
70
|
-
const relToHome = relative(absoluteHome, absoluteSessionDir);
|
|
71
|
-
const segments = relToHome.split(sep);
|
|
72
|
-
if (
|
|
73
|
-
relToHome === ""
|
|
74
|
-
|| isAbsolute(relToHome)
|
|
75
|
-
|| relToHome === ".."
|
|
76
|
-
|| relToHome.startsWith(`..${sep}`)
|
|
77
|
-
|| segments[0] !== "books"
|
|
78
|
-
|| segments[1] === undefined
|
|
79
|
-
|| segments[1] === ""
|
|
80
|
-
|| segments[1] === "."
|
|
81
|
-
|| segments[1] === ".."
|
|
82
|
-
) {
|
|
93
|
+
if (absoluteFile !== absoluteSessionDir && !pathContainedIn(absoluteSessionDir, absoluteFile)) {
|
|
83
94
|
throw new ActivationLedgerError(
|
|
84
|
-
`archivist record
|
|
95
|
+
`archivist record session must be under the authorized nest (${sessionDir}): ${recentFile}`,
|
|
85
96
|
);
|
|
86
97
|
}
|
|
87
|
-
|
|
88
|
-
let realBookRoot: string;
|
|
98
|
+
let realSessionDir: string;
|
|
89
99
|
try {
|
|
90
|
-
|
|
100
|
+
realSessionDir = realpathSync(absoluteSessionDir);
|
|
91
101
|
} catch (error) {
|
|
92
102
|
throw new ActivationLedgerError(
|
|
93
|
-
`
|
|
103
|
+
`archivist record sessionDir is not resolvable (${absoluteSessionDir}): ${errorText(error)}`,
|
|
94
104
|
{ cause: error },
|
|
95
105
|
);
|
|
96
106
|
}
|
|
@@ -103,9 +113,9 @@ function assertRecentFinalFileUnderLedgerHome(
|
|
|
103
113
|
{ cause: error },
|
|
104
114
|
);
|
|
105
115
|
}
|
|
106
|
-
if (realFile !==
|
|
116
|
+
if (realFile !== realSessionDir && !pathContainedIn(realSessionDir, realFile)) {
|
|
107
117
|
throw new ActivationLedgerError(
|
|
108
|
-
`archivist record session must be under the
|
|
118
|
+
`archivist record session must be under the authorized nest (${sessionDir}): ${recentFile}`,
|
|
109
119
|
);
|
|
110
120
|
}
|
|
111
121
|
}
|
|
@@ -117,7 +127,7 @@ function assertRecentFinalFileUnderLedgerHome(
|
|
|
117
127
|
* identity is checked once before SessionManager.open (directory walk cannot see a
|
|
118
128
|
* trailing .jsonl symlink). New principals mint under the already-validated sessionDir
|
|
119
129
|
* via destination-free SessionManager.create — no derived postcondition.
|
|
120
|
-
* Resume via
|
|
130
|
+
* Resume via the AK-owned current-session ledger is limited to subject-keyed identity and the
|
|
121
131
|
* authorized worker-submission-gate durable path. Ordinary no-subject children
|
|
122
132
|
* (evidence-children, auditor-roles, …) always mint a fresh session — never reopen
|
|
123
133
|
* a sibling volume selected only by kind/cwd/mtime.
|
|
@@ -156,18 +166,17 @@ export function createRecordSession(options: CreateRecordSessionOptions): Sessio
|
|
|
156
166
|
parentSession = parentFile;
|
|
157
167
|
}
|
|
158
168
|
|
|
169
|
+
const nestAlreadyExists = existsSync(sessionDir);
|
|
159
170
|
// Directory-chain ownership: containment + physical components (no parallel assert).
|
|
160
171
|
ensureRealDirectoryTree(ledgerHome, sessionDir);
|
|
161
172
|
// Subject-keyed nests continue by subject digest; gate durable resume is the only
|
|
162
173
|
// authorized no-subject same-nest continuation. All other kinds mint fresh.
|
|
163
174
|
const mayResumeSameNest =
|
|
164
175
|
options.subject !== undefined || options.kind === WORKER_SUBMISSION_GATE_KIND;
|
|
165
|
-
if (mayResumeSameNest) {
|
|
166
|
-
const recentFile =
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return SessionManager.open(recentFile, sessionDir, cwd);
|
|
170
|
-
}
|
|
176
|
+
if (mayResumeSameNest && nestAlreadyExists) {
|
|
177
|
+
const recentFile = readCurrentSession(sessionDir);
|
|
178
|
+
assertRecentFinalFileUnderSessionDir(sessionDir, recentFile);
|
|
179
|
+
return SessionManager.open(recentFile, sessionDir, cwd);
|
|
171
180
|
}
|
|
172
181
|
|
|
173
182
|
const session = SessionManager.create(
|
|
@@ -188,6 +197,9 @@ export function createRecordSession(options: CreateRecordSessionOptions): Sessio
|
|
|
188
197
|
session.setSessionFile(file);
|
|
189
198
|
}
|
|
190
199
|
}
|
|
200
|
+
if (mayResumeSameNest && file !== undefined) {
|
|
201
|
+
writeCurrentSession(sessionDir, file);
|
|
202
|
+
}
|
|
191
203
|
}
|
|
192
204
|
return session;
|
|
193
205
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dirname, join, resolve } from "node:path";
|
|
2
2
|
|
|
3
3
|
import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { HostContext } from "./host-contracts.ts";
|
|
4
5
|
import { Type } from "typebox";
|
|
5
6
|
|
|
6
7
|
export const AUDITOR_DOSSIER_TOOL_NAME = "ak_get_run_dossier" as const;
|
|
@@ -14,9 +15,10 @@ export type AuditorDossierLocation = {
|
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
/** Resolve the exact run binding already carried by the parent record session. */
|
|
17
|
-
export function auditorRunDirectory(context: ExtensionContext): string | undefined {
|
|
18
|
+
export function auditorRunDirectory(context: ExtensionContext | HostContext): string | undefined {
|
|
18
19
|
const sessionFile = context.sessionManager?.getSessionFile?.();
|
|
19
|
-
|
|
20
|
+
if (sessionFile !== undefined) return resolve(dirname(dirname(sessionFile)));
|
|
21
|
+
return undefined;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/** The one shared, run-bound dossier locator exposed to every auditor seat. */
|
|
@@ -2,7 +2,8 @@ import { readFile, realpath } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { stripFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { HostSkillExpansionEvidence } from "./host-contracts.ts";
|
|
6
7
|
export type CanonicalSkillName = "tdd" | "code-review";
|
|
7
8
|
|
|
8
9
|
export type CanonicalSkillSnapshot = Readonly<{
|
|
@@ -26,7 +27,7 @@ export type CanonicalSkillBinding<Name extends CanonicalSkillName = CanonicalSki
|
|
|
26
27
|
snapshot: CanonicalSkillSnapshot;
|
|
27
28
|
invocation(originalRequest: string): string;
|
|
28
29
|
captureExpansion(
|
|
29
|
-
|
|
30
|
+
evidence: HostSkillExpansionEvidence | undefined,
|
|
30
31
|
originalRequest: string,
|
|
31
32
|
): CanonicalSkillEvidence<Name> | undefined;
|
|
32
33
|
}>;
|
|
@@ -35,6 +36,33 @@ export type AnyCanonicalSkillBinding =
|
|
|
35
36
|
| CanonicalSkillBinding<"tdd">
|
|
36
37
|
| CanonicalSkillBinding<"code-review">;
|
|
37
38
|
|
|
39
|
+
export function captureCanonicalSkillExpansion<Name extends CanonicalSkillName>(
|
|
40
|
+
name: Name,
|
|
41
|
+
snapshot: CanonicalSkillSnapshot,
|
|
42
|
+
configuredPath: string,
|
|
43
|
+
evidence: HostSkillExpansionEvidence | undefined,
|
|
44
|
+
originalRequest: string,
|
|
45
|
+
): CanonicalSkillEvidence<Name> | undefined {
|
|
46
|
+
const matchedPath =
|
|
47
|
+
evidence?.location === configuredPath
|
|
48
|
+
? configuredPath
|
|
49
|
+
: evidence?.location === snapshot.path
|
|
50
|
+
? snapshot.path
|
|
51
|
+
: undefined;
|
|
52
|
+
const expectedContent = matchedPath === undefined
|
|
53
|
+
? undefined
|
|
54
|
+
: `References are relative to ${dirname(matchedPath)}.\n\n${snapshot.body}`;
|
|
55
|
+
if (
|
|
56
|
+
evidence?.name !== name
|
|
57
|
+
|| matchedPath === undefined
|
|
58
|
+
|| evidence.content !== expectedContent
|
|
59
|
+
|| evidence.userMessage !== originalRequest
|
|
60
|
+
) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return Object.freeze({ ...evidence, name });
|
|
64
|
+
}
|
|
65
|
+
|
|
38
66
|
export class CanonicalSkillUnavailableError extends Error {
|
|
39
67
|
readonly code = "canonical-skill-unavailable" as const;
|
|
40
68
|
constructor(readonly skillName: CanonicalSkillName, path: string, cause: unknown) {
|
|
@@ -75,32 +103,14 @@ export async function loadCanonicalSkillBinding(
|
|
|
75
103
|
invocation(originalRequest) {
|
|
76
104
|
return `/skill:${name} ${originalRequest}`;
|
|
77
105
|
},
|
|
78
|
-
captureExpansion(
|
|
79
|
-
|
|
80
|
-
const matchedPath =
|
|
81
|
-
parsed?.location === configuredPath
|
|
82
|
-
? configuredPath
|
|
83
|
-
: parsed?.location === snapshot.path
|
|
84
|
-
? snapshot.path
|
|
85
|
-
: undefined;
|
|
86
|
-
const expectedContent = matchedPath === undefined
|
|
87
|
-
? undefined
|
|
88
|
-
: `References are relative to ${dirname(matchedPath)}.\n\n${snapshot.body}`;
|
|
89
|
-
const userMessage = parsed?.userMessage ?? "";
|
|
90
|
-
if (
|
|
91
|
-
parsed?.name !== name ||
|
|
92
|
-
matchedPath === undefined ||
|
|
93
|
-
parsed.content !== expectedContent ||
|
|
94
|
-
userMessage !== originalRequest
|
|
95
|
-
) {
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
return Object.freeze({
|
|
106
|
+
captureExpansion(evidence, originalRequest) {
|
|
107
|
+
return captureCanonicalSkillExpansion(
|
|
99
108
|
name,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
snapshot,
|
|
110
|
+
configuredPath,
|
|
111
|
+
evidence,
|
|
112
|
+
originalRequest,
|
|
113
|
+
);
|
|
104
114
|
},
|
|
105
115
|
};
|
|
106
116
|
return Object.freeze(binding) as AnyCanonicalSkillBinding;
|
package/src/collector-role.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { RoleHost, HostContext, HostToolResult } from "./host-contracts.ts";
|
|
5
2
|
import type { Static } from "typebox";
|
|
6
3
|
|
|
7
4
|
import {
|
|
@@ -30,9 +27,6 @@ import {
|
|
|
30
27
|
buildCollectorReceipt,
|
|
31
28
|
type CollectorReceipt,
|
|
32
29
|
} from "./collector-receipt.ts";
|
|
33
|
-
import {
|
|
34
|
-
failOnInfrastructureFailureDeclaration,
|
|
35
|
-
} from "./package-contracts/terminating-infrastructure.ts";
|
|
36
30
|
import {
|
|
37
31
|
collectorObserveArgsSchema,
|
|
38
32
|
collectorOutputArgsSchema,
|
|
@@ -73,7 +67,7 @@ export type CollectorRoleDependencies = {
|
|
|
73
67
|
};
|
|
74
68
|
|
|
75
69
|
export type CollectorRoleHostActions = {
|
|
76
|
-
failInfrastructure(error: unknown, ctx:
|
|
70
|
+
failInfrastructure(error: unknown, ctx: HostContext, toolCallId?: string): never;
|
|
77
71
|
};
|
|
78
72
|
|
|
79
73
|
type CollectorActivation = {
|
|
@@ -89,13 +83,13 @@ type CollectorActivation = {
|
|
|
89
83
|
/** Sole-final at the output execution seam (same shape as other terminating roles). */
|
|
90
84
|
function assertSoleFinalCollectorOutput(
|
|
91
85
|
toolCallId: string,
|
|
92
|
-
ctx:
|
|
86
|
+
ctx: HostContext,
|
|
93
87
|
): void {
|
|
94
88
|
const leaf = ctx.sessionManager.getLeafEntry();
|
|
95
|
-
if (leaf?.type !== "message" || leaf.message.role !== "assistant") {
|
|
89
|
+
if (leaf?.type !== "message" || leaf.message === undefined || leaf.message.role !== "assistant" || !Array.isArray(leaf.message.content)) {
|
|
96
90
|
throw new Error("通进司回执非唯一终局工具调用");
|
|
97
91
|
}
|
|
98
|
-
const calls = leaf.message.content.filter((part) => part.type === "toolCall");
|
|
92
|
+
const calls = leaf.message.content.filter((part): part is Extract<typeof part, { type: "toolCall" }> => part.type === "toolCall");
|
|
99
93
|
const call = calls[0];
|
|
100
94
|
if (
|
|
101
95
|
calls.length !== 1 ||
|
|
@@ -119,12 +113,12 @@ function buildMethodContext(activation: CollectorActivation): string {
|
|
|
119
113
|
}
|
|
120
114
|
|
|
121
115
|
export function createCollectorRoleRuntime(
|
|
122
|
-
pi:
|
|
116
|
+
pi: RoleHost,
|
|
123
117
|
dependencies: CollectorRoleDependencies,
|
|
124
118
|
hostActions: CollectorRoleHostActions,
|
|
125
119
|
): {
|
|
126
120
|
activate(
|
|
127
|
-
ctx:
|
|
121
|
+
ctx: HostContext,
|
|
128
122
|
event: { reason: string },
|
|
129
123
|
): Promise<void>;
|
|
130
124
|
} {
|
|
@@ -288,7 +282,7 @@ export function createCollectorRoleRuntime(
|
|
|
288
282
|
description: "抓取配置目标的完整 GitHub PR 证据,存不可变快照入卷。",
|
|
289
283
|
promptSnippet: "抓取配置目标 PR 证据",
|
|
290
284
|
parameters: observeSchema,
|
|
291
|
-
async execute(toolCallId, _params, signal, _onUpdate, ctx) {
|
|
285
|
+
async execute(toolCallId: string, _params: unknown, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
|
|
292
286
|
if (activation === undefined) {
|
|
293
287
|
throw new Error("通进司未激活");
|
|
294
288
|
}
|
|
@@ -323,7 +317,7 @@ export function createCollectorRoleRuntime(
|
|
|
323
317
|
description: "按配置请求体与关联标记,在所引最新快照 HEAD 发一次请求。",
|
|
324
318
|
promptSnippet: "按配置发一次请求",
|
|
325
319
|
parameters: requestSchema,
|
|
326
|
-
async execute(toolCallId, params: RequestParams, signal, _onUpdate, ctx) {
|
|
320
|
+
async execute(toolCallId: string, params: RequestParams, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
|
|
327
321
|
if (activation === undefined) {
|
|
328
322
|
throw new Error("通进司未激活");
|
|
329
323
|
}
|
|
@@ -355,7 +349,7 @@ export function createCollectorRoleRuntime(
|
|
|
355
349
|
description: "再观察前等待;单次上限五分钟且不超剩余资格。",
|
|
356
350
|
promptSnippet: "资格截止前等待",
|
|
357
351
|
parameters: waitSchema,
|
|
358
|
-
async execute(toolCallId, params: WaitParams, signal, _onUpdate, ctx) {
|
|
352
|
+
async execute(toolCallId: string, params: WaitParams, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
|
|
359
353
|
if (activation === undefined) {
|
|
360
354
|
throw new Error("通进司未激活");
|
|
361
355
|
}
|
|
@@ -386,14 +380,11 @@ export function createCollectorRoleRuntime(
|
|
|
386
380
|
description: "观察完成后提交;回执由 runtime 组装。",
|
|
387
381
|
promptSnippet: "提交通进司回执",
|
|
388
382
|
parameters: outputSchema,
|
|
389
|
-
async execute(toolCallId, params: OutputParams, _signal, _onUpdate, ctx) {
|
|
383
|
+
async execute(toolCallId: string, params: OutputParams, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
|
|
390
384
|
if (activation === undefined) {
|
|
391
385
|
throw new Error("通进司未激活");
|
|
392
386
|
}
|
|
393
387
|
try {
|
|
394
|
-
// #541: infra declaration fails via the shared host seam before the
|
|
395
|
-
// operational ledger begins / a receipt is built.
|
|
396
|
-
failOnInfrastructureFailureDeclaration(params, hostActions, ctx, toolCallId);
|
|
397
388
|
assertSoleFinalCollectorOutput(toolCallId, ctx);
|
|
398
389
|
activation.ledger.beginOperational(COLLECTOR_OUTPUT_TOOL, toolCallId);
|
|
399
390
|
const receipt: CollectorReceipt = buildCollectorReceipt(
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Type, type Static } from "typebox";
|
|
2
2
|
import { COLLECTOR_ELIGIBILITY_MS } from "./collector-evidence.ts";
|
|
3
|
-
import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
|
|
4
3
|
|
|
5
4
|
export const collectorObserveArgsSchema = Type.Object({}, { additionalProperties: false });
|
|
6
5
|
export const collectorRequestArgsSchema = Type.Object({
|
|
@@ -11,9 +10,7 @@ export const collectorWaitArgsSchema = Type.Object({
|
|
|
11
10
|
durationMs: Type.Integer({ minimum: 1, maximum: COLLECTOR_ELIGIBILITY_MS, description: "等待毫秒;单次上限五分钟且不超剩余资格" }),
|
|
12
11
|
}, { additionalProperties: false });
|
|
13
12
|
/** Runtime owns the observed groups; the model only signals sole-final submission. */
|
|
14
|
-
export const collectorOutputArgsSchema =
|
|
15
|
-
Type.Object({}, { additionalProperties: true }),
|
|
16
|
-
);
|
|
13
|
+
export const collectorOutputArgsSchema = Type.Object({}, { additionalProperties: true });
|
|
17
14
|
(collectorOutputArgsSchema as unknown as { required: string[] }).required = [];
|
|
18
15
|
|
|
19
16
|
export type CollectorObserveArgs = Static<typeof collectorObserveArgsSchema>;
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AssistantMessage, Usage } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import {
|
|
5
5
|
executeAuditorChild,
|
|
6
|
-
type AuditorCompletion,
|
|
7
6
|
} from "./evidence-child-executor.ts";
|
|
8
7
|
import { createAuditorDossierTool } from "./auditor-dossier-tool.ts";
|
|
9
8
|
import type { DossierObservation } from "./dossier-resolution.ts";
|
|
10
9
|
import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
|
|
11
10
|
|
|
12
|
-
export type ComplianceCompletion = AuditorCompletion;
|
|
13
11
|
export type ComplianceArgumentRootType = "null" | "array" | "undefined" | "string" | "number" | "boolean" | "bigint" | "symbol" | "function";
|
|
14
12
|
export type ComplianceAuditObservation =
|
|
15
13
|
| { kind: "non-object-arguments"; type: ComplianceArgumentRootType }
|
|
@@ -39,8 +37,6 @@ export class ComplianceCandidateUnreadableError extends Error {
|
|
|
39
37
|
if (usage !== undefined) this.usage = usage;
|
|
40
38
|
}
|
|
41
39
|
}
|
|
42
|
-
export type ComplianceDispatch = { model: Model<Api>; auth: { apiKey?: string; headers?: Record<string, string | null>; env?: Record<string, string> } };
|
|
43
|
-
|
|
44
40
|
/** Zero-projection kickoff — soul already carries dossier-fetch duty; no hand-delivered materials. */
|
|
45
41
|
export const AUDITOR_DOSSIER_PROMPT = "本 run 卷宗已就绪。" as const;
|
|
46
42
|
|
|
@@ -55,15 +51,6 @@ export function createComplianceDecisionTool(name: string, description: string)
|
|
|
55
51
|
return { name, description, parameters: complianceDecisionSchema, async execute(_id: string, params: unknown): Promise<AgentToolResult<unknown>> { return { content: [{ type: "text", text: "审计决议已收" }], details: params, terminate: true }; } };
|
|
56
52
|
}
|
|
57
53
|
|
|
58
|
-
export async function prepareComplianceDispatch(model: Model<Api>, context: ExtensionContext, label: string): Promise<ComplianceDispatch> {
|
|
59
|
-
const resolution = await context.modelRegistry.getProviderAuth(model.provider).catch((error: unknown) => { throw new Error(`${label} authentication failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); });
|
|
60
|
-
if (resolution === undefined) throw new Error(`${label} authentication failed: provider is not configured: ${model.provider}`);
|
|
61
|
-
const auth = await context.modelRegistry.getApiKeyAndHeaders(model);
|
|
62
|
-
if (!auth.ok) throw new Error(`${label} authentication failed: ${auth.error}`);
|
|
63
|
-
const env = auth.env ?? resolution.env;
|
|
64
|
-
return { model: resolution.auth.baseUrl ? { ...model, baseUrl: resolution.auth.baseUrl } : model, auth: { ...(auth.apiKey === undefined ? {} : { apiKey: auth.apiKey }), ...(auth.headers === undefined ? {} : { headers: auth.headers }), ...(env === undefined ? {} : { env }) } };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
54
|
export const COMPLIANCE_RESPONSE_ENTRY_TYPE = "ak_compliance_response" as const;
|
|
68
55
|
export const AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE = "ak_auditor_parent_attempt_binding" as const;
|
|
69
56
|
export const AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE = "ak_auditor_compliance_failure" as const;
|
|
@@ -76,43 +63,32 @@ export type AuditorParentAttemptBinding = {
|
|
|
76
63
|
readonly attemptEntryId?: string;
|
|
77
64
|
};
|
|
78
65
|
};
|
|
66
|
+
import { attachDirectErrnoCode, sitianReport } from "./sitian-facade.ts";
|
|
67
|
+
|
|
79
68
|
export class ComplianceResponseRetentionError extends Error {
|
|
80
|
-
constructor(message: string, options?: ErrorOptions) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
export function appendActiveSessionCustomEntry(
|
|
85
|
-
context: ExtensionContext,
|
|
86
|
-
customType: string,
|
|
87
|
-
data?: unknown,
|
|
88
|
-
labels: { unavailable?: string; failed?: string } = {},
|
|
89
|
-
): string {
|
|
90
|
-
const manager = context.sessionManager as unknown as Partial<ActiveSessionResponseAppender> | undefined;
|
|
91
|
-
if (typeof manager?.appendCustomEntry !== "function") {
|
|
92
|
-
throw new ComplianceResponseRetentionError(
|
|
93
|
-
labels.unavailable ?? "session custom entry append is unavailable",
|
|
94
|
-
);
|
|
69
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
70
|
+
super(message, options);
|
|
71
|
+
this.name = "ComplianceResponseRetentionError";
|
|
72
|
+
attachDirectErrnoCode(this, options?.cause);
|
|
95
73
|
}
|
|
74
|
+
}
|
|
75
|
+
function retainComplianceResponse(context: ExtensionContext, response: AssistantMessage): void {
|
|
96
76
|
try {
|
|
97
|
-
|
|
77
|
+
sitianReport({
|
|
78
|
+
level: "event",
|
|
79
|
+
kind: "auditor",
|
|
80
|
+
cwd: context.cwd,
|
|
81
|
+
sessionParent: context.sessionManager.getSessionFile(),
|
|
82
|
+
payload: { version: 1, response },
|
|
83
|
+
source: "compliance-transport",
|
|
84
|
+
});
|
|
98
85
|
} catch (error) {
|
|
99
86
|
throw new ComplianceResponseRetentionError(
|
|
100
|
-
|
|
87
|
+
`compliance response retention failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
101
88
|
{ cause: error },
|
|
102
89
|
);
|
|
103
90
|
}
|
|
104
91
|
}
|
|
105
|
-
function retainComplianceResponse(context: ExtensionContext, response: AssistantMessage): void {
|
|
106
|
-
appendActiveSessionCustomEntry(
|
|
107
|
-
context,
|
|
108
|
-
COMPLIANCE_RESPONSE_ENTRY_TYPE,
|
|
109
|
-
{ version: 1, response },
|
|
110
|
-
{
|
|
111
|
-
unavailable: "compliance response retention is unavailable",
|
|
112
|
-
failed: "compliance response retention failed",
|
|
113
|
-
},
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
92
|
function readListField(value: unknown): readonly unknown[] { return Array.isArray(value) ? value : value === undefined ? [] : [value]; }
|
|
117
93
|
export function readComplianceCandidate(arguments_: unknown, usage?: Usage): ComplianceDecision {
|
|
118
94
|
if (typeof arguments_ !== "object" || arguments_ === null || Array.isArray(arguments_)) {
|
|
@@ -140,7 +116,6 @@ export type RunComplianceAuditOptions = {
|
|
|
140
116
|
serializedInput?: string;
|
|
141
117
|
roleLabel: string;
|
|
142
118
|
invalidDecisionLabel: string;
|
|
143
|
-
runCompletion?: ComplianceCompletion;
|
|
144
119
|
context: ExtensionContext;
|
|
145
120
|
/** Exact machine-owned run binding; never sourced from AK_ROLE_RUN_DIR. */
|
|
146
121
|
runDirectory?: string | undefined;
|
|
@@ -157,7 +132,7 @@ export async function runComplianceAudit(options: RunComplianceAuditOptions): Pr
|
|
|
157
132
|
roleLabel: options.roleLabel,
|
|
158
133
|
context: options.context,
|
|
159
134
|
retainResponse: (response) => retainComplianceResponse(options.context, response),
|
|
160
|
-
...(options.
|
|
135
|
+
...(options.runDirectory === undefined ? {} : { runDirectory: options.runDirectory }),
|
|
161
136
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
162
137
|
});
|
|
163
138
|
if (receipt.noReceiptLifecycle !== undefined) {
|
package/src/doctor-auditor.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { loadAuditorSoul } from "./auditor-soul.ts";
|
|
|
5
5
|
import {
|
|
6
6
|
createComplianceDecisionTool,
|
|
7
7
|
runComplianceAudit,
|
|
8
|
-
type ComplianceCompletion,
|
|
9
8
|
type ComplianceDecision,
|
|
10
9
|
} from "./compliance-transport.ts";
|
|
11
10
|
import {
|
|
@@ -30,9 +29,7 @@ const tool = createComplianceDecisionTool(
|
|
|
30
29
|
* Doctor auditor: zero hand-delivered materials.
|
|
31
30
|
* Candidate testimony must already be on the parent-session books.
|
|
32
31
|
*/
|
|
33
|
-
export function createPiDoctorAuditor(
|
|
34
|
-
runCompletion?: ComplianceCompletion,
|
|
35
|
-
): (options: DoctorAuditOptions) => Promise<ComplianceDecision> {
|
|
32
|
+
export function createPiDoctorAuditor(): (options: DoctorAuditOptions) => Promise<ComplianceDecision> {
|
|
36
33
|
return async (options) => {
|
|
37
34
|
const dossier = resolveAuditDossier();
|
|
38
35
|
requireAuditMaterials(dossier);
|
|
@@ -47,7 +44,6 @@ export function createPiDoctorAuditor(
|
|
|
47
44
|
context: options.context,
|
|
48
45
|
...(auditorRunDirectory(options.context) === undefined ? {} : { runDirectory: auditorRunDirectory(options.context) }),
|
|
49
46
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
50
|
-
...(runCompletion === undefined ? {} : { runCompletion }),
|
|
51
47
|
});
|
|
52
48
|
};
|
|
53
49
|
}
|
package/src/doctor-contracts.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { canonicalJson } from "./canonical-json.ts";
|
|
3
3
|
import { openToolObjectFromUnion } from "./open-tool-schema.ts";
|
|
4
|
-
import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
|
|
5
4
|
|
|
6
5
|
export const DOCTOR_EVIDENCE_TOOL_NAME = "ak_doctor_evidence";
|
|
7
6
|
export const DOCTOR_OUTPUT_TOOL_NAME = "ak_doctor_output";
|
|
@@ -88,9 +87,7 @@ const doctorSubmissionVariants = Type.Union([
|
|
|
88
87
|
missingEvidence: Type.Array(Type.Object({ need: nonblank, targetKeys: Type.Array(nonblank, { minItems: 1 }) }, { additionalProperties: false }), { minItems: 1, description: "如实证词所需而尚缺的证据" }),
|
|
89
88
|
}, { additionalProperties: false, description: "证据不足以支撑如实案证词" }),
|
|
90
89
|
]);
|
|
91
|
-
export const doctorSubmissionSchema =
|
|
92
|
-
openToolObjectFromUnion(doctorSubmissionVariants),
|
|
93
|
-
);
|
|
90
|
+
export const doctorSubmissionSchema = openToolObjectFromUnion(doctorSubmissionVariants);
|
|
94
91
|
export const doctorOutputSchema = Type.Union([
|
|
95
92
|
Type.Object({ status: Type.Literal("completed"), case: caseIdentity, findings: Type.Array(finding), cost }, { additionalProperties: false }),
|
|
96
93
|
doctorSubmissionVariants.anyOf[1]!,
|
package/src/doctor-role.ts
CHANGED
|
@@ -1,19 +1,42 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RoleHost, HostContext, HostToolResult } from "./host-contracts.ts";
|
|
2
2
|
import { disposeComplianceDecision } from "./audit-escalation.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { ComplianceResponseRetentionError, type ComplianceDecision } from "./compliance-transport.ts";
|
|
4
4
|
import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.ts";
|
|
5
|
-
import { failOnInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
|
|
6
5
|
import { DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT, DOCTOR_ACCEPTED_TEXT, DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, doctorSubmissionSchema, validateDoctorOutput, type DoctorCase } from "./doctor-contracts.ts";
|
|
7
6
|
|
|
7
|
+
import { sitianReport } from "./sitian-facade.ts";
|
|
8
|
+
|
|
8
9
|
export { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME };
|
|
9
10
|
export const DOCTOR_CASE_FLAG = { name: "ak-doctor-case", definition: { description: "Retained .ak-roles/books/<book>/issues/<n>/runs directory", type: "string" as const } } as const;
|
|
10
|
-
export type DoctorRoleDependencies = { loadSoul(): Promise<string>; loadCase(path: string): Promise<DoctorCase>; auditCompliance(options: { context:
|
|
11
|
-
function
|
|
12
|
-
|
|
11
|
+
export type DoctorRoleDependencies = { loadSoul(): Promise<string>; loadCase(path: string): Promise<DoctorCase>; auditCompliance(options: { context: HostContext; signal?: AbortSignal }): Promise<ComplianceDecision> };
|
|
12
|
+
function appendCandidate(ctx: HostContext, data: unknown): void {
|
|
13
|
+
try {
|
|
14
|
+
sitianReport({
|
|
15
|
+
level: "event",
|
|
16
|
+
kind: "candidate",
|
|
17
|
+
cwd: ctx.cwd,
|
|
18
|
+
sessionParent: ctx.sessionManager.getSessionFile(),
|
|
19
|
+
payload: data,
|
|
20
|
+
source: "doctor-role",
|
|
21
|
+
});
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw new ComplianceResponseRetentionError(`太医署候选留存失败: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
24
|
+
}
|
|
25
|
+
const append = ctx.sessionManager.appendCustomEntry;
|
|
26
|
+
if (typeof append === "function") {
|
|
27
|
+
try {
|
|
28
|
+
append.call(ctx.sessionManager, DOCTOR_CANDIDATE_ENTRY_TYPE, data);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new ComplianceResponseRetentionError(`太医署候选留存失败: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function singleton(toolCallId: string, ctx: HostContext) { const leaf = ctx.sessionManager.getLeafEntry(); if (leaf?.type !== "message" || leaf.message === undefined || leaf.message.role !== "assistant" || !Array.isArray(leaf.message.content)) throw new Error("太医署回执非唯一终局工具调用"); const calls = leaf.message.content.filter((part): part is Extract<typeof part, { type: "toolCall" }> => part.type === "toolCall"); if (calls.length !== 1 || calls[0]?.id !== toolCallId || calls[0]?.name !== DOCTOR_OUTPUT_TOOL_NAME) throw new Error("太医署回执非唯一终局工具调用"); }
|
|
35
|
+
export function createDoctorRoleRuntime(pi: RoleHost, dependencies: DoctorRoleDependencies, host: { failInfrastructure(error: unknown, ctx: HostContext, toolCallId?: string): never }) {
|
|
13
36
|
let activation: { soul: string; patient: DoctorCase; store: DoctorEvidenceStore } | undefined; let registered = false; pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
|
|
14
37
|
return { async activate() { const path = pi.getFlag(DOCTOR_CASE_FLAG.name); if (typeof path !== "string" || !path.trim()) throw new Error("Doctor requires --ak-doctor-case"); const soul = (await dependencies.loadSoul()).trim(); if (!soul) throw new Error("Doctor soul is empty"); const patient = await dependencies.loadCase(path); activation = { soul, patient, store: new DoctorEvidenceStore(patient) };
|
|
15
|
-
if (!registered) { registered = true; pi.registerTool({ name: DOCTOR_EVIDENCE_TOOL_NAME, label: "太医署证据", description: "分页读取留存的 Pi session 字节。", parameters: doctorEvidenceReadSchema, async execute(_id, params: { evidenceId: string; offset?: number; limit?: number }) { if (!activation) throw new Error("太医署未激活"); const details = activation.store.read(params.evidenceId, params.offset, params.limit); return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details }; } });
|
|
16
|
-
pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "太医署输出", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id, params, signal, _update, ctx): Promise<
|
|
38
|
+
if (!registered) { registered = true; pi.registerTool({ name: DOCTOR_EVIDENCE_TOOL_NAME, label: "太医署证据", description: "分页读取留存的 Pi session 字节。", parameters: doctorEvidenceReadSchema, async execute(_id: string, params: { evidenceId: string; offset?: number; limit?: number }) { if (!activation) throw new Error("太医署未激活"); const details = activation.store.read(params.evidenceId, params.offset, params.limit); return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details }; } });
|
|
39
|
+
pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "太医署输出", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id: string, params: unknown, signal: AbortSignal | undefined, _update: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> { if (!activation) throw new Error("太医署未激活"); singleton(id, ctx); const testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendCandidate(ctx, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx } : { context: ctx, signal }); } catch (error) { host.failInfrastructure(error, ctx, id); } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; const acceptedDetails = details; return disposeComplianceDecision<HostToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_TEXT }], details: acceptedDetails, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT }], details: { ...acceptedDetails, auditNoReceipt }, terminate: true as const, ...usageProjection }), revise: (violations) => { throw new Error(`太医署回执违 soul:${violations.join("; ")}`); }, escalate: (result) => result }, acceptedDetails); } });
|
|
17
40
|
pi.on("before_agent_start", (event) => { if (!activation) throw new Error("太医署未激活"); const catalog = { version: activation.patient.version, identity: activation.patient.identity, admittedMetrics: { provenance: "由留存 session 字节推导,封入受理回执。", cost: activation.patient.cost }, lawfulTargetKeys: ["case", ...activation.patient.cost.invocations.sources], evidence: activation.patient.evidence.map(({ id, kind, sha256, byteLength, contentLength }) => ({ id, kind, sha256, byteLength, contentLength })) }; return { systemPrompt: `${event.systemPrompt}\n\n<doctor_soul>\n${activation.soul}\n</doctor_soul>\n\n<doctor_case>\n${JSON.stringify(catalog)}\n</doctor_case>` }; }); }
|
|
18
41
|
const required = [DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME]; const names = pi.getAllTools().map((tool) => tool.name); for (const name of required) if (names.filter((item) => item === name).length !== 1) throw new Error(`Doctor required tool collision or missing: ${name}`); pi.setActiveTools(required); const active = pi.getActiveTools?.() ?? required; if (active.length !== 2 || !required.every((name) => active.includes(name))) throw new Error("Doctor active tool narrowing failed"); } };
|
|
19
42
|
}
|