pi-subagents 0.51.0 → 0.52.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/CHANGELOG.md +41 -0
- package/agents/oracle.md +3 -1
- package/agents/reviewer.md +1 -0
- package/agents/scout.md +2 -2
- package/agents/worker.md +2 -1
- package/docs/agents.md +8 -3
- package/docs/extension-api.md +1 -1
- package/docs/models.md +2 -0
- package/docs/observability.md +37 -1
- package/docs/workflows.md +2 -0
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +1 -0
- package/skills/pi-subagents/references/constraints-and-recipes.md +6 -2
- package/skills/pi-subagents/references/execution-controls.md +12 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -0
- package/src/agents/agent-management.ts +24 -6
- package/src/agents/agents.ts +37 -11
- package/src/agents/skills.ts +1 -1
- package/src/api/preflight.ts +5 -2
- package/src/extension/index.ts +1 -0
- package/src/extension/public-execution.ts +3 -1
- package/src/intercom/native-supervisor-channel.ts +2 -1
- package/src/missions/workflow-state.ts +2 -2
- package/src/runs/background/async-execution.ts +19 -9
- package/src/runs/background/async-resume.ts +19 -2
- package/src/runs/background/async-retention.ts +2 -4
- package/src/runs/background/completion-dedupe.ts +5 -1
- package/src/runs/background/completion-replay.ts +22 -12
- package/src/runs/background/fleet-view.ts +68 -19
- package/src/runs/background/inspect-rpc.ts +443 -0
- package/src/runs/background/notify.ts +28 -4
- package/src/runs/background/result-watcher.ts +44 -0
- package/src/runs/background/resume-guidance.ts +8 -5
- package/src/runs/background/scheduled-runs.ts +2 -0
- package/src/runs/background/subagent-runner.ts +3 -2
- package/src/runs/background/subagent-wait.ts +1 -1
- package/src/runs/background/wait-subscriptions.ts +1 -1
- package/src/runs/foreground/execution.ts +1 -1
- package/src/runs/foreground/subagent-executor.ts +239 -76
- package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
- package/src/runs/shared/acceptance.ts +4 -4
- package/src/runs/shared/external-job-bridge.ts +0 -6
- package/src/runs/shared/model-fallback.ts +77 -34
- package/src/runs/shared/session-lease.ts +0 -6
- package/src/runs/shared/worktree.ts +2 -1
- package/src/shared/types.ts +11 -0
- package/src/slash/slash-bridge.ts +2 -1
- package/src/slash/slash-commands.ts +18 -0
- package/src/watchdog/change-signature.ts +1 -1
- package/src/watchdog/lsp-diagnostics.ts +1 -0
- package/src/workflows/chat-progress.ts +1 -1
- package/src/workflows/scripted-workflow.ts +13 -2
- package/agents/gpt-pro.md +0 -17
|
@@ -24,7 +24,7 @@ import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
|
24
24
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
25
25
|
import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
26
26
|
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
|
|
27
|
-
import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
|
|
27
|
+
import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
|
|
28
28
|
import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
|
|
29
29
|
import type { ModelScopeConfig } from "../shared/model-scope.ts";
|
|
30
30
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
@@ -220,6 +220,7 @@ interface AsyncSingleParams {
|
|
|
220
220
|
agentContract?: AgentContract;
|
|
221
221
|
structuredOutputSchema?: JsonSchemaObject;
|
|
222
222
|
modelOverride?: string;
|
|
223
|
+
modelOverrideFromParent?: boolean;
|
|
223
224
|
thinkingOverride?: AgentConfig["thinking"];
|
|
224
225
|
availableModels?: AvailableModelInfo[];
|
|
225
226
|
maxSubagentDepth: number;
|
|
@@ -837,7 +838,10 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
837
838
|
model,
|
|
838
839
|
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
839
840
|
launchResolvedExtensions,
|
|
840
|
-
modelCandidates: externalRunner ? undefined : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider, {
|
|
841
|
+
modelCandidates: externalRunner ? undefined : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider, {
|
|
842
|
+
scope: ctx.modelScope,
|
|
843
|
+
primaryModelFromParent: inheritsParentModel(s.model, a.model, ctx.currentModel),
|
|
844
|
+
}).map((candidate) =>
|
|
841
845
|
applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined),
|
|
842
846
|
),
|
|
843
847
|
tools: a.tools,
|
|
@@ -1414,12 +1418,14 @@ export function executeAsyncSingle(
|
|
|
1414
1418
|
? `[Read from: ${readPaths.join(", ")}]\n\n`
|
|
1415
1419
|
: "";
|
|
1416
1420
|
const taskText = readsInstruction + taskWithOutputInstruction;
|
|
1417
|
-
const primaryModel = externalRunner ? undefined :
|
|
1418
|
-
params.modelOverride
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1421
|
+
const primaryModel = externalRunner ? undefined : params.modelOverrideFromParent
|
|
1422
|
+
? params.modelOverride
|
|
1423
|
+
: resolveSubagentModelOverride(
|
|
1424
|
+
params.modelOverride ?? agentConfig.model,
|
|
1425
|
+
ctx.currentModel,
|
|
1426
|
+
availableModels,
|
|
1427
|
+
ctx.currentModelProvider,
|
|
1428
|
+
);
|
|
1423
1429
|
const effectiveThinking = externalRunner ? undefined : params.thinkingOverride ?? agentConfig.thinking;
|
|
1424
1430
|
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
|
|
1425
1431
|
const toolBudgetInput = params.toolBudget ?? agentConfig.toolBudget ?? params.configToolBudget;
|
|
@@ -1446,7 +1452,10 @@ export function executeAsyncSingle(
|
|
|
1446
1452
|
: undefined;
|
|
1447
1453
|
const modelCandidates = externalRunner
|
|
1448
1454
|
? []
|
|
1449
|
-
: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, {
|
|
1455
|
+
: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, {
|
|
1456
|
+
scope: ctx.modelScope,
|
|
1457
|
+
primaryModelFromParent: params.modelOverrideFromParent,
|
|
1458
|
+
})
|
|
1450
1459
|
.flatMap((candidate) => {
|
|
1451
1460
|
const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
|
|
1452
1461
|
return resolved ? [resolved] : [];
|
|
@@ -1504,6 +1513,7 @@ export function executeAsyncSingle(
|
|
|
1504
1513
|
...(sessionFile ? { sessionFile } : {}),
|
|
1505
1514
|
cwd: runnerCwd,
|
|
1506
1515
|
...(model ? { model } : {}),
|
|
1516
|
+
...(params.modelOverrideFromParent ? { modelOverrideFromParent: true } : {}),
|
|
1507
1517
|
...(recoveryAgentConfig.fallbackModels ? { fallbackModels: [...recoveryAgentConfig.fallbackModels] } : {}),
|
|
1508
1518
|
...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
|
|
1509
1519
|
...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { DIRS, type AcceptanceInput, type AsyncStatus, type ResolvedTurnBudget, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
|
|
3
|
+
import { DIRS, type AcceptanceInput, type AsyncStatus, type ResolvedTurnBudget, type SteeringRecoveryDescriptor, type SubagentRunMode } from "../../shared/types.ts";
|
|
4
4
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
5
5
|
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
6
6
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
@@ -36,6 +36,7 @@ export type AsyncResumeTarget = {
|
|
|
36
36
|
runId: string;
|
|
37
37
|
asyncDir?: string;
|
|
38
38
|
state: AsyncStatus["state"];
|
|
39
|
+
mode?: SubagentRunMode;
|
|
39
40
|
agent: string;
|
|
40
41
|
index: number;
|
|
41
42
|
cwd?: string;
|
|
@@ -293,6 +294,17 @@ function normalizeRecoveryTurnBudget(value: unknown, descriptorPath: string): Re
|
|
|
293
294
|
return result.turnBudget;
|
|
294
295
|
}
|
|
295
296
|
|
|
297
|
+
export function asyncReviveRequiresRecoveryDescriptor(target: Pick<AsyncResumeTarget, "recoveryDescriptor" | "mode" | "sessionFile">): boolean {
|
|
298
|
+
if (target.recoveryDescriptor) return false;
|
|
299
|
+
return !(target.mode === "workflow" && Boolean(target.sessionFile));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function resumeTargetMode(status: AsyncStatus | null, result: AsyncResultFile | undefined): SubagentRunMode | undefined {
|
|
303
|
+
if (status?.mode) return status.mode;
|
|
304
|
+
if (result?.mode === "single" || result?.mode === "parallel" || result?.mode === "chain" || result?.mode === "workflow") return result.mode;
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
296
308
|
export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): SteeringRecoveryDescriptor | undefined {
|
|
297
309
|
if (!asyncDir) return undefined;
|
|
298
310
|
const descriptorPath = path.join(asyncDir, "recovery-descriptor.json");
|
|
@@ -306,7 +318,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
306
318
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
|
|
307
319
|
const parsed = value as Record<string, unknown>;
|
|
308
320
|
const allowedFields = new Set([
|
|
309
|
-
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
|
|
321
|
+
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelOverrideFromParent", "fallbackModels", "thinking", "tools", "extensions",
|
|
310
322
|
"subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
|
|
311
323
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
312
324
|
"artifactsDir", "maxOutput", "controlConfig", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
@@ -333,6 +345,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
333
345
|
}
|
|
334
346
|
if (parsed.systemPromptMode !== "append" && parsed.systemPromptMode !== "replace") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPromptMode is invalid.`);
|
|
335
347
|
if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
|
|
348
|
+
if (parsed.modelOverrideFromParent !== undefined && typeof parsed.modelOverrideFromParent !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOverrideFromParent must be a boolean.`);
|
|
336
349
|
for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
|
|
337
350
|
if (typeof parsed[field] !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a boolean.`);
|
|
338
351
|
}
|
|
@@ -438,6 +451,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
438
451
|
const recoveryDescriptor = readAsyncRecoveryDescriptor(location.asyncDir ?? undefined);
|
|
439
452
|
const result = location.resultPath ? readResultFile(location.resultPath) : undefined;
|
|
440
453
|
const runId = status?.runId ?? result?.runId ?? result?.id ?? location.resolvedId ?? (location.asyncDir ? path.basename(location.asyncDir) : "unknown");
|
|
454
|
+
const mode = resumeTargetMode(status, result);
|
|
441
455
|
if (options.sessionId && ((status && status.sessionId !== options.sessionId) || (result && result.sessionId !== options.sessionId))) {
|
|
442
456
|
throw new Error(`Async run '${runId}' was not found in the active session.`);
|
|
443
457
|
}
|
|
@@ -464,6 +478,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
464
478
|
runId,
|
|
465
479
|
asyncDir: location.asyncDir ?? undefined,
|
|
466
480
|
state,
|
|
481
|
+
...(mode ? { mode } : {}),
|
|
467
482
|
agent: selectedStep.agent,
|
|
468
483
|
index: requestedIndex,
|
|
469
484
|
cwd: status?.cwd ?? result?.cwd,
|
|
@@ -491,6 +506,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
491
506
|
runId,
|
|
492
507
|
asyncDir: location.asyncDir ?? undefined,
|
|
493
508
|
state,
|
|
509
|
+
...(mode ? { mode } : {}),
|
|
494
510
|
agent: selected.step.agent,
|
|
495
511
|
index: selected.index,
|
|
496
512
|
cwd: status?.cwd ?? result?.cwd,
|
|
@@ -531,6 +547,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
531
547
|
runId,
|
|
532
548
|
asyncDir: location.asyncDir ?? undefined,
|
|
533
549
|
state,
|
|
550
|
+
...(mode ? { mode } : {}),
|
|
534
551
|
agent,
|
|
535
552
|
index,
|
|
536
553
|
...(resumeCwd ? { cwd: resumeCwd } : {}),
|
|
@@ -330,13 +330,11 @@ function processStartIdentity(pid: number): string | undefined {
|
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
if (process.platform === "win32") {
|
|
333
|
-
const result = spawnSync("powershell.exe", ["-NoProfile", "-Command", `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate`], { encoding: "utf-8" });
|
|
333
|
+
const result = spawnSync("powershell.exe", ["-NoProfile", "-Command", `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate`], { encoding: "utf-8", windowsHide: true });
|
|
334
334
|
const started = result.status === 0 ? result.stdout.trim() : "";
|
|
335
335
|
return started ? `win:${started}` : undefined;
|
|
336
336
|
}
|
|
337
|
-
|
|
338
|
-
const started = result.status === 0 ? result.stdout.trim() : "";
|
|
339
|
-
return started ? `${process.platform}:${started}` : undefined;
|
|
337
|
+
return undefined;
|
|
340
338
|
}
|
|
341
339
|
|
|
342
340
|
function processIsAlive(pid: number): boolean | undefined {
|
|
@@ -3,6 +3,7 @@ interface CompletionDataLike {
|
|
|
3
3
|
agent?: unknown;
|
|
4
4
|
timestamp?: unknown;
|
|
5
5
|
sessionId?: unknown;
|
|
6
|
+
state?: unknown;
|
|
6
7
|
taskIndex?: unknown;
|
|
7
8
|
totalTasks?: unknown;
|
|
8
9
|
success?: unknown;
|
|
@@ -22,7 +23,10 @@ function asFiniteNumber(value: unknown): number | undefined {
|
|
|
22
23
|
export function buildCompletionKey(data: CompletionDataLike, fallback: string): string {
|
|
23
24
|
const sessionId = asNonEmptyString(data.sessionId) ?? "no-session";
|
|
24
25
|
const id = asNonEmptyString(data.id);
|
|
25
|
-
|
|
26
|
+
const state = asNonEmptyString(data.state);
|
|
27
|
+
if (id) return state
|
|
28
|
+
? `session:${sessionId}:id:${id}:state:${state}`
|
|
29
|
+
: `session:${sessionId}:id:${id}`;
|
|
26
30
|
const agent = asNonEmptyString(data.agent) ?? "unknown";
|
|
27
31
|
const timestamp = asFiniteNumber(data.timestamp);
|
|
28
32
|
const taskIndex = asFiniteNumber(data.taskIndex);
|
|
@@ -14,6 +14,7 @@ const lastCleanupByResultsDir = new Map<string, number>();
|
|
|
14
14
|
|
|
15
15
|
export interface CompletionArchiveEntry {
|
|
16
16
|
agent?: string;
|
|
17
|
+
resultIndex?: number;
|
|
17
18
|
source: "output-artifact" | "session" | "result-tail";
|
|
18
19
|
path?: string;
|
|
19
20
|
text?: string;
|
|
@@ -71,39 +72,45 @@ function outputArtifactPath(child: Record<string, unknown>): string | undefined
|
|
|
71
72
|
/** Create a small archive that references saved child artifacts and retains only bounded fallback output text. */
|
|
72
73
|
export function writeCompletionArchive(resultsDir: string, runId: string, data: Record<string, unknown>, createdAt: number): string {
|
|
73
74
|
const entries: CompletionArchiveEntry[] = [];
|
|
74
|
-
const fallback: string[] = [];
|
|
75
75
|
const results = Array.isArray(data.results) ? data.results : [];
|
|
76
|
-
for (
|
|
76
|
+
for (let resultIndex = 0; resultIndex < results.length; resultIndex++) {
|
|
77
|
+
const value = results[resultIndex];
|
|
77
78
|
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
78
79
|
const child = value as Record<string, unknown>;
|
|
79
80
|
const agent = nonEmptyString(child.agent);
|
|
80
81
|
const artifactPath = outputArtifactPath(child);
|
|
81
82
|
if (artifactPath) {
|
|
82
|
-
entries.push({ ...(agent ? { agent } : {}), source: "output-artifact", path: artifactPath });
|
|
83
|
+
entries.push({ ...(agent ? { agent } : {}), resultIndex, source: "output-artifact", path: artifactPath });
|
|
83
84
|
continue;
|
|
84
85
|
}
|
|
85
86
|
const sessionPath = existingFile(child.sessionFile);
|
|
86
87
|
if (sessionPath) {
|
|
87
|
-
entries.push({ ...(agent ? { agent } : {}), source: "session", path: sessionPath });
|
|
88
|
+
entries.push({ ...(agent ? { agent } : {}), resultIndex, source: "session", path: sessionPath });
|
|
88
89
|
continue;
|
|
89
90
|
}
|
|
90
91
|
const output = nonEmptyString(child.output);
|
|
91
92
|
const error = nonEmptyString(child.error);
|
|
92
93
|
if (output || error) {
|
|
93
|
-
|
|
94
|
+
const bounded = utf8Tail([error ? `Error: ${error}` : undefined, output].filter(Boolean).join("\n"), ARCHIVE_TEXT_LIMIT_BYTES);
|
|
95
|
+
entries.push({
|
|
96
|
+
...(agent ? { agent } : {}),
|
|
97
|
+
resultIndex,
|
|
98
|
+
source: "result-tail",
|
|
99
|
+
text: bounded.text,
|
|
100
|
+
...(bounded.truncated ? { truncated: true } : {}),
|
|
101
|
+
});
|
|
94
102
|
}
|
|
95
103
|
}
|
|
96
104
|
if (results.length === 0) {
|
|
97
105
|
const sessionPath = existingFile(data.sessionFile);
|
|
98
106
|
if (sessionPath) entries.push({ source: "session", path: sessionPath });
|
|
99
107
|
}
|
|
100
|
-
if (entries.length === 0
|
|
108
|
+
if (entries.length === 0) {
|
|
101
109
|
const summary = nonEmptyString(data.summary);
|
|
102
|
-
if (summary)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
entries.push({ source: "result-tail", text: bounded.text, ...(bounded.truncated ? { truncated: true } : {}) });
|
|
110
|
+
if (summary) {
|
|
111
|
+
const bounded = utf8Tail(summary, ARCHIVE_TEXT_LIMIT_BYTES);
|
|
112
|
+
entries.push({ source: "result-tail", text: bounded.text, ...(bounded.truncated ? { truncated: true } : {}) });
|
|
113
|
+
}
|
|
107
114
|
}
|
|
108
115
|
const archive: CompletionArchive = { version: ARCHIVE_VERSION, runId, createdAt, entries };
|
|
109
116
|
const archivePath = completionArchivePath(resultsDir, runId);
|
|
@@ -165,6 +172,7 @@ function parseArchive(value: unknown): CompletionArchive | undefined {
|
|
|
165
172
|
if (entry.source !== "output-artifact" && entry.source !== "session" && entry.source !== "result-tail") return [];
|
|
166
173
|
return [{
|
|
167
174
|
...(typeof entry.agent === "string" ? { agent: entry.agent } : {}),
|
|
175
|
+
...(typeof entry.resultIndex === "number" && Number.isSafeInteger(entry.resultIndex) && entry.resultIndex >= 0 ? { resultIndex: entry.resultIndex } : {}),
|
|
168
176
|
source: entry.source,
|
|
169
177
|
...(typeof entry.path === "string" ? { path: entry.path } : {}),
|
|
170
178
|
...(typeof entry.text === "string" ? { text: entry.text } : {}),
|
|
@@ -228,7 +236,9 @@ export function readCompletionReplay(resultsDir: string, runId: string, options:
|
|
|
228
236
|
|
|
229
237
|
export function readCompletionArchive(archivePath: string): CompletionArchive | undefined {
|
|
230
238
|
try {
|
|
231
|
-
|
|
239
|
+
const archive = parseArchive(JSON.parse(fs.readFileSync(archivePath, "utf-8")));
|
|
240
|
+
if (!archive) throw new Error("Completion archive is malformed.");
|
|
241
|
+
return archive;
|
|
232
242
|
} catch (error) {
|
|
233
243
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
234
244
|
throw error;
|
|
@@ -156,34 +156,83 @@ function stringifyJsonPreview(value: unknown, maxLength = 240): string {
|
|
|
156
156
|
return raw.length > maxLength ? `${raw.slice(0, maxLength)}…` : raw;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
/** One structured content part of a session JSONL record. Shared by the prose transcript formatter and inspect RPC. */
|
|
160
|
+
export interface SessionTranscriptMessage {
|
|
161
|
+
role: string;
|
|
162
|
+
kind: "text" | "toolCall" | "toolResult";
|
|
163
|
+
text: string;
|
|
164
|
+
name?: string;
|
|
165
|
+
isError?: boolean;
|
|
166
|
+
/** Ordinal of the session record this part came from, so consumers can
|
|
167
|
+
* regroup multi-part messages (one session record can yield several parts). */
|
|
168
|
+
recordIndex?: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function sessionMessageParts(record: unknown): SessionTranscriptMessage[] {
|
|
172
|
+
if (!record || typeof record !== "object") return [];
|
|
173
|
+
const outer = record as { message?: unknown; role?: unknown; content?: unknown; type?: unknown };
|
|
174
|
+
const message = outer.message && typeof outer.message === "object" ? outer.message as { role?: unknown; content?: unknown } : outer;
|
|
175
|
+
const role = typeof message.role === "string" ? message.role : undefined;
|
|
176
|
+
if (!role) return [];
|
|
177
|
+
const content = message.content;
|
|
178
|
+
if (typeof content === "string") return content.trim() ? [{ role, kind: "text", text: content }] : [];
|
|
179
|
+
if (!Array.isArray(content)) return [];
|
|
180
|
+
const parts: SessionTranscriptMessage[] = [];
|
|
181
|
+
for (const part of content) {
|
|
182
|
+
if (!part || typeof part !== "object") continue;
|
|
183
|
+
const entry = part as { type?: unknown; text?: unknown; name?: unknown; toolName?: unknown; args?: unknown; result?: unknown; content?: unknown; isError?: unknown };
|
|
184
|
+
if (typeof entry.text === "string") {
|
|
185
|
+
if (entry.text.trim()) parts.push({ role, kind: "text", text: entry.text });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
166
188
|
if (entry.type === "toolCall" || entry.type === "tool_call") {
|
|
167
189
|
const name = typeof entry.name === "string" ? entry.name : typeof entry.toolName === "string" ? entry.toolName : "tool";
|
|
168
|
-
|
|
190
|
+
parts.push({ role, kind: "toolCall", name, text: `[tool: ${name}${entry.args === undefined ? "" : ` ${stringifyJsonPreview(entry.args)}`}]` });
|
|
191
|
+
continue;
|
|
169
192
|
}
|
|
170
193
|
if (entry.type === "toolResult" || entry.type === "tool_result") {
|
|
171
|
-
|
|
194
|
+
const name = typeof entry.name === "string" ? entry.name : typeof entry.toolName === "string" ? entry.toolName : undefined;
|
|
195
|
+
parts.push({ role, kind: "toolResult", ...(name ? { name } : {}), ...(entry.isError === true ? { isError: true } : {}), text: `[tool result${entry.result === undefined ? "" : `: ${stringifyJsonPreview(entry.result)}`}]` });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (entry.content !== undefined) {
|
|
199
|
+
const preview = stringifyJsonPreview(entry.content);
|
|
200
|
+
if (preview.trim()) parts.push({ role, kind: "text", text: preview });
|
|
172
201
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}).filter(Boolean).join("\n");
|
|
202
|
+
}
|
|
203
|
+
return parts;
|
|
176
204
|
}
|
|
177
205
|
|
|
178
206
|
function sessionMessageLine(record: unknown): string | undefined {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
const role = typeof message.role === "string" ? message.role : undefined;
|
|
183
|
-
if (!role) return undefined;
|
|
184
|
-
const text = contentText(message.content).trim();
|
|
207
|
+
const parts = sessionMessageParts(record);
|
|
208
|
+
if (parts.length === 0) return undefined;
|
|
209
|
+
const text = parts.map((part) => part.text).join("\n").trim();
|
|
185
210
|
if (!text) return undefined;
|
|
186
|
-
return `${role}: ${text}`;
|
|
211
|
+
return `${parts[0]!.role}: ${text}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Structured session tail: parsed content parts, newest last, bounded by
|
|
215
|
+
* maxMessages. Same trusted-root containment as the prose transcript tail. */
|
|
216
|
+
export function readSessionMessagesTail(sessionFile: string, maxMessages: number, trustedRoots: string[]): { messages: SessionTranscriptMessage[]; warnings: string[]; truncated: boolean } {
|
|
217
|
+
const tail = readContainedTextTail(sessionFile, Math.max(maxMessages * 4, maxMessages), trustedRoots, "session");
|
|
218
|
+
const warnings: string[] = [];
|
|
219
|
+
if (tail.error) warnings.push(`Session read failed for ${sessionFile}: ${tail.error}`);
|
|
220
|
+
const parsedMessages: SessionTranscriptMessage[] = [];
|
|
221
|
+
let malformed = 0;
|
|
222
|
+
let recordIndex = 0;
|
|
223
|
+
for (const line of tail.lines) {
|
|
224
|
+
if (!line.trim()) continue;
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(line) as unknown;
|
|
227
|
+
for (const part of sessionMessageParts(parsed)) parsedMessages.push({ ...part, recordIndex });
|
|
228
|
+
} catch {
|
|
229
|
+
malformed++;
|
|
230
|
+
}
|
|
231
|
+
recordIndex++;
|
|
232
|
+
}
|
|
233
|
+
if (malformed > 0) warnings.push(`Skipped ${malformed} malformed session tail line${malformed === 1 ? "" : "s"}.`);
|
|
234
|
+
const messages = parsedMessages.slice(-maxMessages);
|
|
235
|
+
return { messages, warnings, truncated: tail.truncated || parsedMessages.length > messages.length };
|
|
187
236
|
}
|
|
188
237
|
|
|
189
238
|
function readSessionTranscriptTail(sessionFile: string, maxLines: number, trustedRoots: string[]): { lines: string[]; warnings: string[] } {
|