@llblab/pi-actors 0.40.1 → 0.41.1
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/AGENTS.md +3 -3
- package/CHANGELOG.md +32 -0
- package/README.md +2 -1
- package/dist/index.js +80 -58
- package/dist/lib/inspector-overlay.d.ts +95 -0
- package/dist/lib/inspector-overlay.js +873 -0
- package/dist/lib/inspector.d.ts +17 -33
- package/dist/lib/inspector.js +101 -151
- package/dist/lib/limits.d.ts +3 -0
- package/dist/lib/limits.js +3 -0
- package/dist/lib/observability.d.ts +38 -3
- package/dist/lib/observability.js +151 -33
- package/dist/lib/recipes-context.d.ts +5 -0
- package/dist/lib/recipes-context.js +15 -0
- package/dist/lib/session-evidence.d.ts +45 -0
- package/dist/lib/session-evidence.js +204 -0
- package/dist/lib/tools-response.d.ts +3 -0
- package/dist/lib/tools-response.js +29 -0
- package/dist/scripts/async-runner.mjs +40 -5
- package/dist/scripts/coordinator.mjs +55 -9
- package/dist/skills/actors/SKILL.md +5 -11
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/README.md +1 -0
- package/docs/actor-inspector.md +77 -0
- package/docs/async-runs.md +1 -1
- package/index.ts +80 -70
- package/lib/inspector-overlay.ts +1071 -0
- package/lib/inspector.ts +135 -204
- package/lib/limits.ts +3 -0
- package/lib/observability.ts +205 -39
- package/lib/recipes-context.ts +25 -0
- package/lib/session-evidence.ts +302 -0
- package/lib/tools-response.ts +32 -0
- package/package.json +1 -1
- package/scripts/async-runner.mjs +40 -5
- package/scripts/coordinator.mjs +55 -9
- package/skills/actors/SKILL.md +5 -11
- package/skills/swarm/SKILL.md +1 -1
|
@@ -16,11 +16,13 @@ export function createRunUiObservationState() {
|
|
|
16
16
|
outboxEventIds: new Map(),
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
-
export function readRunUiSnapshot(state, ownerId) {
|
|
20
|
-
const summary = summarizeRuns(
|
|
19
|
+
export function readRunUiSnapshot(state, ownerId, options = {}) {
|
|
20
|
+
const summary = summarizeRuns(options.stateRoot, ownerId);
|
|
21
21
|
const status = renderRunStatus(summary, state.frame++);
|
|
22
22
|
return {
|
|
23
|
-
outboxEvents:
|
|
23
|
+
outboxEvents: options.includeOutbox === false
|
|
24
|
+
? []
|
|
25
|
+
: detectRunOutboxEvents(state.eventLines, summary, state.outboxEventIds),
|
|
24
26
|
status,
|
|
25
27
|
summary,
|
|
26
28
|
transitions: detectRunTransitions(state.observed, summary),
|
|
@@ -29,25 +31,43 @@ export function readRunUiSnapshot(state, ownerId) {
|
|
|
29
31
|
export function pruneRunUiObservationState(state, snapshot) {
|
|
30
32
|
pruneRunObservationState(state.observed, state.eventLines, snapshot.summary, snapshot.transitions.map((transition) => transition.stateDir ?? transition.run), state.outboxEventIds);
|
|
31
33
|
}
|
|
32
|
-
export function deliverRunTransitionNotifications(transitions, sink) {
|
|
34
|
+
export function deliverRunTransitionNotifications(transitions, sink, inFlight = new Set()) {
|
|
33
35
|
for (const transition of transitions) {
|
|
34
36
|
if (!shouldNotifyRunTransition(transition))
|
|
35
37
|
continue;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
if (!shouldSendRunTransitionFollowUp(transition))
|
|
38
|
+
const key = transition.stateDir ?? transition.run;
|
|
39
|
+
if (inFlight.has(key))
|
|
39
40
|
continue;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
inFlight.add(key);
|
|
42
|
+
try {
|
|
43
|
+
const text = formatRunTransitionMessage(transition);
|
|
44
|
+
sink.notify(text, getRunTransitionNotificationType(transition));
|
|
45
|
+
if (!shouldSendRunTransitionFollowUp(transition))
|
|
46
|
+
continue;
|
|
47
|
+
sink.sendFollowUp({
|
|
48
|
+
customType: "pi-actors-run",
|
|
49
|
+
content: text,
|
|
50
|
+
display: true,
|
|
51
|
+
details: transition,
|
|
52
|
+
});
|
|
53
|
+
if (transition.stateDir) {
|
|
54
|
+
AsyncRuns.markRunTerminalNotificationHandled(transition.stateDir, transition.to);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
inFlight.delete(key);
|
|
48
59
|
}
|
|
49
60
|
}
|
|
50
61
|
}
|
|
62
|
+
export function reconcileRunTerminalNotifications(input) {
|
|
63
|
+
const snapshot = readRunUiSnapshot(input.state, input.ownerId, {
|
|
64
|
+
includeOutbox: false,
|
|
65
|
+
stateRoot: input.stateRoot,
|
|
66
|
+
});
|
|
67
|
+
deliverRunTransitionNotifications(snapshot.transitions, input.sink, input.inFlight);
|
|
68
|
+
pruneRunUiObservationState(input.state, snapshot);
|
|
69
|
+
return snapshot;
|
|
70
|
+
}
|
|
51
71
|
export function deliverRunOutboxNotifications(events, sink) {
|
|
52
72
|
for (const event of events) {
|
|
53
73
|
if (!shouldNotifyRunOutboxEvent(event))
|
|
@@ -64,10 +84,58 @@ export function deliverRunOutboxNotifications(events, sink) {
|
|
|
64
84
|
});
|
|
65
85
|
}
|
|
66
86
|
}
|
|
87
|
+
const RUN_WATCHER_DIAGNOSTIC_LIMIT = 32;
|
|
67
88
|
export function createRunStateWatcher(input) {
|
|
68
89
|
const stateRoot = input.stateRoot ?? Paths.getRunStateRoot();
|
|
90
|
+
const pathExists = input.exists ?? existsSync;
|
|
91
|
+
const listDirectories = input.listDirectories ??
|
|
92
|
+
((path) => readdirSync(path, { withFileTypes: true })
|
|
93
|
+
.filter((entry) => entry.isDirectory())
|
|
94
|
+
.map((entry) => join(path, entry.name)));
|
|
95
|
+
const watchPath = input.watchPath ?? ((path, onChange) => watch(path, onChange));
|
|
96
|
+
let diagnosticId = 0;
|
|
97
|
+
let rootDegraded = false;
|
|
69
98
|
let stateRootWatcher;
|
|
99
|
+
const degradedRunDirs = new Set();
|
|
100
|
+
const diagnostics = [];
|
|
101
|
+
const lastDiagnosticSignatures = new Map();
|
|
70
102
|
const runDirWatchers = new Map();
|
|
103
|
+
const record = (code, scope, path, error) => {
|
|
104
|
+
const detail = error instanceof Error ? `: ${error.message}` : "";
|
|
105
|
+
const signature = `${code}${detail}`;
|
|
106
|
+
const signatureKey = `${scope}:${path}`;
|
|
107
|
+
if (lastDiagnosticSignatures.get(signatureKey) === signature)
|
|
108
|
+
return;
|
|
109
|
+
lastDiagnosticSignatures.set(signatureKey, signature);
|
|
110
|
+
const recovery = code === "rearmed"
|
|
111
|
+
? "; terminal watch acceleration restored"
|
|
112
|
+
: "; terminal reconciliation remains active and watcher rearm will retry";
|
|
113
|
+
diagnostics.push({
|
|
114
|
+
code,
|
|
115
|
+
id: ++diagnosticId,
|
|
116
|
+
message: `Run-state ${scope} watcher ${code.replace("_", " ")} for ${path}${detail}${recovery}`,
|
|
117
|
+
path,
|
|
118
|
+
scope,
|
|
119
|
+
ts: new Date().toISOString(),
|
|
120
|
+
});
|
|
121
|
+
if (diagnostics.length > RUN_WATCHER_DIAGNOSTIC_LIMIT)
|
|
122
|
+
diagnostics.shift();
|
|
123
|
+
};
|
|
124
|
+
const removeRunWatcher = (stateDir, watcher, options = {}) => {
|
|
125
|
+
if (runDirWatchers.get(stateDir) !== watcher)
|
|
126
|
+
return;
|
|
127
|
+
watcher.close();
|
|
128
|
+
runDirWatchers.delete(stateDir);
|
|
129
|
+
if (options.degraded === false) {
|
|
130
|
+
degradedRunDirs.delete(stateDir);
|
|
131
|
+
lastDiagnosticSignatures.delete(`run:${stateDir}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
degradedRunDirs.add(stateDir);
|
|
135
|
+
if (options.error)
|
|
136
|
+
record("error", "run", stateDir, options.error);
|
|
137
|
+
record("removed", "run", stateDir);
|
|
138
|
+
};
|
|
71
139
|
const close = () => {
|
|
72
140
|
stateRootWatcher?.close();
|
|
73
141
|
stateRootWatcher = undefined;
|
|
@@ -76,42 +144,92 @@ export function createRunStateWatcher(input) {
|
|
|
76
144
|
runDirWatchers.clear();
|
|
77
145
|
};
|
|
78
146
|
const watchRunDir = (stateDir) => {
|
|
79
|
-
if (runDirWatchers.has(stateDir) || !
|
|
147
|
+
if (runDirWatchers.has(stateDir) || !pathExists(stateDir))
|
|
80
148
|
return;
|
|
81
149
|
try {
|
|
82
|
-
const watcher =
|
|
83
|
-
watcher.on("error", () => {
|
|
84
|
-
watcher.close();
|
|
85
|
-
runDirWatchers.delete(stateDir);
|
|
86
|
-
});
|
|
150
|
+
const watcher = watchPath(stateDir, input.onChange);
|
|
151
|
+
watcher.on("error", (error) => removeRunWatcher(stateDir, watcher, { error }));
|
|
87
152
|
runDirWatchers.set(stateDir, watcher);
|
|
153
|
+
if (degradedRunDirs.delete(stateDir))
|
|
154
|
+
record("rearmed", "run", stateDir);
|
|
88
155
|
}
|
|
89
|
-
catch {
|
|
90
|
-
|
|
156
|
+
catch (error) {
|
|
157
|
+
degradedRunDirs.add(stateDir);
|
|
158
|
+
record("attach_failed", "run", stateDir, error);
|
|
91
159
|
}
|
|
92
160
|
};
|
|
93
161
|
function refresh() {
|
|
94
|
-
if (!
|
|
162
|
+
if (!pathExists(stateRoot))
|
|
95
163
|
return;
|
|
96
164
|
if (!stateRootWatcher) {
|
|
97
165
|
try {
|
|
98
|
-
|
|
99
|
-
stateRootWatcher
|
|
100
|
-
|
|
166
|
+
const watcher = watchPath(stateRoot, input.onChange);
|
|
167
|
+
stateRootWatcher = watcher;
|
|
168
|
+
watcher.on("error", (error) => {
|
|
169
|
+
if (stateRootWatcher !== watcher)
|
|
170
|
+
return;
|
|
171
|
+
watcher.close();
|
|
101
172
|
stateRootWatcher = undefined;
|
|
173
|
+
rootDegraded = true;
|
|
174
|
+
record("error", "root", stateRoot, error);
|
|
175
|
+
record("removed", "root", stateRoot);
|
|
102
176
|
});
|
|
177
|
+
if (rootDegraded) {
|
|
178
|
+
rootDegraded = false;
|
|
179
|
+
record("rearmed", "root", stateRoot);
|
|
180
|
+
}
|
|
103
181
|
}
|
|
104
|
-
catch {
|
|
105
|
-
|
|
182
|
+
catch (error) {
|
|
183
|
+
rootDegraded = true;
|
|
184
|
+
record("attach_failed", "root", stateRoot, error);
|
|
106
185
|
}
|
|
107
186
|
}
|
|
108
|
-
|
|
109
|
-
|
|
187
|
+
let stateDirs;
|
|
188
|
+
try {
|
|
189
|
+
stateDirs = listDirectories(stateRoot);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
record("attach_failed", "root", stateRoot, error);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const present = new Set(stateDirs);
|
|
196
|
+
for (const [stateDir, watcher] of runDirWatchers) {
|
|
197
|
+
if (present.has(stateDir) && pathExists(stateDir))
|
|
110
198
|
continue;
|
|
111
|
-
|
|
199
|
+
removeRunWatcher(stateDir, watcher, { degraded: false });
|
|
112
200
|
}
|
|
201
|
+
for (const stateDir of stateDirs)
|
|
202
|
+
watchRunDir(stateDir);
|
|
113
203
|
}
|
|
114
|
-
return {
|
|
204
|
+
return {
|
|
205
|
+
close,
|
|
206
|
+
getDiagnostics: () => [...diagnostics],
|
|
207
|
+
refresh,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
export function createRunTerminalReconciliationLoop(input) {
|
|
211
|
+
const intervalMs = input.intervalMs ?? 10_000;
|
|
212
|
+
let interval;
|
|
213
|
+
const reconcileNow = () => {
|
|
214
|
+
try {
|
|
215
|
+
input.refreshWatcher();
|
|
216
|
+
input.reconcile();
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
input.onError?.(error);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const close = () => {
|
|
223
|
+
if (interval)
|
|
224
|
+
clearInterval(interval);
|
|
225
|
+
interval = undefined;
|
|
226
|
+
};
|
|
227
|
+
const start = () => {
|
|
228
|
+
close();
|
|
229
|
+
interval = setInterval(reconcileNow, intervalMs);
|
|
230
|
+
interval.unref?.();
|
|
231
|
+
};
|
|
232
|
+
return { close, reconcileNow, start };
|
|
115
233
|
}
|
|
116
234
|
const TERMINAL = new Set([
|
|
117
235
|
"done",
|
|
@@ -14,6 +14,11 @@ export declare function markRecipeContextRecords(records: TemplateRecipeContextR
|
|
|
14
14
|
export declare function formatRecipeContextJsonl(records: TemplateRecipeContextRecord[], context?: CommandTemplateActorRecipeContext): string;
|
|
15
15
|
export declare function buildRecipeContextPromptBlock(records: TemplateRecipeContextRecord[], context?: CommandTemplateActorRecipeContext): string;
|
|
16
16
|
export declare function appendRecipeContextToPiArgs(command: string, args: string[], records: TemplateRecipeContextRecord[] | undefined, context?: CommandTemplateActorRecipeContext): string[];
|
|
17
|
+
export interface ManagedPiSessionArgs {
|
|
18
|
+
args: string[];
|
|
19
|
+
sessionDir?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function attachPiSessionDir(command: string, args: string[], sessionDir: string): ManagedPiSessionArgs;
|
|
17
22
|
export interface MaterializedPiPrintPromptArgs {
|
|
18
23
|
args: string[];
|
|
19
24
|
promptBytes?: number;
|
|
@@ -134,6 +134,21 @@ export function appendRecipeContextToPiArgs(command, args, records, context) {
|
|
|
134
134
|
next[promptIndex] = `${next[promptIndex]}\n\n${block}`;
|
|
135
135
|
return next;
|
|
136
136
|
}
|
|
137
|
+
const PI_SESSION_POLICY_FLAGS = new Set([
|
|
138
|
+
"--fork",
|
|
139
|
+
"--no-session",
|
|
140
|
+
"--session",
|
|
141
|
+
"--session-dir",
|
|
142
|
+
"--session-id",
|
|
143
|
+
]);
|
|
144
|
+
export function attachPiSessionDir(command, args, sessionDir) {
|
|
145
|
+
if (!isPiCommand(command) || findPiPrintPromptIndex(args) === undefined) {
|
|
146
|
+
return { args };
|
|
147
|
+
}
|
|
148
|
+
if (args.some((arg) => PI_SESSION_POLICY_FLAGS.has(arg)))
|
|
149
|
+
return { args };
|
|
150
|
+
return { args: ["--session-dir", sessionDir, ...args], sessionDir };
|
|
151
|
+
}
|
|
137
152
|
export function materializePiPrintPromptArg(command, args, promptFile) {
|
|
138
153
|
if (!isPiCommand(command))
|
|
139
154
|
return { args };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persisted Pi session evidence reader.
|
|
3
|
+
* Zones: subagent turns, active session branches, bounded/redacted previews
|
|
4
|
+
* Owns read-only normalization of session JSONL into inspector-ready turns.
|
|
5
|
+
*/
|
|
6
|
+
import { type StateReadDiagnostic } from "./state-readers.ts";
|
|
7
|
+
export interface SessionEvidenceToolCall {
|
|
8
|
+
arguments?: unknown;
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
result?: unknown;
|
|
12
|
+
resultError?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface SessionEvidenceTurn {
|
|
15
|
+
assistantEntryId?: string;
|
|
16
|
+
assistantText?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
index: number;
|
|
19
|
+
model?: string;
|
|
20
|
+
provider?: string;
|
|
21
|
+
stopReason?: string;
|
|
22
|
+
thinking?: string;
|
|
23
|
+
timestamp?: string;
|
|
24
|
+
toolCalls: SessionEvidenceToolCall[];
|
|
25
|
+
unmatchedToolResults: number;
|
|
26
|
+
usage?: unknown;
|
|
27
|
+
userEntryId?: string;
|
|
28
|
+
userText?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface SessionEvidence {
|
|
31
|
+
activeLeafId?: string;
|
|
32
|
+
diagnostics: StateReadDiagnostic[];
|
|
33
|
+
path: string;
|
|
34
|
+
session?: Record<string, unknown>;
|
|
35
|
+
totalTurns: number;
|
|
36
|
+
truncated: boolean;
|
|
37
|
+
turns: SessionEvidenceTurn[];
|
|
38
|
+
}
|
|
39
|
+
export interface SessionEvidenceReadOptions {
|
|
40
|
+
maxTextChars?: number;
|
|
41
|
+
maxToolCalls?: number;
|
|
42
|
+
maxTurns?: number;
|
|
43
|
+
}
|
|
44
|
+
export declare function redactSessionEvidenceValue(value: unknown, maxTextChars?: number, seen?: WeakSet<object>): unknown;
|
|
45
|
+
export declare function readSessionEvidence(path: string, options?: SessionEvidenceReadOptions): SessionEvidence;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persisted Pi session evidence reader.
|
|
3
|
+
* Zones: subagent turns, active session branches, bounded/redacted previews
|
|
4
|
+
* Owns read-only normalization of session JSONL into inspector-ready turns.
|
|
5
|
+
*/
|
|
6
|
+
import * as Limits from "./limits.js";
|
|
7
|
+
import { readJsonlFileResilient, } from "./state-readers.js";
|
|
8
|
+
const SENSITIVE_KEY = /(?:^|[_-])(?:api[-_]?key|authorization|cookie|credential|password|private[-_]?key|secret|secret[-_]?access[-_]?key|token)$|^(?:access|refresh|auth|api)Token$|^(?:clientSecret|privateKey|secretAccessKey)$/i;
|
|
9
|
+
const SENSITIVE_TEXT = /(bearer\s+)[A-Za-z0-9._~+/=-]+|["']?\b(api[-_]?key|authorization|clientSecret|cookie|password|private[-_]?key|privateKey|secret|secretAccessKey|token)["']?(\s*[:=]\s*)["']?([^\s,;"'}]+)/gi;
|
|
10
|
+
function asRecord(value) {
|
|
11
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
12
|
+
? value
|
|
13
|
+
: {};
|
|
14
|
+
}
|
|
15
|
+
function boundedText(value, maxChars) {
|
|
16
|
+
if (typeof value !== "string")
|
|
17
|
+
return undefined;
|
|
18
|
+
if (/^\s*[\[{]/.test(value)) {
|
|
19
|
+
try {
|
|
20
|
+
const structured = JSON.stringify(redactSessionEvidenceValue(JSON.parse(value), maxChars));
|
|
21
|
+
return structured.length > maxChars
|
|
22
|
+
? `${structured.slice(0, Math.max(0, maxChars - 1))}…`
|
|
23
|
+
: structured;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Fall through to bounded text redaction.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const redacted = value.replaceAll(SENSITIVE_TEXT, (match, bearer, key, separator) => bearer ? `${bearer}[REDACTED]` : `${key}${separator}[REDACTED]`);
|
|
30
|
+
return redacted.length > maxChars
|
|
31
|
+
? `${redacted.slice(0, Math.max(0, maxChars - 1))}…`
|
|
32
|
+
: redacted;
|
|
33
|
+
}
|
|
34
|
+
export function redactSessionEvidenceValue(value, maxTextChars = Limits.SESSION_EVIDENCE_TEXT_CHARS, seen = new WeakSet()) {
|
|
35
|
+
if (typeof value === "string")
|
|
36
|
+
return boundedText(value, maxTextChars);
|
|
37
|
+
if (!value || typeof value !== "object")
|
|
38
|
+
return value;
|
|
39
|
+
if (seen.has(value))
|
|
40
|
+
return "[CIRCULAR]";
|
|
41
|
+
seen.add(value);
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
return value.map((item) => redactSessionEvidenceValue(item, maxTextChars, seen));
|
|
44
|
+
}
|
|
45
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
46
|
+
key,
|
|
47
|
+
SENSITIVE_KEY.test(key)
|
|
48
|
+
? "[REDACTED]"
|
|
49
|
+
: redactSessionEvidenceValue(item, maxTextChars, seen),
|
|
50
|
+
]));
|
|
51
|
+
}
|
|
52
|
+
function contentBlocks(message) {
|
|
53
|
+
return Array.isArray(message.content)
|
|
54
|
+
? message.content.map(asRecord)
|
|
55
|
+
: typeof message.content === "string"
|
|
56
|
+
? [{ type: "text", text: message.content }]
|
|
57
|
+
: [];
|
|
58
|
+
}
|
|
59
|
+
function contentText(message, type, maxChars) {
|
|
60
|
+
const text = contentBlocks(message)
|
|
61
|
+
.filter((block) => block.type === type && typeof block[type] === "string")
|
|
62
|
+
.map((block) => String(block[type]))
|
|
63
|
+
.join("\n");
|
|
64
|
+
return boundedText(text, maxChars);
|
|
65
|
+
}
|
|
66
|
+
function activeBranch(entries, path, diagnostics) {
|
|
67
|
+
const treeEntries = entries.filter((entry) => entry.type !== "session" && typeof entry.id === "string");
|
|
68
|
+
const leaf = treeEntries.at(-1);
|
|
69
|
+
if (!leaf || typeof leaf.id !== "string")
|
|
70
|
+
return [];
|
|
71
|
+
const byId = new Map(treeEntries.map((entry) => [String(entry.id), entry]));
|
|
72
|
+
const branch = [];
|
|
73
|
+
const visited = new Set();
|
|
74
|
+
let current = leaf;
|
|
75
|
+
while (current && typeof current.id === "string") {
|
|
76
|
+
if (visited.has(current.id)) {
|
|
77
|
+
diagnostics.push({ message: `session entry cycle at ${current.id}`, path });
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
visited.add(current.id);
|
|
81
|
+
branch.push(current);
|
|
82
|
+
if (current.parentId === null || current.parentId === undefined)
|
|
83
|
+
break;
|
|
84
|
+
if (typeof current.parentId !== "string" || !byId.has(current.parentId)) {
|
|
85
|
+
diagnostics.push({
|
|
86
|
+
message: `missing parent ${String(current.parentId)} for ${current.id}`,
|
|
87
|
+
path,
|
|
88
|
+
});
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
current = byId.get(current.parentId);
|
|
92
|
+
}
|
|
93
|
+
return branch.reverse();
|
|
94
|
+
}
|
|
95
|
+
function toolCalls(message, maxTextChars, maxToolCalls) {
|
|
96
|
+
return contentBlocks(message)
|
|
97
|
+
.filter((block) => block.type === "toolCall" &&
|
|
98
|
+
typeof block.id === "string" &&
|
|
99
|
+
typeof block.name === "string")
|
|
100
|
+
.slice(0, maxToolCalls)
|
|
101
|
+
.map((block) => ({
|
|
102
|
+
arguments: redactSessionEvidenceValue(block.arguments, maxTextChars),
|
|
103
|
+
id: String(block.id),
|
|
104
|
+
name: String(block.name),
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
export function readSessionEvidence(path, options = {}) {
|
|
108
|
+
const maxTextChars = Math.max(1, options.maxTextChars ?? Limits.SESSION_EVIDENCE_TEXT_CHARS);
|
|
109
|
+
const maxToolCalls = Math.max(1, options.maxToolCalls ?? Limits.SESSION_EVIDENCE_MAX_TOOL_CALLS);
|
|
110
|
+
const maxTurns = Math.max(1, options.maxTurns ?? Limits.SESSION_EVIDENCE_MAX_TURNS);
|
|
111
|
+
const read = readJsonlFileResilient(path);
|
|
112
|
+
const diagnostics = [...read.diagnostics];
|
|
113
|
+
const header = read.records.find((entry) => entry.type === "session");
|
|
114
|
+
const branch = activeBranch(read.records, path, diagnostics);
|
|
115
|
+
const turns = [];
|
|
116
|
+
let pendingUser;
|
|
117
|
+
let currentTurn;
|
|
118
|
+
for (const entry of branch) {
|
|
119
|
+
if (entry.type !== "message")
|
|
120
|
+
continue;
|
|
121
|
+
const message = asRecord(entry.message);
|
|
122
|
+
const role = message.role;
|
|
123
|
+
if (role === "user") {
|
|
124
|
+
pendingUser = {
|
|
125
|
+
id: String(entry.id),
|
|
126
|
+
text: contentText(message, "text", maxTextChars),
|
|
127
|
+
};
|
|
128
|
+
currentTurn = undefined;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (role === "assistant") {
|
|
132
|
+
currentTurn = {
|
|
133
|
+
...(typeof entry.id === "string" ? { assistantEntryId: entry.id } : {}),
|
|
134
|
+
...(contentText(message, "text", maxTextChars)
|
|
135
|
+
? { assistantText: contentText(message, "text", maxTextChars) }
|
|
136
|
+
: {}),
|
|
137
|
+
...(typeof message.errorMessage === "string"
|
|
138
|
+
? { error: boundedText(message.errorMessage, maxTextChars) }
|
|
139
|
+
: {}),
|
|
140
|
+
index: turns.length + 1,
|
|
141
|
+
...(typeof message.model === "string" ? { model: message.model } : {}),
|
|
142
|
+
...(typeof message.provider === "string"
|
|
143
|
+
? { provider: message.provider }
|
|
144
|
+
: {}),
|
|
145
|
+
...(typeof message.stopReason === "string"
|
|
146
|
+
? { stopReason: message.stopReason }
|
|
147
|
+
: {}),
|
|
148
|
+
...(contentText(message, "thinking", maxTextChars)
|
|
149
|
+
? { thinking: contentText(message, "thinking", maxTextChars) }
|
|
150
|
+
: {}),
|
|
151
|
+
...(typeof entry.timestamp === "string"
|
|
152
|
+
? { timestamp: entry.timestamp }
|
|
153
|
+
: {}),
|
|
154
|
+
toolCalls: toolCalls(message, maxTextChars, maxToolCalls),
|
|
155
|
+
unmatchedToolResults: 0,
|
|
156
|
+
...(message.usage !== undefined
|
|
157
|
+
? { usage: redactSessionEvidenceValue(message.usage, maxTextChars) }
|
|
158
|
+
: {}),
|
|
159
|
+
...(pendingUser
|
|
160
|
+
? {
|
|
161
|
+
userEntryId: pendingUser.id,
|
|
162
|
+
...(pendingUser.text ? { userText: pendingUser.text } : {}),
|
|
163
|
+
}
|
|
164
|
+
: {}),
|
|
165
|
+
};
|
|
166
|
+
pendingUser = undefined;
|
|
167
|
+
turns.push(currentTurn);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (role === "toolResult" && currentTurn) {
|
|
171
|
+
const callId = typeof message.toolCallId === "string" ? message.toolCallId : "";
|
|
172
|
+
const call = currentTurn.toolCalls.find((item) => item.id === callId);
|
|
173
|
+
if (!call) {
|
|
174
|
+
currentTurn.unmatchedToolResults += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
call.result = redactSessionEvidenceValue(message.content, maxTextChars);
|
|
178
|
+
call.resultError = message.isError === true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (pendingUser) {
|
|
182
|
+
turns.push({
|
|
183
|
+
index: turns.length + 1,
|
|
184
|
+
toolCalls: [],
|
|
185
|
+
unmatchedToolResults: 0,
|
|
186
|
+
userEntryId: pendingUser.id,
|
|
187
|
+
...(pendingUser.text ? { userText: pendingUser.text } : {}),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const firstVisibleIndex = Math.max(0, turns.length - maxTurns);
|
|
191
|
+
const visibleTurns = turns.slice(firstVisibleIndex).map((turn, index) => ({
|
|
192
|
+
...turn,
|
|
193
|
+
index: firstVisibleIndex + index + 1,
|
|
194
|
+
}));
|
|
195
|
+
return {
|
|
196
|
+
...(branch.at(-1)?.id ? { activeLeafId: String(branch.at(-1)?.id) } : {}),
|
|
197
|
+
diagnostics,
|
|
198
|
+
path,
|
|
199
|
+
...(header ? { session: asRecord(header) } : {}),
|
|
200
|
+
totalTurns: turns.length,
|
|
201
|
+
truncated: turns.length > visibleTurns.length,
|
|
202
|
+
turns: visibleTurns,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* Zones: compact text summaries, verbose JSON switching, next-action rendering
|
|
4
4
|
* Owns model-facing response helpers shared by public tool execution paths
|
|
5
5
|
*/
|
|
6
|
+
export declare function withLeadingLineBreak(text: string): string;
|
|
7
|
+
export declare function spaceToolResult<T>(result: T): T;
|
|
8
|
+
export declare function spaceToolError(error: unknown): unknown;
|
|
6
9
|
export declare function asRecord(value: unknown): Record<string, unknown>;
|
|
7
10
|
export declare function jsonText(value: unknown): string;
|
|
8
11
|
export declare function compactPreview(value: unknown, maxLength?: number): string | undefined;
|
|
@@ -5,6 +5,35 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as Limits from "./limits.js";
|
|
7
7
|
import * as ToolsMailbox from "./tools-mailbox.js";
|
|
8
|
+
export function withLeadingLineBreak(text) {
|
|
9
|
+
return `\n${text.replace(/^\n+/, "")}`;
|
|
10
|
+
}
|
|
11
|
+
export function spaceToolResult(result) {
|
|
12
|
+
if (!result || typeof result !== "object")
|
|
13
|
+
return result;
|
|
14
|
+
const content = result.content;
|
|
15
|
+
if (!Array.isArray(content))
|
|
16
|
+
return result;
|
|
17
|
+
return {
|
|
18
|
+
...result,
|
|
19
|
+
content: content.map((item) => item &&
|
|
20
|
+
typeof item === "object" &&
|
|
21
|
+
item.type === "text" &&
|
|
22
|
+
typeof item.text === "string"
|
|
23
|
+
? {
|
|
24
|
+
...item,
|
|
25
|
+
text: withLeadingLineBreak(item.text),
|
|
26
|
+
}
|
|
27
|
+
: item),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function spaceToolError(error) {
|
|
31
|
+
if (error instanceof Error) {
|
|
32
|
+
error.message = withLeadingLineBreak(error.message);
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
return new Error(withLeadingLineBreak(String(error)));
|
|
36
|
+
}
|
|
8
37
|
export function asRecord(value) {
|
|
9
38
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
10
39
|
? value
|