@akagilnc/pi-workflow-roles 0.1.3412 → 0.1.3439
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/archivist-record-entry.js +21 -12
- package/dist/archivist-record-topology.js +20 -0
- package/dist/auditor-dossier-tool.js +67 -76
- package/dist/canonical-json.js +66 -69
- package/dist/compliance-transport.js +99 -86
- package/dist/countersign-contracts.js +23 -25
- package/dist/doctor-contracts.js +169 -145
- package/dist/engine-detour-tool.js +83 -87
- package/dist/engine-detour.js +148 -183
- package/dist/evidence-child-executor.js +10 -1
- package/dist/exact-utf8.js +11 -10
- package/dist/git-object-id.js +10 -5
- package/dist/gleaner-left-contracts.js +33 -35
- package/dist/grok/production-host.js +229 -118
- package/dist/inspector-contracts.js +28 -31
- package/dist/institutional-resolution.js +101 -104
- package/dist/ledger-session-read.js +241 -0
- package/dist/merger-contracts.js +66 -56
- package/dist/notary-contracts.js +91 -100
- package/dist/open-tool-schema.js +32 -31
- package/dist/package-contracts/collector-output.js +56 -43
- package/dist/package-contracts/fixer-output.js +52 -50
- package/dist/package-contracts/fixer-packet.js +67 -64
- package/dist/package-contracts/gatekeeper-output.js +48 -56
- package/dist/package-contracts/judge-output.js +23 -20
- package/dist/package-contracts/navigator-output.js +35 -40
- package/dist/package-contracts/reviewer-output.js +94 -110
- package/dist/package-contracts/terminating-infrastructure.js +62 -90
- package/dist/package-contracts/worker-output.js +30 -11
- package/dist/package-resources/engine-material.js +58 -94
- package/dist/public-cli/config.js +420 -484
- package/dist/public-cli/main.js +1219 -524
- package/dist/public-cli/registry.js +107 -114
- package/dist/receipt-delivery-policy.js +74 -76
- package/dist/session-opening-materials.js +44 -47
- package/dist/sha256.js +5 -3
- package/dist/sitian-appender.js +204 -210
- package/dist/sitian-contracts.js +15 -19
- package/dist/sitian-facade.js +5 -10
- package/dist/sitian-reader.js +37 -46
- package/dist/stream-idle-guard.js +63 -71
- package/dist/ticket-seat-memory.js +244 -0
- package/package.json +1 -1
- package/scripts/build-package.mjs +41 -0
- package/souls/audit-law.md +4 -0
- package/souls/countersign.md +1 -1
- package/souls/gleaner-left.md +1 -1
- package/souls/inspector.md +1 -1
- package/souls/notary.md +1 -1
- package/souls/reviewer.md +1 -1
- package/src/analyst-gate-cycles-read.ts +152 -83
- package/src/analyst-ledger.ts +170 -43
- package/src/archivist-record-entry.ts +32 -11
- package/src/archivist-record-topology.ts +39 -0
- package/src/evidence-child-executor.ts +14 -1
- package/src/grok/production-host.ts +44 -9
- package/src/host-contracts.ts +18 -0
- package/src/host-transition-prior-native.ts +8 -1
- package/src/ledger-session-read.ts +56 -0
- package/src/pi/durable-principal.ts +6 -0
- package/src/public-cli/inspector-run.ts +21 -10
- package/src/public-cli/invocation.ts +4 -20
- package/src/public-cli/notary-run.ts +21 -4
- package/src/public-cli/post-admission.ts +333 -42
- package/src/public-cli/settlement.ts +266 -81
- package/src/session-opening-materials.ts +4 -2
- package/src/ticket-seat-memory.ts +454 -0
|
@@ -1,34 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*/
|
|
6
|
-
export const INSPECTOR_OUTPUT_TOOL_NAME = "ak_inspector_output";
|
|
7
|
-
export const INSPECTOR_ACCEPTED_TEXT = "察院回执已接受";
|
|
8
|
-
export function validateRecordedInspectorOutput(value) {
|
|
9
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
10
|
-
throw new Error("Inspector output has no execution discriminator");
|
|
11
|
-
}
|
|
12
|
-
let status;
|
|
13
|
-
try {
|
|
14
|
-
status = value.status;
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
throw new Error("Inspector output has no execution discriminator");
|
|
18
|
-
}
|
|
19
|
-
if (status === "pass" || status === "bounce" || status === "escalate") {
|
|
20
|
-
return value;
|
|
21
|
-
}
|
|
1
|
+
const INSPECTOR_OUTPUT_TOOL_NAME = "ak_inspector_output";
|
|
2
|
+
const INSPECTOR_ACCEPTED_TEXT = "\u5BDF\u9662\u56DE\u6267\u5DF2\u63A5\u53D7";
|
|
3
|
+
function validateRecordedInspectorOutput(value) {
|
|
4
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
22
5
|
throw new Error("Inspector output has no execution discriminator");
|
|
6
|
+
}
|
|
7
|
+
let status;
|
|
8
|
+
try {
|
|
9
|
+
status = value.status;
|
|
10
|
+
} catch {
|
|
11
|
+
throw new Error("Inspector output has no execution discriminator");
|
|
12
|
+
}
|
|
13
|
+
if (status === "pass" || status === "bounce" || status === "escalate") {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
throw new Error("Inspector output has no execution discriminator");
|
|
23
17
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (reason !== undefined)
|
|
32
|
-
facts.reason = reason;
|
|
33
|
-
return facts;
|
|
18
|
+
function inspectorDecisiveFacts(output) {
|
|
19
|
+
const facts = { status: output.status };
|
|
20
|
+
const findings = output.findings;
|
|
21
|
+
if (findings !== void 0) facts.findings = findings;
|
|
22
|
+
const reason = output.reason;
|
|
23
|
+
if (reason !== void 0) facts.reason = reason;
|
|
24
|
+
return facts;
|
|
34
25
|
}
|
|
26
|
+
export {
|
|
27
|
+
INSPECTOR_ACCEPTED_TEXT,
|
|
28
|
+
INSPECTOR_OUTPUT_TOOL_NAME,
|
|
29
|
+
inspectorDecisiveFacts,
|
|
30
|
+
validateRecordedInspectorOutput
|
|
31
|
+
};
|
|
@@ -1,115 +1,112 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { seatModelOnly } from "./public-cli/registry.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
const INSTITUTIONAL_RESOLUTION_FILE = "institutional-resolution.json";
|
|
5
|
+
class InstitutionalResolutionError extends Error {
|
|
6
|
+
reason;
|
|
7
|
+
constructor(message, reason, options) {
|
|
8
|
+
super(message, options);
|
|
9
|
+
this.name = "InstitutionalResolutionError";
|
|
10
|
+
this.reason = reason;
|
|
11
|
+
}
|
|
12
12
|
}
|
|
13
13
|
function cleanSelection(model) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
...(model.thinking === undefined ? {} : { thinking: model.thinking }),
|
|
22
|
-
};
|
|
14
|
+
if (model === void 0) return void 0;
|
|
15
|
+
if (typeof model.provider !== "string" || typeof model.model !== "string") return void 0;
|
|
16
|
+
return {
|
|
17
|
+
provider: model.provider,
|
|
18
|
+
model: model.model,
|
|
19
|
+
...model.thinking === void 0 ? {} : { thinking: model.thinking }
|
|
20
|
+
};
|
|
23
21
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*/
|
|
31
|
-
export function resolveConfiguredProvinceOfficer(config, officer) {
|
|
32
|
-
const ownModel = seatModelOnly(config.seats[officer]);
|
|
33
|
-
if (ownModel !== undefined) {
|
|
34
|
-
return { selection: ownModel, source: "persistent" };
|
|
35
|
-
}
|
|
36
|
-
if (officer === "gatekeeper") {
|
|
37
|
-
return { source: "unconfigured" };
|
|
38
|
-
}
|
|
39
|
-
const gatekeeperModel = seatModelOnly(config.seats.gatekeeper);
|
|
40
|
-
if (gatekeeperModel !== undefined) {
|
|
41
|
-
return { selection: gatekeeperModel, source: "inherit-gatekeeper" };
|
|
42
|
-
}
|
|
22
|
+
function resolveConfiguredProvinceOfficer(config, officer) {
|
|
23
|
+
const ownModel = seatModelOnly(config.seats[officer]);
|
|
24
|
+
if (ownModel !== void 0) {
|
|
25
|
+
return { selection: ownModel, source: "persistent" };
|
|
26
|
+
}
|
|
27
|
+
if (officer === "gatekeeper") {
|
|
43
28
|
return { source: "unconfigured" };
|
|
29
|
+
}
|
|
30
|
+
const gatekeeperModel = seatModelOnly(config.seats.gatekeeper);
|
|
31
|
+
if (gatekeeperModel !== void 0) {
|
|
32
|
+
return { selection: gatekeeperModel, source: "inherit-gatekeeper" };
|
|
33
|
+
}
|
|
34
|
+
return { source: "unconfigured" };
|
|
44
35
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
version: 1,
|
|
65
|
-
seats: {
|
|
66
|
-
...(gatekeeper === undefined ? {} : { gatekeeper }),
|
|
67
|
-
...(inspector === undefined ? {} : { inspector }),
|
|
68
|
-
...(notary === undefined ? {} : { notary }),
|
|
69
|
-
...(auditor === undefined ? {} : { auditor }),
|
|
70
|
-
...(evidenceChild === undefined ? {} : { evidenceChild }),
|
|
71
|
-
...(navigator === undefined ? {} : { navigator }),
|
|
72
|
-
},
|
|
73
|
-
};
|
|
36
|
+
function resolveInstitutionalSeatSelections(config, parentEffectiveModel) {
|
|
37
|
+
const parentSelection = cleanSelection(parentEffectiveModel);
|
|
38
|
+
const gatekeeper = resolveConfiguredProvinceOfficer(config, "gatekeeper").selection ?? parentSelection;
|
|
39
|
+
const inspector = resolveConfiguredProvinceOfficer(config, "inspector").selection ?? parentSelection;
|
|
40
|
+
const notary = resolveConfiguredProvinceOfficer(config, "notary").selection ?? parentSelection;
|
|
41
|
+
const auditor = parentSelection;
|
|
42
|
+
const evidenceChild = parentSelection;
|
|
43
|
+
const navigator = cleanSelection(seatModelOnly(config.seats?.navigator));
|
|
44
|
+
return {
|
|
45
|
+
version: 1,
|
|
46
|
+
seats: {
|
|
47
|
+
...gatekeeper === void 0 ? {} : { gatekeeper },
|
|
48
|
+
...inspector === void 0 ? {} : { inspector },
|
|
49
|
+
...notary === void 0 ? {} : { notary },
|
|
50
|
+
...auditor === void 0 ? {} : { auditor },
|
|
51
|
+
...evidenceChild === void 0 ? {} : { evidenceChild },
|
|
52
|
+
...navigator === void 0 ? {} : { navigator }
|
|
53
|
+
}
|
|
54
|
+
};
|
|
74
55
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
export async function writeInstitutionalResolutionPage(runDirectory, page) {
|
|
80
|
-
const filePath = join(runDirectory, INSTITUTIONAL_RESOLUTION_FILE);
|
|
81
|
-
await writeFile(filePath, `${JSON.stringify(page, null, 2)}\n`, "utf8");
|
|
56
|
+
async function writeInstitutionalResolutionPage(runDirectory, page) {
|
|
57
|
+
const filePath = join(runDirectory, INSTITUTIONAL_RESOLUTION_FILE);
|
|
58
|
+
await writeFile(filePath, `${JSON.stringify(page, null, 2)}
|
|
59
|
+
`, "utf8");
|
|
82
60
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
61
|
+
async function readInstitutionalSeatSelection(runDirectory, seat) {
|
|
62
|
+
const filePath = join(runDirectory, INSTITUTIONAL_RESOLUTION_FILE);
|
|
63
|
+
let raw;
|
|
64
|
+
try {
|
|
65
|
+
raw = await readFile(filePath, "utf8");
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new InstitutionalResolutionError(
|
|
68
|
+
`institutional resolution page is missing at ${filePath}`,
|
|
69
|
+
"missing-page",
|
|
70
|
+
{ cause: error }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
let page;
|
|
74
|
+
try {
|
|
75
|
+
page = JSON.parse(raw);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new InstitutionalResolutionError(
|
|
78
|
+
`institutional resolution page is corrupted at ${filePath}`,
|
|
79
|
+
"corrupted",
|
|
80
|
+
{ cause: error }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (typeof page !== "object" || page === null || page.version !== 1) {
|
|
84
|
+
throw new InstitutionalResolutionError(
|
|
85
|
+
`institutional resolution page format is invalid at ${filePath}`,
|
|
86
|
+
"invalid-format"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const seats = page.seats;
|
|
90
|
+
if (typeof seats !== "object" || seats === null) {
|
|
91
|
+
throw new InstitutionalResolutionError(
|
|
92
|
+
`institutional resolution page missing seats object at ${filePath}`,
|
|
93
|
+
"invalid-format"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const selection = cleanSelection(seats[seat]);
|
|
97
|
+
if (selection === void 0) {
|
|
98
|
+
throw new InstitutionalResolutionError(
|
|
99
|
+
`institutional resolution page has no resolution for seat "${seat}" at ${filePath}`,
|
|
100
|
+
"missing-seat"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return selection;
|
|
115
104
|
}
|
|
105
|
+
export {
|
|
106
|
+
INSTITUTIONAL_RESOLUTION_FILE,
|
|
107
|
+
InstitutionalResolutionError,
|
|
108
|
+
readInstitutionalSeatSelection,
|
|
109
|
+
resolveConfiguredProvinceOfficer,
|
|
110
|
+
resolveInstitutionalSeatSelections,
|
|
111
|
+
writeInstitutionalResolutionPage
|
|
112
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical ledger session JSONL read primitives (shared owner).
|
|
3
|
+
* Consumers (ticket-trajectory, analyst, …) must import here — no second parse kernel.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
/**
|
|
7
|
+
* Loud JSONL failure that still retains rows parsed before the bad line.
|
|
8
|
+
* Callers that only need the throw keep catching Error; owners that must
|
|
9
|
+
* surface partial typed facts (e.g. first-frame timestamp) read prefixRows.
|
|
10
|
+
*/
|
|
11
|
+
export class LedgerSessionJsonlError extends Error {
|
|
12
|
+
path;
|
|
13
|
+
line;
|
|
14
|
+
prefixRows;
|
|
15
|
+
constructor(message, init) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "LedgerSessionJsonlError";
|
|
18
|
+
this.path = init.path;
|
|
19
|
+
this.line = init.line;
|
|
20
|
+
this.prefixRows = init.prefixRows;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read session JSONL with honest live-tail semantics:
|
|
28
|
+
* a malformed line is tolerated only when it is an unfinished final
|
|
29
|
+
* fragment at EOF (no record terminator after it). Any malformed line
|
|
30
|
+
* completed by a line terminator must fail loudly with file and 1-based
|
|
31
|
+
* line context — even when no non-empty record follows — never silently
|
|
32
|
+
* under-count.
|
|
33
|
+
*
|
|
34
|
+
* Loud failures throw LedgerSessionJsonlError carrying prefixRows so the
|
|
35
|
+
* single parse kernel can still expose facts obtained before the bad line.
|
|
36
|
+
*/
|
|
37
|
+
export async function readLedgerSessionJsonl(path) {
|
|
38
|
+
const text = await readFile(path, "utf8");
|
|
39
|
+
// split keeps a trailing empty segment iff text ends with "\n", so
|
|
40
|
+
// index < lines.length - 1 means this segment was terminated.
|
|
41
|
+
const lines = text.split("\n");
|
|
42
|
+
const rows = [];
|
|
43
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
44
|
+
const line = lines[index];
|
|
45
|
+
if (!line.trim())
|
|
46
|
+
continue;
|
|
47
|
+
let row;
|
|
48
|
+
try {
|
|
49
|
+
row = JSON.parse(line);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (!(error instanceof SyntaxError))
|
|
53
|
+
throw error;
|
|
54
|
+
const completedByTerminator = index < lines.length - 1;
|
|
55
|
+
if (completedByTerminator) {
|
|
56
|
+
throw new LedgerSessionJsonlError(`malformed JSONL record in ${path} at line ${index + 1}: ${error.message}`, { path, line: index + 1, prefixRows: rows });
|
|
57
|
+
}
|
|
58
|
+
// unfinished fragment at EOF — keep prior complete rows
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
// Syntactically complete line: must be a session object. Silent omission
|
|
62
|
+
// would under-count ledger evidence (failure honesty).
|
|
63
|
+
if (!isRecord(row)) {
|
|
64
|
+
const kind = row === null ? "null" : Array.isArray(row) ? "array" : typeof row;
|
|
65
|
+
throw new LedgerSessionJsonlError(`complete non-object JSONL record in ${path} at line ${index + 1}: expected object, got ${kind}`, { path, line: index + 1, prefixRows: rows });
|
|
66
|
+
}
|
|
67
|
+
rows.push(row);
|
|
68
|
+
}
|
|
69
|
+
return rows;
|
|
70
|
+
}
|
|
71
|
+
/** First and last record timestamps in encounter order. */
|
|
72
|
+
export function extractSessionTimestampSpan(rows) {
|
|
73
|
+
let startedAt;
|
|
74
|
+
let endedAt;
|
|
75
|
+
for (const row of rows) {
|
|
76
|
+
if (typeof row.timestamp !== "string" || !row.timestamp)
|
|
77
|
+
continue;
|
|
78
|
+
if (startedAt === undefined)
|
|
79
|
+
startedAt = row.timestamp;
|
|
80
|
+
endedAt = row.timestamp;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
84
|
+
...(endedAt !== undefined ? { endedAt } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Ordered-unique model ids from session frames (first-seen order).
|
|
89
|
+
* Sources (same faces ticket-trajectory already reads — single parse kernel):
|
|
90
|
+
* - `model_change.modelId`
|
|
91
|
+
* - assistant `message.model`
|
|
92
|
+
* Blank / non-string values are skipped. Does not invent a default model.
|
|
93
|
+
*/
|
|
94
|
+
export function extractSessionModelSequence(rows) {
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
const ordered = [];
|
|
97
|
+
const push = (raw) => {
|
|
98
|
+
const model = raw.trim();
|
|
99
|
+
if (model === "" || seen.has(model))
|
|
100
|
+
return;
|
|
101
|
+
seen.add(model);
|
|
102
|
+
ordered.push(model);
|
|
103
|
+
};
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
if (row.type === "model_change" && typeof row.modelId === "string") {
|
|
106
|
+
push(row.modelId);
|
|
107
|
+
}
|
|
108
|
+
const message = isRecord(row.message) ? row.message : undefined;
|
|
109
|
+
if (message?.role === "assistant" && typeof message.model === "string") {
|
|
110
|
+
push(message.model);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return ordered;
|
|
114
|
+
}
|
|
115
|
+
/** First line of a bash `command` argument (sole owner of this summary). */
|
|
116
|
+
export function bashCommandFirstLine(command) {
|
|
117
|
+
const match = /^[^\r\n]*/.exec(command);
|
|
118
|
+
return match?.[0] ?? "";
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Pair toolCall frames → toolResult frames by toolCallId.
|
|
122
|
+
* Throws when a tool-bearing frame is structurally unreadable for association
|
|
123
|
+
* (toolCall missing string id, toolResult missing string toolCallId).
|
|
124
|
+
* Unpaired open calls remain without endedAt — that is incomplete, not unreadable.
|
|
125
|
+
*/
|
|
126
|
+
export function extractSessionToolIntervals(rows) {
|
|
127
|
+
const order = [];
|
|
128
|
+
const openById = new Map();
|
|
129
|
+
for (const row of rows) {
|
|
130
|
+
const rowTimestamp = typeof row.timestamp === "string" ? row.timestamp : undefined;
|
|
131
|
+
const message = isRecord(row.message) ? row.message : undefined;
|
|
132
|
+
if (message?.role === "assistant" && Array.isArray(message.content)) {
|
|
133
|
+
const callTimestamp = typeof message.timestamp === "string" && message.timestamp
|
|
134
|
+
? message.timestamp
|
|
135
|
+
: rowTimestamp;
|
|
136
|
+
for (const part of message.content) {
|
|
137
|
+
if (!isRecord(part) || part.type !== "toolCall")
|
|
138
|
+
continue;
|
|
139
|
+
if (typeof part.id !== "string" || part.id.length === 0) {
|
|
140
|
+
throw new Error("toolCall frame missing string id");
|
|
141
|
+
}
|
|
142
|
+
if (typeof part.name !== "string" || part.name.length === 0) {
|
|
143
|
+
throw new Error(`toolCall ${part.id} missing string name`);
|
|
144
|
+
}
|
|
145
|
+
if (callTimestamp === undefined || callTimestamp.length === 0) {
|
|
146
|
+
throw new Error(`toolCall ${part.id} missing timestamp`);
|
|
147
|
+
}
|
|
148
|
+
if (openById.has(part.id)) {
|
|
149
|
+
throw new Error(`duplicate toolCall id ${part.id}`);
|
|
150
|
+
}
|
|
151
|
+
const args = isRecord(part.arguments) ? part.arguments : undefined;
|
|
152
|
+
// Ticket surface: only bash first-line summary is authorized here.
|
|
153
|
+
const command = part.name === "bash" &&
|
|
154
|
+
args !== undefined &&
|
|
155
|
+
typeof args.command === "string"
|
|
156
|
+
? bashCommandFirstLine(args.command)
|
|
157
|
+
: undefined;
|
|
158
|
+
const interval = {
|
|
159
|
+
toolCallId: part.id,
|
|
160
|
+
toolName: part.name,
|
|
161
|
+
startedAt: callTimestamp,
|
|
162
|
+
...(command !== undefined ? { command } : {}),
|
|
163
|
+
};
|
|
164
|
+
order.push(interval);
|
|
165
|
+
openById.set(part.id, interval);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (message?.role === "toolResult") {
|
|
169
|
+
if (typeof message.toolCallId !== "string" || message.toolCallId.length === 0) {
|
|
170
|
+
throw new Error("toolResult frame missing string toolCallId");
|
|
171
|
+
}
|
|
172
|
+
const resultTimestamp = typeof message.timestamp === "string" && message.timestamp
|
|
173
|
+
? message.timestamp
|
|
174
|
+
: rowTimestamp;
|
|
175
|
+
if (resultTimestamp === undefined || resultTimestamp.length === 0) {
|
|
176
|
+
throw new Error(`toolResult ${message.toolCallId} missing timestamp`);
|
|
177
|
+
}
|
|
178
|
+
const open = openById.get(message.toolCallId);
|
|
179
|
+
if (open === undefined) {
|
|
180
|
+
// Result without a prior call is still associable as a closed interval
|
|
181
|
+
// once a name is known; keep structural readability without inventing a call.
|
|
182
|
+
const toolName = typeof message.toolName === "string" && message.toolName.length > 0
|
|
183
|
+
? message.toolName
|
|
184
|
+
: "unknown";
|
|
185
|
+
order.push({
|
|
186
|
+
toolCallId: message.toolCallId,
|
|
187
|
+
toolName,
|
|
188
|
+
startedAt: resultTimestamp,
|
|
189
|
+
endedAt: resultTimestamp,
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (open.endedAt !== undefined) {
|
|
194
|
+
throw new Error(`duplicate toolResult for toolCallId ${message.toolCallId}`);
|
|
195
|
+
}
|
|
196
|
+
open.endedAt = resultTimestamp;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return order.map((interval) => {
|
|
200
|
+
const base = {
|
|
201
|
+
toolCallId: interval.toolCallId,
|
|
202
|
+
toolName: interval.toolName,
|
|
203
|
+
startedAt: interval.startedAt,
|
|
204
|
+
...(interval.command !== undefined ? { command: interval.command } : {}),
|
|
205
|
+
};
|
|
206
|
+
return interval.endedAt === undefined
|
|
207
|
+
? base
|
|
208
|
+
: { ...base, endedAt: interval.endedAt };
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
export function intervalRowsAroundAnchor(rows, anchorIndex, isBindingRow) {
|
|
212
|
+
let start = 0;
|
|
213
|
+
for (let i = anchorIndex; i >= 0; i -= 1) {
|
|
214
|
+
if (isBindingRow(rows[i])) {
|
|
215
|
+
start = i;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
let end = rows.length;
|
|
220
|
+
for (let i = Math.max(anchorIndex, start) + 1; i < rows.length; i += 1) {
|
|
221
|
+
if (isBindingRow(rows[i])) {
|
|
222
|
+
end = i;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { rows: rows.slice(start, end), closed: end < rows.length };
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Interval owned by the first binding row that satisfies `matches`.
|
|
230
|
+
* `undefined` when the volume carries no matching binding — callers must not
|
|
231
|
+
* silently treat the whole continuous volume as that owner.
|
|
232
|
+
*/
|
|
233
|
+
export function intervalRowsForMatchingBinding(rows, isBindingRow, matches) {
|
|
234
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
235
|
+
const row = rows[i];
|
|
236
|
+
if (!isBindingRow(row) || !matches(row))
|
|
237
|
+
continue;
|
|
238
|
+
return intervalRowsAroundAnchor(rows, i, isBindingRow);
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|