@llblab/pi-actors 0.40.1 → 0.41.0
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 +1 -1
- package/BACKLOG.md +19 -0
- package/CHANGELOG.md +25 -0
- package/README.md +2 -1
- package/dist/index.js +30 -51
- package/dist/lib/inspector-overlay.d.ts +84 -0
- package/dist/lib/inspector-overlay.js +627 -0
- package/dist/lib/inspector.d.ts +17 -33
- package/dist/lib/inspector.js +98 -151
- package/dist/lib/limits.d.ts +3 -0
- package/dist/lib/limits.js +3 -0
- 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 +31 -0
- package/dist/scripts/async-runner.mjs +40 -5
- package/dist/scripts/coordinator.mjs +55 -9
- package/dist/skills/actors/SKILL.md +5 -7
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/README.md +1 -0
- package/docs/actor-inspector.md +72 -0
- package/index.ts +32 -63
- package/lib/inspector-overlay.ts +735 -0
- package/lib/inspector.ts +132 -204
- package/lib/limits.ts +3 -0
- package/lib/recipes-context.ts +25 -0
- package/lib/session-evidence.ts +302 -0
- package/lib/tools-response.ts +33 -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 -7
- package/skills/swarm/SKILL.md +1 -1
package/dist/lib/inspector.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Zones: terminal run inspection, room/direct message previews, no-dependency UI formatting
|
|
4
4
|
* Owns read-only previews and TUI-ready summaries; actor routing stays outside.
|
|
5
5
|
*/
|
|
6
|
+
import * as SessionEvidence from "./session-evidence.ts";
|
|
6
7
|
export interface ActorInspectorPreview {
|
|
7
8
|
body_preview?: string;
|
|
8
9
|
branch?: string;
|
|
@@ -45,47 +46,30 @@ export interface ActorInspectorPreviewReadOptions {
|
|
|
45
46
|
ownerId?: string;
|
|
46
47
|
branch?: string;
|
|
47
48
|
currentRunOnly?: boolean;
|
|
49
|
+
run?: string;
|
|
48
50
|
channels?: ActorInspectorPreview["channel"][];
|
|
49
51
|
mention?: string;
|
|
50
52
|
readKeys?: Iterable<string>;
|
|
51
53
|
roomLimitPerRun?: number;
|
|
52
54
|
unreadOnly?: boolean;
|
|
53
55
|
}
|
|
54
|
-
export interface
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
readKeys: Set<string>;
|
|
59
|
-
roomLimitPerRun: number;
|
|
60
|
-
rows: number;
|
|
61
|
-
selectedSequence?: number;
|
|
62
|
-
unreadOnly: boolean;
|
|
63
|
-
visible: boolean;
|
|
56
|
+
export interface ActorInspectorRunItem {
|
|
57
|
+
run: string;
|
|
58
|
+
status: string;
|
|
59
|
+
updatedAt?: string;
|
|
64
60
|
}
|
|
65
|
-
export
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
export declare function readActorInspectorRuns(stateRoot: string, ownerId: string): ActorInspectorRunItem[];
|
|
62
|
+
export interface ActorInspectorTurnItem extends SessionEvidence.SessionEvidenceTurn {
|
|
63
|
+
commandId: string;
|
|
64
|
+
diagnostics: string[];
|
|
65
|
+
promptBytes?: number;
|
|
66
|
+
promptFile?: string;
|
|
67
|
+
recipeContext?: unknown;
|
|
68
|
+
sessionFile: string;
|
|
69
|
+
sessionTruncated: boolean;
|
|
70
|
+
stage?: string;
|
|
69
71
|
}
|
|
70
|
-
export declare
|
|
71
|
-
export declare const ACTOR_INSPECTOR_COMMAND_DESCRIPTIONS: {
|
|
72
|
-
filter: string;
|
|
73
|
-
inspect: string;
|
|
74
|
-
toggle: string;
|
|
75
|
-
};
|
|
76
|
-
export declare function createActorInspectorControllerState(): ActorInspectorControllerState;
|
|
77
|
-
export declare function getActorInspectorPreviewOptions(state: ActorInspectorControllerState, ownerId: string): ActorInspectorPreviewReadOptions;
|
|
78
|
-
export declare function handleActorInspectorToggle(state: ActorInspectorControllerState, args: unknown): ActorInspectorCommandResult;
|
|
79
|
-
export declare function handleActorInspectorFilter(state: ActorInspectorControllerState, args: unknown): ActorInspectorCommandResult;
|
|
80
|
-
export declare function handleActorInspectorInspect(state: ActorInspectorControllerState, args: unknown, previews: ActorInspectorPreview[]): ActorInspectorCommandResult;
|
|
81
|
-
export declare function selectActorInspectorSequence(state: ActorInspectorControllerState, sequence: number, previews: ActorInspectorPreview[]): ActorInspectorCommandResult;
|
|
82
|
-
export declare function renderActorInspectorPanel(input: {
|
|
83
|
-
stateRoot: string;
|
|
84
|
-
state: ActorInspectorControllerState;
|
|
85
|
-
ownerId: string;
|
|
86
|
-
width: number;
|
|
87
|
-
style: ActorInspectorWidgetStyle;
|
|
88
|
-
}): string[];
|
|
72
|
+
export declare function readActorInspectorTurns(stateDir: string): ActorInspectorTurnItem[];
|
|
89
73
|
export declare function inspectorPreviewReadKey(preview: ActorInspectorPreview): string;
|
|
90
74
|
export declare function readActorInspectorPreviews(stateRoot?: string, limit?: number, options?: ActorInspectorPreviewReadOptions): ActorInspectorPreview[];
|
|
91
75
|
export declare function readActorInspectorRoster(stateRoot: string | undefined, run: string, room?: string): ActorInspectorRosterMember[];
|
package/dist/lib/inspector.js
CHANGED
|
@@ -8,164 +8,108 @@ import * as path from "node:path";
|
|
|
8
8
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import * as Limits from "./limits.js";
|
|
10
10
|
import * as Paths from "./paths.js";
|
|
11
|
+
import * as SessionEvidence from "./session-evidence.js";
|
|
11
12
|
import { readJsonFileResilient, readJsonlFileResilient, } from "./state-readers.js";
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
readKeys: state.readKeys,
|
|
35
|
-
roomLimitPerRun: state.roomLimitPerRun,
|
|
36
|
-
unreadOnly: state.unreadOnly,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
export function handleActorInspectorToggle(state, args) {
|
|
40
|
-
const raw = Array.isArray(args) ? args[0] : String(args ?? "");
|
|
41
|
-
if (String(raw).trim()) {
|
|
42
|
-
const rows = Number.parseInt(String(raw), 10);
|
|
43
|
-
if (!Number.isFinite(rows) || rows <= 0) {
|
|
44
|
-
return {
|
|
45
|
-
notify: "Usage: /actors-inspector-toggle [rows] where rows > 0",
|
|
46
|
-
type: "warning",
|
|
47
|
-
update: false,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
state.rows = rows;
|
|
51
|
-
state.roomLimitPerRun = rows;
|
|
52
|
-
state.selectedSequence = undefined;
|
|
53
|
-
state.visible = true;
|
|
54
|
-
return {
|
|
55
|
-
notify: `Actor inspector rows ${rows}`,
|
|
56
|
-
type: "info",
|
|
57
|
-
update: true,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
if (state.selectedSequence !== undefined) {
|
|
61
|
-
state.selectedSequence = undefined;
|
|
62
|
-
state.visible = true;
|
|
63
|
-
return { notify: "Actor inspector table", type: "info", update: true };
|
|
13
|
+
export function readActorInspectorRuns(stateRoot, ownerId) {
|
|
14
|
+
try {
|
|
15
|
+
return fs
|
|
16
|
+
.readdirSync(stateRoot, { withFileTypes: true })
|
|
17
|
+
.filter((entry) => entry.isDirectory())
|
|
18
|
+
.flatMap((entry) => {
|
|
19
|
+
const stateDir = path.join(stateRoot, entry.name);
|
|
20
|
+
if (!matchesOwner(stateDir, ownerId))
|
|
21
|
+
return [];
|
|
22
|
+
const progress = readJsonFileResilient(path.join(stateDir, "progress.json"), {}).value;
|
|
23
|
+
const result = readJsonFileResilient(path.join(stateDir, "result.json"), {}).value;
|
|
24
|
+
return [{
|
|
25
|
+
run: entry.name,
|
|
26
|
+
status: String(progress.phase ?? (Object.keys(result).length ? "terminal" : "unknown")),
|
|
27
|
+
...(typeof progress.updatedAt === "string"
|
|
28
|
+
? { updatedAt: progress.updatedAt }
|
|
29
|
+
: typeof result.completedAt === "string"
|
|
30
|
+
? { updatedAt: result.completedAt }
|
|
31
|
+
: {}),
|
|
32
|
+
}];
|
|
33
|
+
})
|
|
34
|
+
.sort((left, right) => String(left.updatedAt ?? "").localeCompare(String(right.updatedAt ?? "")));
|
|
64
35
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
state.roomLimitPerRun = DEFAULT_ACTOR_INSPECTOR_ROWS;
|
|
70
|
-
state.visible = true;
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code === "ENOENT")
|
|
38
|
+
return [];
|
|
39
|
+
return [];
|
|
71
40
|
}
|
|
72
|
-
return {
|
|
73
|
-
notify: `Actor inspector ${state.visible ? "shown" : "hidden"}`,
|
|
74
|
-
type: "info",
|
|
75
|
-
update: true,
|
|
76
|
-
};
|
|
77
41
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
if (!
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
state.mention = undefined;
|
|
92
|
-
}
|
|
93
|
-
else if (mode === "unread") {
|
|
94
|
-
state.unreadOnly = true;
|
|
95
|
-
}
|
|
96
|
-
else if (mode === "branch" || mode === "current-branch") {
|
|
97
|
-
const branch = parts.slice(1).join(" ").trim();
|
|
98
|
-
if (!branch) {
|
|
99
|
-
return {
|
|
100
|
-
notify: `Usage: /actors-inspector-filter ${mode} <branch-name>`,
|
|
101
|
-
type: "warning",
|
|
102
|
-
update: false,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
state.branch = branch;
|
|
106
|
-
}
|
|
107
|
-
else if (mode === "mention") {
|
|
108
|
-
const mention = parts.slice(1).join(" ").trim();
|
|
109
|
-
if (!mention) {
|
|
110
|
-
return {
|
|
111
|
-
notify: "Usage: /actors-inspector-filter mention <text>",
|
|
112
|
-
type: "warning",
|
|
113
|
-
update: false,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
state.channels = undefined;
|
|
117
|
-
state.mention = mention;
|
|
42
|
+
function ownedSessionPath(stateDir, sessionFile) {
|
|
43
|
+
if (path.isAbsolute(sessionFile))
|
|
44
|
+
return undefined;
|
|
45
|
+
const sessionsRoot = path.resolve(stateDir, "sessions");
|
|
46
|
+
const resolved = path.resolve(stateDir, sessionFile);
|
|
47
|
+
if (!resolved.startsWith(`${sessionsRoot}${path.sep}`))
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
const canonicalRoot = fs.realpathSync(sessionsRoot);
|
|
51
|
+
const canonicalFile = fs.realpathSync(resolved);
|
|
52
|
+
return canonicalFile.startsWith(`${canonicalRoot}${path.sep}`)
|
|
53
|
+
? canonicalFile
|
|
54
|
+
: undefined;
|
|
118
55
|
}
|
|
119
|
-
|
|
120
|
-
return
|
|
121
|
-
notify: "Usage: /actors-inspector-filter all|room|direct|broadcast|unread|branch <name>|mention <text>",
|
|
122
|
-
type: "warning",
|
|
123
|
-
update: false,
|
|
124
|
-
};
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
125
58
|
}
|
|
126
|
-
state.selectedSequence = undefined;
|
|
127
|
-
state.visible = true;
|
|
128
|
-
return {
|
|
129
|
-
notify: `Actor inspector filter ${mode || "all"}`,
|
|
130
|
-
type: "info",
|
|
131
|
-
update: true,
|
|
132
|
-
};
|
|
133
59
|
}
|
|
134
|
-
export function
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
60
|
+
export function readActorInspectorTurns(stateDir) {
|
|
61
|
+
const evidence = readJsonFileResilient(path.join(stateDir, "review-evidence.json"), {}).value;
|
|
62
|
+
const commands = Array.isArray(evidence.commands)
|
|
63
|
+
? evidence.commands
|
|
64
|
+
: [];
|
|
65
|
+
const recordedFiles = new Set(commands.flatMap((command) => (Array.isArray(command.session_files) ? command.session_files : []).filter((file) => typeof file === "string")));
|
|
66
|
+
let discoveredCommands = [];
|
|
67
|
+
try {
|
|
68
|
+
const sessionsDir = path.join(stateDir, "sessions");
|
|
69
|
+
discoveredCommands = fs
|
|
70
|
+
.readdirSync(sessionsDir, { withFileTypes: true })
|
|
71
|
+
.filter((entry) => entry.isDirectory())
|
|
72
|
+
.flatMap((entry) => {
|
|
73
|
+
const files = fs
|
|
74
|
+
.readdirSync(path.join(sessionsDir, entry.name), {
|
|
75
|
+
withFileTypes: true,
|
|
76
|
+
})
|
|
77
|
+
.filter((file) => file.isFile() && file.name.endsWith(".jsonl"))
|
|
78
|
+
.map((file) => path.join("sessions", entry.name, file.name))
|
|
79
|
+
.filter((file) => !recordedFiles.has(file));
|
|
80
|
+
return files.length > 0
|
|
81
|
+
? [{ id: entry.name, session_files: files, stage: "subagent" }]
|
|
82
|
+
: [];
|
|
83
|
+
});
|
|
145
84
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
85
|
+
catch {
|
|
86
|
+
discoveredCommands = [];
|
|
87
|
+
}
|
|
88
|
+
return [...commands, ...discoveredCommands].flatMap((command) => (Array.isArray(command.session_files) ? command.session_files : [])
|
|
89
|
+
.filter((file) => typeof file === "string")
|
|
90
|
+
.flatMap((file) => {
|
|
91
|
+
const sessionPath = ownedSessionPath(stateDir, file);
|
|
92
|
+
if (!sessionPath)
|
|
93
|
+
return [];
|
|
94
|
+
const session = SessionEvidence.readSessionEvidence(sessionPath);
|
|
95
|
+
return session.turns.map((turn) => ({
|
|
96
|
+
...turn,
|
|
97
|
+
commandId: String(command.id ?? "unknown"),
|
|
98
|
+
diagnostics: session.diagnostics.map((item) => `${item.line === undefined ? "" : `line ${item.line}: `}${item.message}`),
|
|
99
|
+
...(typeof command.prompt_bytes === "number"
|
|
100
|
+
? { promptBytes: command.prompt_bytes }
|
|
101
|
+
: {}),
|
|
102
|
+
...(typeof command.prompt_file === "string"
|
|
103
|
+
? { promptFile: command.prompt_file }
|
|
104
|
+
: {}),
|
|
105
|
+
...(command.recipe_context !== undefined
|
|
106
|
+
? { recipeContext: command.recipe_context }
|
|
107
|
+
: {}),
|
|
108
|
+
sessionFile: file,
|
|
109
|
+
sessionTruncated: session.truncated,
|
|
110
|
+
...(typeof command.stage === "string" ? { stage: command.stage } : {}),
|
|
111
|
+
}));
|
|
112
|
+
})).map((turn, index) => ({ ...turn, index: index + 1 }));
|
|
169
113
|
}
|
|
170
114
|
function asRecord(value) {
|
|
171
115
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -178,7 +122,8 @@ function readJsonLines(file) {
|
|
|
178
122
|
function previewValue(value, maxLength = Limits.INSPECTOR_BODY_PREVIEW_CHARS) {
|
|
179
123
|
if (value === undefined)
|
|
180
124
|
return undefined;
|
|
181
|
-
const
|
|
125
|
+
const redacted = SessionEvidence.redactSessionEvidenceValue(value);
|
|
126
|
+
const text = typeof redacted === "string" ? redacted : JSON.stringify(redacted);
|
|
182
127
|
const compact = text.replaceAll(/\s+/g, " ").trim();
|
|
183
128
|
if (!compact)
|
|
184
129
|
return undefined;
|
|
@@ -393,6 +338,8 @@ export function readActorInspectorPreviews(stateRoot = Paths.getRunStateRoot(),
|
|
|
393
338
|
const stateDir = path.join(stateRoot, entry.name);
|
|
394
339
|
if (!matchesOwner(stateDir, options.ownerId))
|
|
395
340
|
return [];
|
|
341
|
+
if (options.run && entry.name !== options.run)
|
|
342
|
+
return [];
|
|
396
343
|
return [
|
|
397
344
|
...readRoomPreviews(entry.name, stateDir),
|
|
398
345
|
...readInboxPreviews(entry.name, stateDir),
|
package/dist/lib/limits.d.ts
CHANGED
|
@@ -9,3 +9,6 @@ export declare const COMPACT_PREVIEW_CHARS = 160;
|
|
|
9
9
|
export declare const INSPECTOR_BODY_PREVIEW_CHARS = 320;
|
|
10
10
|
export declare const ROOM_MESSAGE_PREVIEW_CHARS = 320;
|
|
11
11
|
export declare const DOCTOR_ACTION_PREVIEW_CHARS = 72;
|
|
12
|
+
export declare const SESSION_EVIDENCE_MAX_TURNS = 100;
|
|
13
|
+
export declare const SESSION_EVIDENCE_TEXT_CHARS = 4000;
|
|
14
|
+
export declare const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
|
package/dist/lib/limits.js
CHANGED
|
@@ -9,3 +9,6 @@ export const COMPACT_PREVIEW_CHARS = 160;
|
|
|
9
9
|
export const INSPECTOR_BODY_PREVIEW_CHARS = 320;
|
|
10
10
|
export const ROOM_MESSAGE_PREVIEW_CHARS = 320;
|
|
11
11
|
export const DOCTOR_ACTION_PREVIEW_CHARS = 72;
|
|
12
|
+
export const SESSION_EVIDENCE_MAX_TURNS = 100;
|
|
13
|
+
export const SESSION_EVIDENCE_TEXT_CHARS = 4_000;
|
|
14
|
+
export const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
|
|
@@ -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 withLeadingBlankLine(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;
|