@mrclrchtr/supi-debug 11.0.0 → 11.1.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/README.md +62 -3
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +41 -1
- package/package.json +4 -2
- package/skills/gather-retrospectives/SKILL.md +72 -0
- package/skills/gather-retrospectives/scripts/collect.mjs +399 -0
- package/skills/gather-retrospectives/scripts/snapshot.mjs +183 -0
- package/src/command.ts +4 -2
- package/src/config.ts +9 -4
- package/src/debug.ts +13 -6
- package/src/format-events.ts +28 -11
- package/src/render-details.ts +26 -3
- package/src/renderer.ts +29 -1
- package/src/session-events.ts +20 -6
- package/src/tool/debug/input.ts +11 -0
- package/src/tool/debug/render.ts +19 -5
- package/src/tool/debug/result-schema.ts +71 -0
- package/src/tool/debug/result.ts +101 -17
- package/src/tool/debug/serialize.ts +66 -0
- package/src/tool/debug/spec.ts +8 -12
package/src/debug.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { registerContextProvider } from "@mrclrchtr/supi-core/context";
|
|
|
5
5
|
import {
|
|
6
6
|
clearDebugEvents,
|
|
7
7
|
getDebugSummary,
|
|
8
|
-
|
|
8
|
+
subscribeDebugPersistence,
|
|
9
9
|
} from "@mrclrchtr/supi-core/debug";
|
|
10
10
|
import { defineConfigSettings, registerSettings } from "@mrclrchtr/supi-core/settings";
|
|
11
11
|
import { registerDebugCommand } from "./command.ts";
|
|
@@ -52,6 +52,14 @@ function registerDebugSettings(pi: ExtensionAPI): void {
|
|
|
52
52
|
description: "Maximum session-local debug events retained in memory",
|
|
53
53
|
values: ["50", "100", "250", "500"],
|
|
54
54
|
},
|
|
55
|
+
{
|
|
56
|
+
kind: "boolean" as const,
|
|
57
|
+
// biome-ignore lint/security/noSecrets: Configuration field name, not a credential.
|
|
58
|
+
key: "agentRunRetrospectivesEnabled",
|
|
59
|
+
label: "Agent Run Retrospectives",
|
|
60
|
+
description:
|
|
61
|
+
"Allow completed Agent Runs to request a retrospective when Debug logging is active",
|
|
62
|
+
},
|
|
55
63
|
],
|
|
56
64
|
afterPersist: ({ cwd }) => {
|
|
57
65
|
syncLiveDebugRegistry(cwd);
|
|
@@ -79,7 +87,7 @@ export default function debugExtension(pi: ExtensionAPI) {
|
|
|
79
87
|
applyDebugConfig(process.cwd());
|
|
80
88
|
registerDebugSettings(pi);
|
|
81
89
|
registerDebugMessageRenderer(pi);
|
|
82
|
-
const
|
|
90
|
+
const unsubscribeDebugPersistence = subscribeDebugPersistence((event) => {
|
|
83
91
|
pi.appendEntry(DEBUG_EVENT_ENTRY_TYPE, event);
|
|
84
92
|
});
|
|
85
93
|
|
|
@@ -96,13 +104,12 @@ export default function debugExtension(pi: ExtensionAPI) {
|
|
|
96
104
|
|
|
97
105
|
pi.on("resources_discover", async (_event, ctx) => {
|
|
98
106
|
maybeLogLoadStatus(pi, ctx.cwd, "resources_discover");
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
return { promptPaths: [join(baseDir, "prompts")] };
|
|
107
|
+
// Standalone installs and workspace-root loads expose the same resources.
|
|
108
|
+
return { promptPaths: [join(baseDir, "prompts")], skillPaths: [join(baseDir, "skills")] };
|
|
102
109
|
});
|
|
103
110
|
|
|
104
111
|
pi.on("session_shutdown", () => {
|
|
105
|
-
|
|
112
|
+
unsubscribeDebugPersistence();
|
|
106
113
|
});
|
|
107
114
|
|
|
108
115
|
registerDebugCommand(pi, applyDebugConfig, normalizeMaxEvents);
|
package/src/format-events.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { DebugEventView } from "@mrclrchtr/supi-core/debug";
|
|
9
9
|
import { formatDataLines } from "./format.ts";
|
|
10
|
+
import type { SessionDebugEvents } from "./session-events.ts";
|
|
10
11
|
|
|
11
12
|
const TRUNCATION_RESERVE_LINES = 2;
|
|
12
13
|
const TRUNCATION_RESERVE_BYTES = 512;
|
|
@@ -27,22 +28,28 @@ function formatTimestamp(timestamp: number): string {
|
|
|
27
28
|
return Number.isNaN(date.getTime()) ? String(timestamp) : date.toISOString();
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
/**
|
|
31
|
+
/** Session-wide scan counts and empty-selection context for both debug surfaces. */
|
|
32
|
+
export type DebugEventFormatOptions = Partial<
|
|
33
|
+
Pick<SessionDebugEvents, "persistedEventCount" | "invalidEventCount" | "malformedLineCount">
|
|
34
|
+
> & { queryLimitExcludedAll?: boolean };
|
|
35
|
+
|
|
36
|
+
function formatEmptySelection(options: DebugEventFormatOptions): string {
|
|
37
|
+
if (options.queryLimitExcludedAll) {
|
|
38
|
+
return "No events selected: the query limit excludes all matching events.";
|
|
39
|
+
}
|
|
40
|
+
return options.persistedEventCount === 0 && !options.malformedLineCount
|
|
41
|
+
? "This session has no persisted debug events; sessions recorded before persistence cannot be backfilled."
|
|
42
|
+
: "No matching debug events available.";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Format debug events and access/integrity warnings, including empty selections. */
|
|
31
46
|
export function formatDebugEvents(
|
|
32
47
|
events: readonly DebugEventView[],
|
|
33
48
|
rawAccessDenied: boolean,
|
|
34
49
|
rawDataUnavailable = false,
|
|
35
|
-
|
|
50
|
+
options: DebugEventFormatOptions = {},
|
|
36
51
|
): string[] {
|
|
37
|
-
|
|
38
|
-
return persistedEventCount === 0
|
|
39
|
-
? [
|
|
40
|
-
"This session has no persisted debug events; sessions recorded before persistence cannot be backfilled.",
|
|
41
|
-
]
|
|
42
|
-
: ["No matching debug events available."];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const lines: string[] = [];
|
|
52
|
+
const lines: string[] = events.length === 0 ? [formatEmptySelection(options)] : [];
|
|
46
53
|
for (const event of events) {
|
|
47
54
|
lines.push(
|
|
48
55
|
`[${formatTimestamp(event.timestamp)}] ${event.level.toUpperCase()} ${event.source}/${event.category}: ${event.message}`,
|
|
@@ -52,6 +59,16 @@ export function formatDebugEvents(
|
|
|
52
59
|
pushFormattedData(lines, "data", event.data);
|
|
53
60
|
pushFormattedData(lines, "rawData", event.rawData);
|
|
54
61
|
}
|
|
62
|
+
if (options.invalidEventCount) {
|
|
63
|
+
lines.unshift(
|
|
64
|
+
`Skipped ${options.invalidEventCount} invalid persisted debug entries; evidence is incomplete.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (options.malformedLineCount) {
|
|
68
|
+
lines.unshift(
|
|
69
|
+
`Skipped ${options.malformedLineCount} malformed JSONL line${options.malformedLineCount === 1 ? "" : "s"}; evidence is incomplete.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
55
72
|
if (rawDataUnavailable) {
|
|
56
73
|
lines.push("", "Raw debug data is not persisted for historical sessions.");
|
|
57
74
|
} else if (rawAccessDenied) {
|
package/src/render-details.ts
CHANGED
|
@@ -33,10 +33,16 @@ export interface DebugRenderDetails {
|
|
|
33
33
|
events: DebugRenderEvent[];
|
|
34
34
|
eventCount: number;
|
|
35
35
|
omittedEventCount: number;
|
|
36
|
+
/** Matching events excluded by the query limit, separate from transcript omissions. */
|
|
37
|
+
queryOmittedEventCount?: number;
|
|
38
|
+
/** Recognized persisted entries with invalid event payloads. */
|
|
39
|
+
invalidEventCount?: number;
|
|
40
|
+
/** Nonblank session lines that could not be parsed. */
|
|
41
|
+
malformedLineCount?: number;
|
|
36
42
|
eventDataTruncated: boolean;
|
|
37
43
|
persistedEventCount?: number;
|
|
38
44
|
truncation?: DebugOutputTruncation;
|
|
39
|
-
emptyReason?: "no-persisted-events" | "no-matches";
|
|
45
|
+
emptyReason?: "no-persisted-events" | "no-matches" | "query-limit";
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
interface DetailValueState {
|
|
@@ -55,7 +61,10 @@ interface CreateDebugRenderDetailsOptions {
|
|
|
55
61
|
persistedEventCount?: number;
|
|
56
62
|
truncation?: TruncationResult;
|
|
57
63
|
eventCount?: number;
|
|
58
|
-
|
|
64
|
+
queryOmittedEventCount?: number;
|
|
65
|
+
invalidEventCount?: number;
|
|
66
|
+
malformedLineCount?: number;
|
|
67
|
+
emptyReason?: "no-persisted-events" | "no-matches" | "query-limit";
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
function boundedText(value: string, maxChars: number): { value: string; truncated: boolean } {
|
|
@@ -242,6 +251,15 @@ export function createDebugRenderDetails(
|
|
|
242
251
|
events: boundedEvents.map((entry) => entry.event),
|
|
243
252
|
eventCount,
|
|
244
253
|
omittedEventCount: Math.max(0, eventCount - boundedEvents.length),
|
|
254
|
+
...(options.queryOmittedEventCount === undefined
|
|
255
|
+
? {}
|
|
256
|
+
: { queryOmittedEventCount: boundedCount(options.queryOmittedEventCount, 0) }),
|
|
257
|
+
...(options.invalidEventCount === undefined
|
|
258
|
+
? {}
|
|
259
|
+
: { invalidEventCount: boundedCount(options.invalidEventCount, 0) }),
|
|
260
|
+
...(options.malformedLineCount === undefined
|
|
261
|
+
? {}
|
|
262
|
+
: { malformedLineCount: boundedCount(options.malformedLineCount, 0) }),
|
|
245
263
|
eventDataTruncated,
|
|
246
264
|
...(options.persistedEventCount === undefined
|
|
247
265
|
? {}
|
|
@@ -317,12 +335,17 @@ export function readDebugRenderDetails(value: unknown): DebugRenderDetails {
|
|
|
317
335
|
rawAccessDenied: record.rawAccessDenied === true,
|
|
318
336
|
rawDataUnavailable: record.rawDataUnavailable === true,
|
|
319
337
|
emptyReason:
|
|
320
|
-
record.emptyReason === "no-persisted-events" ||
|
|
338
|
+
record.emptyReason === "no-persisted-events" ||
|
|
339
|
+
record.emptyReason === "no-matches" ||
|
|
340
|
+
record.emptyReason === "query-limit"
|
|
321
341
|
? record.emptyReason
|
|
322
342
|
: undefined,
|
|
323
343
|
persistedEventCount:
|
|
324
344
|
typeof record.persistedEventCount === "number" ? record.persistedEventCount : undefined,
|
|
325
345
|
eventCount: boundedCount(record.eventCount, events.length),
|
|
346
|
+
queryOmittedEventCount: boundedCount(record.queryOmittedEventCount, 0),
|
|
347
|
+
invalidEventCount: boundedCount(record.invalidEventCount, 0),
|
|
348
|
+
malformedLineCount: boundedCount(record.malformedLineCount, 0),
|
|
326
349
|
});
|
|
327
350
|
|
|
328
351
|
const parsedTruncation = parseTruncation(record.truncation);
|
package/src/renderer.ts
CHANGED
|
@@ -66,6 +66,30 @@ function formatTruncation(truncation: DebugRenderDetails["truncation"]): string
|
|
|
66
66
|
|
|
67
67
|
function buildDetailNotes(details: DebugRenderDetails, theme: Theme): string[] {
|
|
68
68
|
const notes: string[] = [];
|
|
69
|
+
if (details.queryOmittedEventCount) {
|
|
70
|
+
notes.push(
|
|
71
|
+
theme.fg(
|
|
72
|
+
"warning",
|
|
73
|
+
`${details.queryOmittedEventCount} matching events excluded by the query limit.`,
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (details.invalidEventCount) {
|
|
78
|
+
notes.push(
|
|
79
|
+
theme.fg(
|
|
80
|
+
"warning",
|
|
81
|
+
`${details.invalidEventCount} invalid persisted debug entries skipped; evidence is incomplete.`,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (details.malformedLineCount) {
|
|
86
|
+
notes.push(
|
|
87
|
+
theme.fg(
|
|
88
|
+
"warning",
|
|
89
|
+
`${details.malformedLineCount} malformed JSONL lines skipped; evidence is incomplete.`,
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
69
93
|
if (details.omittedEventCount > 0) {
|
|
70
94
|
notes.push(
|
|
71
95
|
theme.fg(
|
|
@@ -104,7 +128,9 @@ function buildEventLines(details: DebugRenderDetails, theme: Theme): string[] {
|
|
|
104
128
|
"muted",
|
|
105
129
|
details.emptyReason === "no-persisted-events"
|
|
106
130
|
? "This session has no persisted debug events."
|
|
107
|
-
:
|
|
131
|
+
: details.emptyReason === "query-limit"
|
|
132
|
+
? "No events selected by limit."
|
|
133
|
+
: "No matching debug events available.",
|
|
108
134
|
),
|
|
109
135
|
);
|
|
110
136
|
}
|
|
@@ -141,6 +167,8 @@ function renderCollapsedMessage(details: DebugRenderDetails, theme: Theme): Text
|
|
|
141
167
|
let summary = `${details.eventCount} event${details.eventCount === 1 ? "" : "s"} — ${first?.source ?? "debug"}/${first?.category ?? "event"}`;
|
|
142
168
|
if (details.eventCount > 1) summary += ` +${details.eventCount - 1} more`;
|
|
143
169
|
if (details.omittedEventCount > 0) summary += ` · ${details.omittedEventCount} omitted`;
|
|
170
|
+
if (details.invalidEventCount) summary += ` · ${details.invalidEventCount} invalid entries`;
|
|
171
|
+
if (details.malformedLineCount) summary += ` · ${details.malformedLineCount} malformed lines`;
|
|
144
172
|
if (details.eventDataTruncated) summary += " · data bounded";
|
|
145
173
|
if (details.truncation?.truncated) summary += " · output truncated";
|
|
146
174
|
if (details.rawDataUnavailable || details.rawAccessDenied) summary += " · raw unavailable";
|
package/src/session-events.ts
CHANGED
|
@@ -18,10 +18,16 @@ type PersistedDebugEventQuery = Pick<
|
|
|
18
18
|
"operationId" | "source" | "level" | "category" | "limit"
|
|
19
19
|
>;
|
|
20
20
|
|
|
21
|
-
/** Sanitized
|
|
21
|
+
/** Sanitized selection and scan totals from one PI session file. */
|
|
22
22
|
export interface SessionDebugEvents {
|
|
23
23
|
events: DebugEventView[];
|
|
24
24
|
persistedEventCount: number;
|
|
25
|
+
/** Valid matching events before the query limit is applied. */
|
|
26
|
+
matchedEventCount: number;
|
|
27
|
+
/** Recognized debug entries whose event payload could not be validated. */
|
|
28
|
+
invalidEventCount: number;
|
|
29
|
+
/** Nonblank JSONL lines that could not be parsed; their event identity is unknown. */
|
|
30
|
+
malformedLineCount: number;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/** Small progress facts emitted while a persisted session file is scanned. */
|
|
@@ -68,12 +74,13 @@ function parsePersistedEvent(data: unknown): DebugEventView | undefined {
|
|
|
68
74
|
};
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
function
|
|
72
|
-
if (!line.
|
|
77
|
+
function parseSessionLine(line: string): { entry?: unknown; malformed: boolean } {
|
|
78
|
+
if (!line.trim()) return { malformed: false };
|
|
73
79
|
try {
|
|
74
|
-
return JSON.parse(line);
|
|
80
|
+
return { entry: JSON.parse(line), malformed: false };
|
|
75
81
|
} catch {
|
|
76
|
-
|
|
82
|
+
// Do not retain line contents: damaged rows can contain unsanitized session data.
|
|
83
|
+
return { malformed: true };
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
|
|
@@ -99,6 +106,8 @@ export async function readSessionDebugEvents(
|
|
|
99
106
|
): Promise<SessionDebugEvents> {
|
|
100
107
|
const events: DebugEventView[] = [];
|
|
101
108
|
let persistedEventCount = 0;
|
|
109
|
+
let invalidEventCount = 0;
|
|
110
|
+
let malformedLineCount = 0;
|
|
102
111
|
let scannedLines = 0;
|
|
103
112
|
throwIfAborted(options.signal);
|
|
104
113
|
|
|
@@ -117,7 +126,8 @@ export async function readSessionDebugEvents(
|
|
|
117
126
|
for await (const line of lines) {
|
|
118
127
|
scannedLines++;
|
|
119
128
|
throwIfAborted(options.signal);
|
|
120
|
-
const entry =
|
|
129
|
+
const { entry, malformed } = parseSessionLine(line);
|
|
130
|
+
malformedLineCount += Number(malformed);
|
|
121
131
|
if (
|
|
122
132
|
typeof entry !== "object" ||
|
|
123
133
|
entry === null ||
|
|
@@ -136,6 +146,7 @@ export async function readSessionDebugEvents(
|
|
|
136
146
|
|
|
137
147
|
persistedEventCount++;
|
|
138
148
|
const event = parsePersistedEvent((entry as Record<string, unknown>).data);
|
|
149
|
+
invalidEventCount += Number(event === undefined);
|
|
139
150
|
if (event && matchesDebugEventQuery(event, query)) events.push(event);
|
|
140
151
|
|
|
141
152
|
if (scannedLines % 250 === 0) {
|
|
@@ -164,5 +175,8 @@ export async function readSessionDebugEvents(
|
|
|
164
175
|
return {
|
|
165
176
|
events: events.sort((a, b) => b.timestamp - a.timestamp).slice(0, limit),
|
|
166
177
|
persistedEventCount,
|
|
178
|
+
matchedEventCount: events.length,
|
|
179
|
+
invalidEventCount,
|
|
180
|
+
malformedLineCount,
|
|
167
181
|
};
|
|
168
182
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { DebugLevel } from "@mrclrchtr/supi-core/debug";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
|
|
5
|
+
/** Shared Debug Operation ID constraint for filters and returned event identity. */
|
|
6
|
+
export const debugOperationIdSchema = Type.String({ pattern: "^op-[A-Za-z0-9_-]{21}[AQgw]$" });
|
|
7
|
+
|
|
8
|
+
/** Shared level vocabulary; StringEnum's string type is narrowed to the registry contract. */
|
|
9
|
+
export const debugLevelSchema = Type.Unsafe<DebugLevel>(
|
|
10
|
+
StringEnum(["debug", "info", "warning", "error"]),
|
|
11
|
+
);
|
package/src/tool/debug/render.ts
CHANGED
|
@@ -53,15 +53,29 @@ export function renderDebugToolCall(args: unknown, theme: Theme): Text {
|
|
|
53
53
|
return new Text(content, 0, 0);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function formatEmptySummary(details: DebugRenderDetails): string {
|
|
57
|
+
const empty =
|
|
58
|
+
details.emptyReason === "no-persisted-events"
|
|
59
|
+
? "No persisted debug events"
|
|
60
|
+
: details.emptyReason === "query-limit"
|
|
61
|
+
? "No events selected by limit"
|
|
62
|
+
: "No matching debug events";
|
|
63
|
+
const notes = [empty];
|
|
64
|
+
if (details.invalidEventCount) notes.push("invalid entries skipped");
|
|
65
|
+
if (details.malformedLineCount) notes.push(`${details.malformedLineCount} malformed lines`);
|
|
66
|
+
if (details.rawAccessDenied || details.rawDataUnavailable) notes.push("raw unavailable");
|
|
67
|
+
return notes.join(" · ");
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
function renderToolSummary(details: DebugRenderDetails, theme: Theme): string {
|
|
57
|
-
if (details.eventCount === 0)
|
|
58
|
-
return details.emptyReason === "no-persisted-events"
|
|
59
|
-
? theme.fg("muted", "No persisted debug events")
|
|
60
|
-
: theme.fg("muted", "No matching debug events");
|
|
61
|
-
}
|
|
71
|
+
if (details.eventCount === 0) return theme.fg("muted", formatEmptySummary(details));
|
|
62
72
|
|
|
63
73
|
let summary = `${details.eventCount} event${details.eventCount === 1 ? "" : "s"}`;
|
|
64
74
|
if (details.omittedEventCount > 0) summary += ` · ${details.omittedEventCount} omitted`;
|
|
75
|
+
if (details.queryOmittedEventCount)
|
|
76
|
+
summary += ` · ${details.queryOmittedEventCount} excluded by limit`;
|
|
77
|
+
if (details.invalidEventCount) summary += ` · ${details.invalidEventCount} invalid entries`;
|
|
78
|
+
if (details.malformedLineCount) summary += ` · ${details.malformedLineCount} malformed lines`;
|
|
65
79
|
if (details.eventDataTruncated) summary += " · data bounded";
|
|
66
80
|
if (details.truncation?.truncated) summary += " · output truncated";
|
|
67
81
|
if (details.rawDataUnavailable || details.rawAccessDenied) summary += " · raw unavailable";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
import { debugLevelSchema, debugOperationIdSchema } from "./input.ts";
|
|
4
|
+
|
|
5
|
+
// Diagnostic payloads have producer-defined keys. Transport values are normalized to JSON.
|
|
6
|
+
const diagnosticDataSchema = Type.Unsafe<JsonValue>(
|
|
7
|
+
Type.Unknown({ description: "JSON-safe diagnostic data with producer-defined fields." }),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
/** Native structured result contract for the debug tool, independent of UI/text bounds. */
|
|
11
|
+
export const debugOutputSchema = Type.Object({
|
|
12
|
+
events: Type.Array(
|
|
13
|
+
Type.Object({
|
|
14
|
+
id: Type.Number(),
|
|
15
|
+
timestamp: Type.Number({ description: "Unix time in milliseconds." }),
|
|
16
|
+
operationId: Type.Optional(debugOperationIdSchema),
|
|
17
|
+
source: Type.String(),
|
|
18
|
+
level: debugLevelSchema,
|
|
19
|
+
category: Type.String(),
|
|
20
|
+
message: Type.String(),
|
|
21
|
+
cwd: Type.Optional(Type.String()),
|
|
22
|
+
data: Type.Optional(diagnosticDataSchema),
|
|
23
|
+
rawData: Type.Optional(diagnosticDataSchema),
|
|
24
|
+
}),
|
|
25
|
+
),
|
|
26
|
+
provenance: Type.Union([
|
|
27
|
+
Type.Object({ kind: Type.Literal("live"), scope: Type.Literal("retained-buffer") }),
|
|
28
|
+
Type.Object({
|
|
29
|
+
kind: Type.Literal("persisted"),
|
|
30
|
+
sessionFile: Type.String(),
|
|
31
|
+
persistedEventCount: Type.Integer({ minimum: 0 }),
|
|
32
|
+
invalidEventCount: Type.Integer({ minimum: 0 }),
|
|
33
|
+
malformedLineCount: Type.Integer({
|
|
34
|
+
minimum: 0,
|
|
35
|
+
description:
|
|
36
|
+
"Nonblank session JSONL lines that could not be parsed; their event identity is unknown.",
|
|
37
|
+
}),
|
|
38
|
+
}),
|
|
39
|
+
]),
|
|
40
|
+
selection: Type.Object({
|
|
41
|
+
matchedEventCount: Type.Integer({ minimum: 0 }),
|
|
42
|
+
returnedEventCount: Type.Integer({ minimum: 0 }),
|
|
43
|
+
omittedEventCount: Type.Integer({ minimum: 0 }),
|
|
44
|
+
complete: Type.Boolean({
|
|
45
|
+
description:
|
|
46
|
+
"All matching events in this source were returned, no recognized persisted debug entries were invalid, and no nonblank session JSONL lines were malformed. Live evidence covers only the retained buffer, not the entire session.",
|
|
47
|
+
}),
|
|
48
|
+
}),
|
|
49
|
+
rawAccessDenied: Type.Boolean(),
|
|
50
|
+
rawDataUnavailable: Type.Boolean(),
|
|
51
|
+
dataNormalized: Type.Boolean({
|
|
52
|
+
description: "Non-JSON values or excessive nesting were replaced with diagnostic text markers.",
|
|
53
|
+
}),
|
|
54
|
+
emptyReason: Type.Optional(
|
|
55
|
+
Type.Union([
|
|
56
|
+
Type.Literal("no-persisted-events"),
|
|
57
|
+
Type.Literal("no-matches"),
|
|
58
|
+
Type.Literal("query-limit"),
|
|
59
|
+
]),
|
|
60
|
+
),
|
|
61
|
+
modelOutput: Type.Object({
|
|
62
|
+
truncated: Type.Boolean(),
|
|
63
|
+
outputLines: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
64
|
+
totalLines: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
65
|
+
outputBytes: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
66
|
+
totalBytes: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** Selected event evidence and query completeness delivered to programmatic callers. */
|
|
71
|
+
export type DebugToolOutput = Static<typeof debugOutputSchema>;
|
package/src/tool/debug/result.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentToolUpdateCallback } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
type DebugEventQuery,
|
|
4
4
|
type DebugEventView,
|
|
@@ -8,8 +8,10 @@ import { resolveToolPath } from "@mrclrchtr/supi-core/path";
|
|
|
8
8
|
import type { DebugConfig } from "../../config.ts";
|
|
9
9
|
import { formatDebugEvents, truncateDebugOutput } from "../../format-events.ts";
|
|
10
10
|
import type { DebugToolParams } from "../../query.ts";
|
|
11
|
-
import { createDebugRenderDetails } from "../../render-details.ts";
|
|
11
|
+
import { createDebugRenderDetails, type DebugRenderDetails } from "../../render-details.ts";
|
|
12
12
|
import { readSessionDebugEvents } from "../../session-events.ts";
|
|
13
|
+
import type { DebugToolOutput } from "./result-schema.ts";
|
|
14
|
+
import { serializeDebugEvents } from "./serialize.ts";
|
|
13
15
|
|
|
14
16
|
interface DebugProgressDetails {
|
|
15
17
|
scannedLines: number;
|
|
@@ -39,8 +41,49 @@ export interface DebugToolExecutionOptions {
|
|
|
39
41
|
onUpdate?: AgentToolUpdateCallback<unknown>;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
/**
|
|
43
|
-
export
|
|
44
|
+
/** Final native result with separate machine-readable evidence and bounded transcript details. */
|
|
45
|
+
export type DebugToolResult = AgentToolResult<DebugRenderDetails> & {
|
|
46
|
+
structuredContent: DebugToolOutput;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function getEmptyReason(
|
|
50
|
+
returnedEventCount: number,
|
|
51
|
+
matchedEventCount: number,
|
|
52
|
+
provenance: DebugToolOutput["provenance"],
|
|
53
|
+
): DebugToolOutput["emptyReason"] {
|
|
54
|
+
if (returnedEventCount > 0) return undefined;
|
|
55
|
+
if (matchedEventCount > 0) return "query-limit";
|
|
56
|
+
return provenance.kind === "persisted" &&
|
|
57
|
+
provenance.persistedEventCount === 0 &&
|
|
58
|
+
provenance.malformedLineCount === 0
|
|
59
|
+
? "no-persisted-events"
|
|
60
|
+
: "no-matches";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatResult(evidence: DebugToolOutput) {
|
|
64
|
+
const { events, selection, provenance, rawAccessDenied, rawDataUnavailable } = evidence;
|
|
65
|
+
const persisted = provenance.kind === "persisted" ? provenance : undefined;
|
|
66
|
+
const lines = formatDebugEvents(events, rawAccessDenied, rawDataUnavailable, {
|
|
67
|
+
persistedEventCount: persisted?.persistedEventCount,
|
|
68
|
+
invalidEventCount: persisted?.invalidEventCount,
|
|
69
|
+
malformedLineCount: persisted?.malformedLineCount,
|
|
70
|
+
queryLimitExcludedAll: evidence.emptyReason === "query-limit",
|
|
71
|
+
});
|
|
72
|
+
if (selection.omittedEventCount > 0) {
|
|
73
|
+
lines.unshift(
|
|
74
|
+
`Showing ${selection.returnedEventCount} of ${selection.matchedEventCount} matching debug events; ${selection.omittedEventCount} omitted by limit.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (evidence.dataNormalized)
|
|
78
|
+
lines.push("Non-JSON diagnostic values were normalized to text markers.");
|
|
79
|
+
return truncateDebugOutput(lines.join("\n"));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Assemble both result representations from the same selected event facts. */
|
|
83
|
+
export async function buildToolResult(
|
|
84
|
+
params: DebugToolParams,
|
|
85
|
+
options: DebugToolExecutionOptions,
|
|
86
|
+
): Promise<DebugToolResult> {
|
|
44
87
|
const { config, cwd, signal, onUpdate } = options;
|
|
45
88
|
if (!config.enabled && !params.sessionFile) {
|
|
46
89
|
throw new Error(
|
|
@@ -68,6 +111,10 @@ export async function buildToolResult(params: DebugToolParams, options: DebugToo
|
|
|
68
111
|
let rawAccessDenied: boolean;
|
|
69
112
|
let rawDataUnavailable = false;
|
|
70
113
|
let persistedEventCount: number | undefined;
|
|
114
|
+
let matchedEventCount: number;
|
|
115
|
+
let invalidEventCount = 0;
|
|
116
|
+
let malformedLineCount = 0;
|
|
117
|
+
let provenance: DebugToolOutput["provenance"];
|
|
71
118
|
if (params.sessionFile) {
|
|
72
119
|
const sessionFile = resolveToolPath(cwd, params.sessionFile);
|
|
73
120
|
const persisted =
|
|
@@ -79,35 +126,72 @@ export async function buildToolResult(params: DebugToolParams, options: DebugToo
|
|
|
79
126
|
: await readSessionDebugEvents(sessionFile, filters);
|
|
80
127
|
events = persisted.events;
|
|
81
128
|
persistedEventCount = persisted.persistedEventCount;
|
|
129
|
+
matchedEventCount = persisted.matchedEventCount;
|
|
130
|
+
invalidEventCount = persisted.invalidEventCount;
|
|
131
|
+
malformedLineCount = persisted.malformedLineCount;
|
|
132
|
+
provenance = {
|
|
133
|
+
kind: "persisted",
|
|
134
|
+
sessionFile,
|
|
135
|
+
persistedEventCount,
|
|
136
|
+
invalidEventCount,
|
|
137
|
+
malformedLineCount,
|
|
138
|
+
};
|
|
82
139
|
rawAccessDenied = Boolean(params.includeRaw);
|
|
83
140
|
rawDataUnavailable = rawAccessDenied;
|
|
84
141
|
} else {
|
|
85
|
-
|
|
86
|
-
|
|
142
|
+
// The live registry is already retention-bounded. Select locally so omission counts are exact.
|
|
143
|
+
const result = getDebugEvents({ ...query, limit: undefined });
|
|
144
|
+
matchedEventCount = result.events.length;
|
|
145
|
+
const limit = params.limit && params.limit > 0 ? Math.floor(params.limit) : matchedEventCount;
|
|
146
|
+
events = result.events.slice(0, limit);
|
|
87
147
|
rawAccessDenied = result.rawAccessDenied;
|
|
148
|
+
provenance = { kind: "live", scope: "retained-buffer" };
|
|
149
|
+
}
|
|
150
|
+
const serialized = serializeDebugEvents(events);
|
|
151
|
+
const omittedEventCount = matchedEventCount - serialized.events.length;
|
|
152
|
+
const emptyReason = getEmptyReason(serialized.events.length, matchedEventCount, provenance);
|
|
153
|
+
const structuredContent: DebugToolOutput = {
|
|
154
|
+
...serialized,
|
|
155
|
+
provenance,
|
|
156
|
+
selection: {
|
|
157
|
+
matchedEventCount,
|
|
158
|
+
returnedEventCount: serialized.events.length,
|
|
159
|
+
omittedEventCount,
|
|
160
|
+
complete: omittedEventCount === 0 && invalidEventCount === 0 && malformedLineCount === 0,
|
|
161
|
+
},
|
|
162
|
+
rawAccessDenied,
|
|
163
|
+
rawDataUnavailable,
|
|
164
|
+
...(emptyReason === undefined ? {} : { emptyReason }),
|
|
165
|
+
modelOutput: { truncated: false },
|
|
166
|
+
};
|
|
167
|
+
const output = formatResult(structuredContent);
|
|
168
|
+
if (output.truncation) {
|
|
169
|
+
structuredContent.modelOutput = {
|
|
170
|
+
truncated: true,
|
|
171
|
+
outputLines: output.truncation.outputLines,
|
|
172
|
+
totalLines: output.truncation.totalLines,
|
|
173
|
+
outputBytes: output.truncation.outputBytes,
|
|
174
|
+
totalBytes: output.truncation.totalBytes,
|
|
175
|
+
};
|
|
88
176
|
}
|
|
89
|
-
const
|
|
90
|
-
formatDebugEvents(events, rawAccessDenied, rawDataUnavailable, persistedEventCount).join("\n"),
|
|
91
|
-
);
|
|
92
|
-
const details = createDebugRenderDetails(events, {
|
|
177
|
+
const details = createDebugRenderDetails(serialized.events, {
|
|
93
178
|
enabled: config.enabled,
|
|
94
179
|
agentAccess: config.agentAccess,
|
|
95
180
|
sessionFile: params.sessionFile,
|
|
96
181
|
rawAccessDenied,
|
|
97
182
|
rawDataUnavailable,
|
|
98
183
|
persistedEventCount,
|
|
99
|
-
eventCount:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
: "no-matches"
|
|
105
|
-
: undefined,
|
|
184
|
+
eventCount: structuredContent.selection.returnedEventCount,
|
|
185
|
+
queryOmittedEventCount: structuredContent.selection.omittedEventCount,
|
|
186
|
+
invalidEventCount,
|
|
187
|
+
malformedLineCount,
|
|
188
|
+
emptyReason,
|
|
106
189
|
truncation: output.truncation,
|
|
107
190
|
});
|
|
108
191
|
|
|
109
192
|
return {
|
|
110
193
|
content: [{ type: "text" as const, text: output.text }],
|
|
111
194
|
details,
|
|
195
|
+
structuredContent,
|
|
112
196
|
};
|
|
113
197
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { DebugEventView } from "@mrclrchtr/supi-core/debug";
|
|
3
|
+
import type { DebugToolOutput } from "./result-schema.ts";
|
|
4
|
+
|
|
5
|
+
const MAX_DATA_DEPTH = 64;
|
|
6
|
+
|
|
7
|
+
interface SerializationState {
|
|
8
|
+
normalized: boolean;
|
|
9
|
+
ancestors: WeakSet<object>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function marker(state: SerializationState, text: string): string {
|
|
13
|
+
state.normalized = true;
|
|
14
|
+
return text;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function serializeValue(value: unknown, state: SerializationState, depth: number): JsonValue {
|
|
18
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
19
|
+
if (typeof value === "number") {
|
|
20
|
+
return Number.isFinite(value) ? value : marker(state, String(value));
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "bigint") return marker(state, `${value}n`);
|
|
23
|
+
if (typeof value === "undefined") return marker(state, "[Undefined]");
|
|
24
|
+
if (typeof value === "function") return marker(state, "[Function]");
|
|
25
|
+
if (typeof value === "symbol") return marker(state, "[Symbol]");
|
|
26
|
+
if (state.ancestors.has(value)) return marker(state, "[Circular]");
|
|
27
|
+
if (depth >= MAX_DATA_DEPTH) return marker(state, "[MaxDepth]");
|
|
28
|
+
|
|
29
|
+
// Never transport runtime instances, invoke toJSON, or copy hidden/private state.
|
|
30
|
+
const prototype: unknown = Object.getPrototypeOf(value);
|
|
31
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
|
|
32
|
+
return marker(state, "[Non-JSON object]");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
state.ancestors.add(value);
|
|
36
|
+
try {
|
|
37
|
+
return Array.isArray(value)
|
|
38
|
+
? Array.from(value, (item) => serializeValue(item, state, depth + 1))
|
|
39
|
+
: Object.fromEntries(
|
|
40
|
+
Object.entries(value).map(([key, item]) => [key, serializeValue(item, state, depth + 1)]),
|
|
41
|
+
);
|
|
42
|
+
} finally {
|
|
43
|
+
state.ancestors.delete(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Snapshot selected events as plain JSON without retaining producer-owned references. */
|
|
48
|
+
export function serializeDebugEvents(events: readonly DebugEventView[]): {
|
|
49
|
+
events: DebugToolOutput["events"];
|
|
50
|
+
dataNormalized: boolean;
|
|
51
|
+
} {
|
|
52
|
+
const state: SerializationState = { normalized: false, ancestors: new WeakSet<object>() };
|
|
53
|
+
const serialized = events.map((event) => ({
|
|
54
|
+
id: event.id,
|
|
55
|
+
timestamp: event.timestamp,
|
|
56
|
+
source: event.source,
|
|
57
|
+
level: event.level,
|
|
58
|
+
category: event.category,
|
|
59
|
+
message: event.message,
|
|
60
|
+
...(event.operationId === undefined ? {} : { operationId: event.operationId }),
|
|
61
|
+
...(event.cwd === undefined ? {} : { cwd: event.cwd }),
|
|
62
|
+
...(event.data === undefined ? {} : { data: serializeValue(event.data, state, 0) }),
|
|
63
|
+
...(event.rawData === undefined ? {} : { rawData: serializeValue(event.rawData, state, 0) }),
|
|
64
|
+
}));
|
|
65
|
+
return { events: serialized, dataNormalized: state.normalized };
|
|
66
|
+
}
|