@marcoscale98/pi-extensible-workflows 5.14.1-fork.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/CHANGELOG.md +536 -0
- package/README.md +64 -0
- package/dist/src/agent-execution.d.ts +338 -0
- package/dist/src/agent-execution.js +1633 -0
- package/dist/src/background-widget.d.ts +74 -0
- package/dist/src/background-widget.js +1340 -0
- package/dist/src/budget.d.ts +38 -0
- package/dist/src/budget.js +188 -0
- package/dist/src/changelog.d.ts +6 -0
- package/dist/src/changelog.js +112 -0
- package/dist/src/decoders.d.ts +96 -0
- package/dist/src/decoders.js +629 -0
- package/dist/src/execution.d.ts +19 -0
- package/dist/src/execution.js +749 -0
- package/dist/src/herdr.d.ts +53 -0
- package/dist/src/herdr.js +270 -0
- package/dist/src/host-delivery.d.ts +95 -0
- package/dist/src/host-delivery.js +376 -0
- package/dist/src/host-navigator.d.ts +83 -0
- package/dist/src/host-navigator.js +1087 -0
- package/dist/src/host-phases.d.ts +89 -0
- package/dist/src/host-phases.js +320 -0
- package/dist/src/host-recovery.d.ts +141 -0
- package/dist/src/host-recovery.js +356 -0
- package/dist/src/host-runtime.d.ts +78 -0
- package/dist/src/host-runtime.js +378 -0
- package/dist/src/host-view.d.ts +116 -0
- package/dist/src/host-view.js +868 -0
- package/dist/src/host.d.ts +40 -0
- package/dist/src/host.js +1641 -0
- package/dist/src/index.d.ts +29 -0
- package/dist/src/index.js +13725 -0
- package/dist/src/index.js.map +6 -0
- package/dist/src/io.d.ts +14 -0
- package/dist/src/io.js +82 -0
- package/dist/src/package-identity.d.ts +4 -0
- package/dist/src/package-identity.js +6 -0
- package/dist/src/paths.d.ts +14 -0
- package/dist/src/paths.js +51 -0
- package/dist/src/persistence.d.ts +5 -0
- package/dist/src/persistence.js +5 -0
- package/dist/src/pi-runtime-adapter.d.ts +27 -0
- package/dist/src/pi-runtime-adapter.js +243 -0
- package/dist/src/pi-runtime-runner.d.ts +42 -0
- package/dist/src/pi-runtime-runner.js +505 -0
- package/dist/src/registry.d.ts +48 -0
- package/dist/src/registry.js +379 -0
- package/dist/src/retention.d.ts +17 -0
- package/dist/src/retention.js +195 -0
- package/dist/src/runtime/agent-runner.d.ts +143 -0
- package/dist/src/runtime/agent-runner.js +12 -0
- package/dist/src/runtime/index.d.ts +2 -0
- package/dist/src/runtime/index.js +2 -0
- package/dist/src/runtime/namespace.d.ts +14 -0
- package/dist/src/runtime/namespace.js +1 -0
- package/dist/src/runtime/workflow-result.d.ts +2 -0
- package/dist/src/runtime/workflow-result.js +6 -0
- package/dist/src/session-handoff.d.ts +2 -0
- package/dist/src/session-handoff.js +82 -0
- package/dist/src/session-lease.d.ts +12 -0
- package/dist/src/session-lease.js +180 -0
- package/dist/src/store.d.ts +95 -0
- package/dist/src/store.js +951 -0
- package/dist/src/subagent-manager-handle.d.ts +4 -0
- package/dist/src/subagent-manager-handle.js +6 -0
- package/dist/src/tool-timing.d.ts +12 -0
- package/dist/src/tool-timing.js +33 -0
- package/dist/src/trajectory-contracts.d.ts +8 -0
- package/dist/src/trajectory-contracts.js +9 -0
- package/dist/src/trajectory-host-handle.d.ts +12 -0
- package/dist/src/trajectory-host-handle.js +6 -0
- package/dist/src/trajectory.d.ts +128 -0
- package/dist/src/trajectory.js +594 -0
- package/dist/src/types.d.ts +960 -0
- package/dist/src/types.js +60 -0
- package/dist/src/utils.d.ts +57 -0
- package/dist/src/utils.js +225 -0
- package/dist/src/validation.d.ts +29 -0
- package/dist/src/validation.js +1100 -0
- package/dist/src/workflow-artifacts.d.ts +14 -0
- package/dist/src/workflow-artifacts.js +40 -0
- package/dist/starter/index.d.ts +1 -0
- package/dist/starter/index.js +103 -0
- package/dist/starter/index.js.map +6 -0
- package/dist/starter/review-loop.d.ts +72 -0
- package/dist/starter/review-loop.js +43 -0
- package/dist/starter/roles/developer.md +6 -0
- package/dist/starter/roles/oracle.md +18 -0
- package/dist/starter/roles/researcher.md +20 -0
- package/dist/starter/roles/reviewer.md +19 -0
- package/dist/starter/roles/scout.md +18 -0
- package/dist/subagents/index.d.ts +8 -0
- package/dist/subagents/index.js +8021 -0
- package/dist/subagents/index.js.map +6 -0
- package/dist/subagents/src/contracts.d.ts +173 -0
- package/dist/subagents/src/contracts.js +68 -0
- package/dist/subagents/src/decode.d.ts +15 -0
- package/dist/subagents/src/decode.js +182 -0
- package/dist/subagents/src/index.d.ts +12 -0
- package/dist/subagents/src/index.js +180 -0
- package/dist/subagents/src/manager.d.ts +3 -0
- package/dist/subagents/src/manager.js +1636 -0
- package/dist/subagents/src/navigator.d.ts +5 -0
- package/dist/subagents/src/navigator.js +627 -0
- package/dist/subagents/src/view.d.ts +70 -0
- package/dist/subagents/src/view.js +387 -0
- package/dist/subagents/src/worktree.d.ts +34 -0
- package/dist/subagents/src/worktree.js +68 -0
- package/dist/trajectory/assets/favicon.png +0 -0
- package/dist/trajectory/assets/index.html +772 -0
- package/dist/trajectory/assets/marked.min.js +6 -0
- package/dist/trajectory/assets/morphdom.min.js +1 -0
- package/dist/trajectory/index.d.ts +5 -0
- package/dist/trajectory/index.js +3459 -0
- package/dist/trajectory/index.js.map +6 -0
- package/dist/trajectory/src/assets/favicon.png +0 -0
- package/dist/trajectory/src/assets/index.html +772 -0
- package/dist/trajectory/src/assets/marked.min.js +6 -0
- package/dist/trajectory/src/assets/morphdom.min.js +1 -0
- package/dist/trajectory/src/export.d.ts +25 -0
- package/dist/trajectory/src/export.js +80 -0
- package/dist/trajectory/src/index.d.ts +26 -0
- package/dist/trajectory/src/index.js +732 -0
- package/dist/trajectory/src/server.d.ts +7 -0
- package/dist/trajectory/src/server.js +2534 -0
- package/dist/trajectory/src/server.js.map +6 -0
- package/examples/advanced-patterns/auto-model-router.mjs +78 -0
- package/examples/workflow-extension-template/README.md +37 -0
- package/examples/workflow-extension-template/extension.test.mjs +59 -0
- package/examples/workflow-extension-template/index.js +51 -0
- package/examples/workflow-extension-template/roles/reviewer.md +4 -0
- package/package.json +105 -0
- package/skills/pi-extensible-workflows/SKILL.md +160 -0
- package/src/agent-execution.ts +1533 -0
- package/src/background-widget.ts +1496 -0
- package/src/budget.ts +109 -0
- package/src/changelog.ts +114 -0
- package/src/decoders.ts +567 -0
- package/src/execution.ts +652 -0
- package/src/herdr.ts +240 -0
- package/src/host-delivery.ts +346 -0
- package/src/host-navigator.ts +871 -0
- package/src/host-phases.ts +264 -0
- package/src/host-recovery.ts +333 -0
- package/src/host-runtime.ts +373 -0
- package/src/host-view.ts +828 -0
- package/src/host.ts +1502 -0
- package/src/index.ts +29 -0
- package/src/io.ts +73 -0
- package/src/package-identity.ts +7 -0
- package/src/paths.ts +49 -0
- package/src/persistence.ts +11 -0
- package/src/pi-runtime-adapter.ts +247 -0
- package/src/pi-runtime-runner.ts +436 -0
- package/src/registry.ts +321 -0
- package/src/retention.ts +178 -0
- package/src/runtime/agent-runner.ts +158 -0
- package/src/runtime/index.ts +2 -0
- package/src/runtime/namespace.ts +16 -0
- package/src/runtime/workflow-result.ts +8 -0
- package/src/session-handoff.ts +78 -0
- package/src/session-lease.ts +125 -0
- package/src/store.ts +832 -0
- package/src/subagent-manager-handle.ts +8 -0
- package/src/tool-timing.ts +47 -0
- package/src/trajectory-contracts.ts +13 -0
- package/src/trajectory-host-handle.ts +16 -0
- package/src/trajectory.ts +543 -0
- package/src/types.ts +374 -0
- package/src/utils.ts +192 -0
- package/src/validation.ts +910 -0
- package/src/workflow-artifacts.ts +35 -0
- package/starter/index.ts +35 -0
- package/starter/prompts/council.md +16 -0
- package/starter/prompts/deep-research.md +16 -0
- package/starter/prompts/oracle.md +14 -0
- package/starter/prompts/parallel-review.md +22 -0
- package/starter/prompts/parallel-scout.md +16 -0
- package/starter/prompts/review-loop.md +14 -0
- package/starter/prompts/review.md +14 -0
- package/starter/prompts/scout.md +14 -0
- package/starter/review-loop.ts +73 -0
- package/starter/roles/developer.md +6 -0
- package/starter/roles/oracle.md +18 -0
- package/starter/roles/researcher.md +20 -0
- package/starter/roles/reviewer.md +19 -0
- package/starter/roles/scout.md +18 -0
- package/subagents/README.md +145 -0
- package/subagents/index.ts +8 -0
- package/subagents/src/contracts.ts +176 -0
- package/subagents/src/decode.ts +158 -0
- package/subagents/src/index.ts +194 -0
- package/subagents/src/manager.ts +1560 -0
- package/subagents/src/navigator.ts +488 -0
- package/subagents/src/view.ts +376 -0
- package/subagents/src/worktree.ts +99 -0
- package/trajectory/index.ts +5 -0
- package/trajectory/src/assets/favicon.png +0 -0
- package/trajectory/src/assets/index.html +772 -0
- package/trajectory/src/assets/marked.min.js +6 -0
- package/trajectory/src/assets/morphdom.min.js +1 -0
- package/trajectory/src/export.ts +85 -0
- package/trajectory/src/index.ts +604 -0
- package/trajectory/src/server.ts +447 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SubagentManager } from "../subagents/src/contracts.js";
|
|
2
|
+
|
|
3
|
+
const SUBAGENT_MANAGER_KEY = Symbol.for("pi-extensible-workflows.subagent-manager");
|
|
4
|
+
const globalSubagentManagers = globalThis as typeof globalThis & Record<symbol, SubagentManager | undefined>;
|
|
5
|
+
|
|
6
|
+
export function setSubagentManager(manager: SubagentManager | undefined): void { globalSubagentManagers[SUBAGENT_MANAGER_KEY] = manager; }
|
|
7
|
+
export function getSubagentManager(): SubagentManager | undefined { return globalSubagentManagers[SUBAGENT_MANAGER_KEY]; }
|
|
8
|
+
export function clearSubagentManager(manager: SubagentManager): void { if (globalSubagentManagers[SUBAGENT_MANAGER_KEY] === manager) globalSubagentManagers[SUBAGENT_MANAGER_KEY] = undefined; }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionFactory, ToolExecutionEndEvent, ToolExecutionStartEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const TOOL_TIMING_ENTRY_TYPE = "pi-workflows:tool-timing";
|
|
4
|
+
|
|
5
|
+
export interface ToolTimingEntry {
|
|
6
|
+
readonly toolCallId: string;
|
|
7
|
+
readonly toolName: string;
|
|
8
|
+
readonly startedAt: number;
|
|
9
|
+
readonly completedAt: number;
|
|
10
|
+
readonly durationMs: number;
|
|
11
|
+
readonly isError: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// NOTE: Keep this factory closure-free because Herdr serializes its source for the child Pi process.
|
|
15
|
+
// NOTE: The entry type literal must match TOOL_TIMING_ENTRY_TYPE; it is local for the same serialization boundary.
|
|
16
|
+
type ClockedToolTimingExtension = (pi: ExtensionAPI, clock?: () => number) => void;
|
|
17
|
+
// Bound unfinished executions so missing end events cannot grow session state indefinitely.
|
|
18
|
+
const toolTimingExtension: ClockedToolTimingExtension = (pi, clock = Date.now) => {
|
|
19
|
+
const MAX_ACTIVE_TOOL_TIMINGS = 400;
|
|
20
|
+
const starts = new Map<string, { toolName: string; startedAt: number }>();
|
|
21
|
+
pi.on("tool_execution_start", (event: ToolExecutionStartEvent) => {
|
|
22
|
+
if (!starts.has(event.toolCallId) && starts.size >= MAX_ACTIVE_TOOL_TIMINGS) return;
|
|
23
|
+
starts.set(event.toolCallId, { toolName: event.toolName, startedAt: clock() });
|
|
24
|
+
});
|
|
25
|
+
pi.on("tool_execution_end", (event: ToolExecutionEndEvent) => {
|
|
26
|
+
const started = starts.get(event.toolCallId);
|
|
27
|
+
if (started === undefined) return;
|
|
28
|
+
starts.delete(event.toolCallId);
|
|
29
|
+
const completedAt = clock();
|
|
30
|
+
pi.appendEntry<ToolTimingEntry>("pi-workflows:tool-timing", {
|
|
31
|
+
toolCallId: event.toolCallId,
|
|
32
|
+
toolName: event.toolName,
|
|
33
|
+
startedAt: started.startedAt,
|
|
34
|
+
completedAt,
|
|
35
|
+
durationMs: Math.max(0, completedAt - started.startedAt),
|
|
36
|
+
isError: event.isError,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
pi.on("session_shutdown", () => { starts.clear(); });
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const TOOL_TIMING_EXTENSION: ExtensionFactory = toolTimingExtension;
|
|
43
|
+
|
|
44
|
+
export function createToolTimingExtension(clock: () => number = Date.now): ExtensionFactory {
|
|
45
|
+
if (clock === Date.now) return TOOL_TIMING_EXTENSION;
|
|
46
|
+
return (pi: ExtensionAPI) => { toolTimingExtension(pi, clock); };
|
|
47
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type TrajectoryAction = "checkpoint-approve" | "checkpoint-reject" | "pause" | "resume" | "steer" | "stop" | "retry" | "share";
|
|
2
|
+
export type TrajectoryTarget = { kind: "run" | "subagent"; id: string };
|
|
3
|
+
|
|
4
|
+
const TRAJECTORY_ACTIONS: readonly TrajectoryAction[] = ["checkpoint-approve", "checkpoint-reject", "pause", "resume", "steer", "stop", "retry", "share"];
|
|
5
|
+
|
|
6
|
+
export function isTrajectoryAction(value: unknown): value is TrajectoryAction { return typeof value === "string" && TRAJECTORY_ACTIONS.includes(value as TrajectoryAction); }
|
|
7
|
+
export function isTrajectoryTarget(value: unknown): value is TrajectoryTarget {
|
|
8
|
+
const target = value as { kind?: unknown; id?: unknown };
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && (target.kind === "run" || target.kind === "subagent") && typeof target.id === "string" && target.id.length >= 1 && target.id.length <= 200;
|
|
10
|
+
}
|
|
11
|
+
export function trajectoryActionError(action: TrajectoryAction, target: TrajectoryTarget): string | undefined {
|
|
12
|
+
return target.kind === "subagent" && (action === "checkpoint-approve" || action === "checkpoint-reject" || action === "pause" || action === "resume" || action === "share") ? `Trajectory action ${action} is not supported for subagent targets` : target.kind === "run" && action === "steer" ? "Trajectory action steer is not supported for run targets" : undefined;
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TrajectoryPublisherInput } from "./trajectory.js";
|
|
2
|
+
|
|
3
|
+
export type TrajectoryPublisherProvider = (context: unknown) => TrajectoryPublisherInput;
|
|
4
|
+
|
|
5
|
+
export type TrajectoryHost = {
|
|
6
|
+
open(provider: TrajectoryPublisherProvider, context: unknown): Promise<void>;
|
|
7
|
+
autoAttach(provider: TrajectoryPublisherProvider, context: { hasUI: boolean }): void;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const TRAJECTORY_HOST_KEY = Symbol.for("pi-extensible-workflows.trajectory-host");
|
|
12
|
+
const globalTrajectoryHosts = globalThis as typeof globalThis & Record<symbol, TrajectoryHost | undefined>;
|
|
13
|
+
|
|
14
|
+
export function setTrajectoryHost(host: TrajectoryHost | undefined): void { globalTrajectoryHosts[TRAJECTORY_HOST_KEY] = host; }
|
|
15
|
+
export function getTrajectoryHost(): TrajectoryHost | undefined { return globalTrajectoryHosts[TRAJECTORY_HOST_KEY]; }
|
|
16
|
+
export function clearTrajectoryHost(host: TrajectoryHost): void { if (globalTrajectoryHosts[TRAJECTORY_HOST_KEY] === host) globalTrajectoryHosts[TRAJECTORY_HOST_KEY] = undefined; }
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { open, readFile, readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, DefaultPackageManager, getAgentDir, SettingsManager, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { listRunIds, RunStore, type AwaitingCheckpoint, type EffectiveSystemPrompt, type PersistedRun } from "./persistence.js";
|
|
7
|
+
import { navigatorAttentionSortByState } from "./host-view.js";
|
|
8
|
+
import type { AgentAttemptSummary, JsonValue, LaunchSnapshot, WorkflowAgentSessionReference } from "./types.js";
|
|
9
|
+
import { normalizeSubagentRunRequest, type SubagentProgress, type SubagentRunRequest, type SubagentStatus } from "../subagents/src/contracts.js";
|
|
10
|
+
import { statusValue, subagentErrorValue } from "../subagents/src/decode.js";
|
|
11
|
+
import { isNodeError, jsonValue, object, resourcePatternHasMagic, selectResourcesByLayers } from "./utils.js";
|
|
12
|
+
import type { TrajectoryAction, TrajectoryTarget } from "./trajectory-contracts.js";
|
|
13
|
+
import { canonicalPath, sameFilesystemPath } from "./paths.js";
|
|
14
|
+
|
|
15
|
+
export const TRAJECTORY_MAX_TRANSCRIPT_BYTES = 2 * 1024 * 1024;
|
|
16
|
+
const TRAJECTORY_MAX_NON_TIMING_ENTRIES = 400;
|
|
17
|
+
|
|
18
|
+
export type TrajectoryRun = {
|
|
19
|
+
run: PersistedRun;
|
|
20
|
+
snapshot: Readonly<LaunchSnapshot>;
|
|
21
|
+
awaiting: readonly AwaitingCheckpoint[];
|
|
22
|
+
createdAt?: string;
|
|
23
|
+
transcripts: Readonly<Record<string, readonly unknown[]>>;
|
|
24
|
+
};
|
|
25
|
+
export type TrajectoryTranscriptStatus = "available" | "empty" | "missing" | "failed" | "oversized" | "disconnected";
|
|
26
|
+
export type TrajectoryTranscriptMetadata = { readonly revision: number; readonly status: TrajectoryTranscriptStatus; readonly bytes?: number; readonly timing?: readonly unknown[]; };
|
|
27
|
+
export type TrajectoryRunMetadata = Omit<TrajectoryRun, "transcripts"> & { transcripts: Readonly<Record<string, TrajectoryTranscriptMetadata>> };
|
|
28
|
+
export type TrajectorySubagentArtifact = { readonly truncated: true; readonly path: string; readonly bytes: number };
|
|
29
|
+
export type TrajectorySubagent = {
|
|
30
|
+
id: string;
|
|
31
|
+
sessionId: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
label?: string;
|
|
34
|
+
role?: string;
|
|
35
|
+
mode: "background" | "foreground";
|
|
36
|
+
state: SubagentStatus["state"];
|
|
37
|
+
startedAt?: number;
|
|
38
|
+
finishedAt?: number;
|
|
39
|
+
attempts?: number;
|
|
40
|
+
error?: { readonly code: string; readonly message: string };
|
|
41
|
+
worktree?: { readonly path: string; readonly branch: string };
|
|
42
|
+
request: Readonly<SubagentRunRequest>;
|
|
43
|
+
tools: readonly string[];
|
|
44
|
+
toolDefinitions?: readonly { readonly name: string; readonly description: string }[];
|
|
45
|
+
model?: AgentAttemptSummary["setup"]["model"];
|
|
46
|
+
progress?: SubagentProgress;
|
|
47
|
+
attempt?: AgentAttemptSummary;
|
|
48
|
+
result?: JsonValue | TrajectorySubagentArtifact;
|
|
49
|
+
failure?: { readonly code: string; readonly message: string } | TrajectorySubagentArtifact;
|
|
50
|
+
transcript: readonly unknown[];
|
|
51
|
+
};
|
|
52
|
+
export type TrajectorySubagentMetadata = Omit<TrajectorySubagent, "transcript"> & { transcript: TrajectoryTranscriptMetadata };
|
|
53
|
+
export type TrajectoryTranscriptRequest = { readonly runId?: string; readonly agentId?: string; readonly subagentId?: string; readonly revision?: number };
|
|
54
|
+
export type TrajectorySubagentSessionResolver = (subagentId: string) => WorkflowAgentSessionReference | undefined;
|
|
55
|
+
export type TrajectoryTranscriptResult = { readonly status: TrajectoryTranscriptStatus; readonly revision: number; readonly entries: readonly unknown[]; readonly bytes?: number; readonly error?: string };
|
|
56
|
+
export type TrajectoryTranscriptLoader = (request: Readonly<TrajectoryTranscriptRequest>) => Promise<TrajectoryTranscriptResult>;
|
|
57
|
+
export type TrajectoryRunMetadataLoader = () => Promise<readonly TrajectoryRunMetadata[]>;
|
|
58
|
+
export type TrajectorySubagentMetadataLoader = () => Promise<readonly TrajectorySubagentMetadata[]>;
|
|
59
|
+
export type TrajectoryPublisherMetadata = { readonly runs: readonly TrajectoryRunMetadata[]; readonly subagents: readonly TrajectorySubagentMetadata[] };
|
|
60
|
+
export type TrajectoryPublisherMetadataLoader = () => Promise<TrajectoryPublisherMetadata>;
|
|
61
|
+
export type TrajectorySubagentLoader = () => Promise<readonly TrajectorySubagent[]>;
|
|
62
|
+
export type { TrajectoryAction, TrajectoryTarget } from "./trajectory-contracts.js";
|
|
63
|
+
export { isTrajectoryAction, isTrajectoryTarget, trajectoryActionError } from "./trajectory-contracts.js";
|
|
64
|
+
export type TrajectoryActionRequest = { action: TrajectoryAction; target: TrajectoryTarget; name?: string; payload?: unknown };
|
|
65
|
+
export type TrajectoryActionResult = { readonly id: string; readonly state: "running" };
|
|
66
|
+
export type TrajectoryActionHandler = (request: Readonly<TrajectoryActionRequest>) => Promise<void> | Promise<TrajectoryActionResult | undefined>;
|
|
67
|
+
export type TrajectoryRunLoader = () => Promise<readonly TrajectoryRun[]>;
|
|
68
|
+
export type TrajectoryPublisherInput = {
|
|
69
|
+
cwd: string;
|
|
70
|
+
sessionId: string;
|
|
71
|
+
port?: number;
|
|
72
|
+
themes: boolean;
|
|
73
|
+
loadRuns: TrajectoryRunLoader;
|
|
74
|
+
loadSubagents: TrajectorySubagentLoader;
|
|
75
|
+
loadMetadata: TrajectoryPublisherMetadataLoader;
|
|
76
|
+
loadTranscript?: TrajectoryTranscriptLoader;
|
|
77
|
+
handleAction: TrajectoryActionHandler;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function sessionFile(reference: WorkflowAgentSessionReference | undefined): string | undefined {
|
|
81
|
+
const locator = reference?.locator;
|
|
82
|
+
if (!object(locator) || typeof locator.sessionFile !== "string" || !locator.sessionFile) return undefined;
|
|
83
|
+
return locator.sessionFile;
|
|
84
|
+
}
|
|
85
|
+
function transcriptToolCallId(value: unknown): string | undefined {
|
|
86
|
+
if (!object(value)) return undefined;
|
|
87
|
+
if (typeof value.toolCallId === "string") return value.toolCallId;
|
|
88
|
+
const message = object(value.message) ? value.message : undefined;
|
|
89
|
+
if (message && typeof message.toolCallId === "string") return message.toolCallId;
|
|
90
|
+
if (!message || !Array.isArray(message.content)) return undefined;
|
|
91
|
+
for (const part of message.content) if (object(part) && typeof part.id === "string") return part.id;
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
function isTimingTranscriptEntry(value: unknown): boolean { return object(value) && value.type === "custom" && value.customType === "pi-workflows:tool-timing"; }
|
|
95
|
+
function timingToolCallId(value: unknown): string | undefined {
|
|
96
|
+
if (!object(value) || !isTimingTranscriptEntry(value) || !object(value.data)) return undefined;
|
|
97
|
+
return typeof value.data.toolCallId === "string" ? value.data.toolCallId : undefined;
|
|
98
|
+
}
|
|
99
|
+
class OversizedTranscriptError extends Error {}
|
|
100
|
+
async function readTranscript(path: string, throwOnError = false): Promise<readonly unknown[]> {
|
|
101
|
+
let handle: Awaited<ReturnType<typeof open>> | undefined;
|
|
102
|
+
try {
|
|
103
|
+
const info = await stat(path);
|
|
104
|
+
if (!info.isFile()) return [];
|
|
105
|
+
const startOffset = Math.max(0, info.size - TRAJECTORY_MAX_TRANSCRIPT_BYTES);
|
|
106
|
+
const length = info.size - startOffset;
|
|
107
|
+
handle = await open(path, "r");
|
|
108
|
+
const buffer = Buffer.alloc(length);
|
|
109
|
+
let bytesRead = 0;
|
|
110
|
+
while (bytesRead < length) {
|
|
111
|
+
const result = await handle.read(buffer, bytesRead, length - bytesRead, startOffset + bytesRead);
|
|
112
|
+
if (result.bytesRead === 0) break;
|
|
113
|
+
bytesRead += result.bytesRead;
|
|
114
|
+
}
|
|
115
|
+
let content = buffer.subarray(0, bytesRead).toString("utf8");
|
|
116
|
+
if (startOffset > 0) {
|
|
117
|
+
const boundary = Buffer.alloc(1);
|
|
118
|
+
const boundaryRead = await handle.read(boundary, 0, 1, startOffset - 1);
|
|
119
|
+
const startsAtRecordBoundary = boundaryRead.bytesRead === 1 && boundary[0] === 0x0a;
|
|
120
|
+
const firstLine = content.indexOf("\n");
|
|
121
|
+
if (firstLine < 0) {
|
|
122
|
+
if (!startsAtRecordBoundary && throwOnError) throw new OversizedTranscriptError("Transcript is too large");
|
|
123
|
+
if (!startsAtRecordBoundary) return [];
|
|
124
|
+
} else if (firstLine === content.length - 1 && content.slice(0, firstLine).trim() && !startsAtRecordBoundary) {
|
|
125
|
+
if (throwOnError) throw new OversizedTranscriptError("Transcript is too large");
|
|
126
|
+
return [];
|
|
127
|
+
} else content = content.slice(firstLine + 1);
|
|
128
|
+
}
|
|
129
|
+
const entries: unknown[] = [];
|
|
130
|
+
for (const line of content.split("\n")) {
|
|
131
|
+
if (!line.trim()) continue;
|
|
132
|
+
try {
|
|
133
|
+
const value: unknown = JSON.parse(line);
|
|
134
|
+
if (object(value)) entries.push(value);
|
|
135
|
+
} catch { /* A partially written JSONL line is ignored until the next poll. */ }
|
|
136
|
+
}
|
|
137
|
+
const retainedEntries = entries.filter((entry) => !isTimingTranscriptEntry(entry)).slice(-TRAJECTORY_MAX_NON_TIMING_ENTRIES);
|
|
138
|
+
const retainedIds = new Set(retainedEntries.map(transcriptToolCallId).filter((id): id is string => id !== undefined));
|
|
139
|
+
const retainedTiming = entries.filter((entry) => { const id = timingToolCallId(entry); return id !== undefined && retainedIds.has(id); });
|
|
140
|
+
const retained = new Set([...retainedEntries, ...retainedTiming]);
|
|
141
|
+
return entries.filter((entry) => retained.has(entry));
|
|
142
|
+
} catch (error) { if (throwOnError) throw error; return []; }
|
|
143
|
+
finally { await handle?.close(); }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function transcriptPaths(run: PersistedRun): ReadonlyMap<string, string> {
|
|
147
|
+
const paths = new Map<string, string>();
|
|
148
|
+
for (const agent of run.agents) {
|
|
149
|
+
const path = sessionFile(agent.attemptDetails?.at(-1)?.session);
|
|
150
|
+
if (path) paths.set(agent.id, path);
|
|
151
|
+
}
|
|
152
|
+
return paths;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function runTranscripts(run: PersistedRun): Promise<Readonly<Record<string, readonly unknown[]>>> {
|
|
156
|
+
const result: Record<string, readonly unknown[]> = {};
|
|
157
|
+
for (const [agentId, path] of transcriptPaths(run)) result[agentId] = await readTranscript(path);
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function applySystemPrompts(run: PersistedRun, prompts: readonly EffectiveSystemPrompt[]): PersistedRun {
|
|
162
|
+
if (!prompts.length) return run;
|
|
163
|
+
const bySession = new Map<string, string>();
|
|
164
|
+
for (const entry of prompts) bySession.set(entry.sessionId, entry.prompt);
|
|
165
|
+
const agents = run.agents.map((agent) => {
|
|
166
|
+
if (agent.systemPrompt) return agent;
|
|
167
|
+
const sessionId = agent.attemptDetails?.at(-1)?.session?.sessionId;
|
|
168
|
+
const prompt = typeof sessionId === "string" ? bySession.get(sessionId) : undefined;
|
|
169
|
+
if (prompt === undefined) return agent;
|
|
170
|
+
return { ...agent, systemPrompt: prompt };
|
|
171
|
+
});
|
|
172
|
+
if (agents.every((agent, index) => agent === run.agents[index])) return run;
|
|
173
|
+
return { ...run, agents };
|
|
174
|
+
}
|
|
175
|
+
function toolDefinitionsFor(tools: readonly string[], catalog: ReadonlyMap<string, string>): readonly { readonly name: string; readonly description: string }[] {
|
|
176
|
+
return tools.flatMap((name) => {
|
|
177
|
+
const description = catalog.get(name);
|
|
178
|
+
return description ? [{ name, description }] : [];
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
export function applyToolDescriptions(run: PersistedRun, catalog: ReadonlyMap<string, string>): PersistedRun {
|
|
182
|
+
const agents = run.agents.map((agent) => {
|
|
183
|
+
if (agent.toolDefinitions?.length) return agent;
|
|
184
|
+
const toolDefinitions = toolDefinitionsFor(agent.tools, catalog);
|
|
185
|
+
if (!toolDefinitions.length) return agent;
|
|
186
|
+
return { ...agent, toolDefinitions };
|
|
187
|
+
});
|
|
188
|
+
if (agents.every((agent, index) => agent === run.agents[index])) return run;
|
|
189
|
+
return { ...run, agents };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function piToolCatalog(cwd = process.cwd()): ReadonlyMap<string, string> {
|
|
193
|
+
return new Map([
|
|
194
|
+
createReadToolDefinition(cwd),
|
|
195
|
+
createBashToolDefinition(cwd),
|
|
196
|
+
createGrepToolDefinition(cwd),
|
|
197
|
+
createFindToolDefinition(cwd),
|
|
198
|
+
createLsToolDefinition(cwd),
|
|
199
|
+
createEditToolDefinition(cwd),
|
|
200
|
+
createWriteToolDefinition(cwd),
|
|
201
|
+
].map((tool) => [tool.name, tool.description]));
|
|
202
|
+
}
|
|
203
|
+
export function withPiToolDescriptions(run: PersistedRun): PersistedRun {
|
|
204
|
+
return applyToolDescriptions(run, piToolCatalog());
|
|
205
|
+
}
|
|
206
|
+
export function withPiToolDescriptionsForTools(tools: readonly string[], cwd: string): readonly { readonly name: string; readonly description: string }[] {
|
|
207
|
+
return toolDefinitionsFor(tools, piToolCatalog(cwd));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function canonicalExtensionSelector(selector: string, base: string): string {
|
|
211
|
+
const negated = selector.startsWith("!");
|
|
212
|
+
const body = negated ? selector.slice(1) : selector;
|
|
213
|
+
if (body === "*" || body === "**" || body.startsWith("**/")) return selector;
|
|
214
|
+
const resolved = resolve(base, body);
|
|
215
|
+
if (resourcePatternHasMagic(body)) return `${negated ? "!" : ""}${resolved}`;
|
|
216
|
+
return `${negated ? "!" : ""}${canonicalPath(resolved)}`;
|
|
217
|
+
}
|
|
218
|
+
function skillNameFromPath(path: string): string {
|
|
219
|
+
const file = basename(path);
|
|
220
|
+
return file.toLowerCase() === "skill.md" ? basename(dirname(path)) : file;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
type DiscoveredResources = { skills: readonly string[]; extensions: readonly string[] };
|
|
224
|
+
const discoveredResourcesByCwd = new Map<string, Promise<DiscoveredResources>>();
|
|
225
|
+
async function discoveredResources(cwd: string): Promise<DiscoveredResources> {
|
|
226
|
+
const cached = discoveredResourcesByCwd.get(cwd);
|
|
227
|
+
if (cached) return cached;
|
|
228
|
+
const pending = (async (): Promise<DiscoveredResources> => {
|
|
229
|
+
const agentDir = getAgentDir();
|
|
230
|
+
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
|
|
231
|
+
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
|
|
232
|
+
const resolved = await packageManager.resolve();
|
|
233
|
+
return {
|
|
234
|
+
extensions: [...new Set(resolved.extensions.filter((entry) => entry.enabled).map((entry) => canonicalPath(entry.path)))],
|
|
235
|
+
skills: [...new Set(resolved.skills.filter((entry) => entry.enabled).map((entry) => skillNameFromPath(entry.path)))],
|
|
236
|
+
};
|
|
237
|
+
})();
|
|
238
|
+
discoveredResourcesByCwd.set(cwd, pending);
|
|
239
|
+
// A cached rejection would keep the fallback empty for the rest of the process lifetime.
|
|
240
|
+
pending.catch(() => discoveredResourcesByCwd.delete(cwd));
|
|
241
|
+
return pending;
|
|
242
|
+
}
|
|
243
|
+
type ResourceInspection = NonNullable<AgentAttemptSummary["setup"]["resourceSelectors"]>;
|
|
244
|
+
async function resolveResourceInspection(inspection: ResourceInspection | undefined, cwd: string, selectorCwd = cwd): Promise<ResourceInspection | undefined> {
|
|
245
|
+
if (!inspection || inspection.skills.length && inspection.extensions.length) return inspection;
|
|
246
|
+
let discovered: DiscoveredResources;
|
|
247
|
+
try { discovered = await discoveredResources(cwd); } catch { return inspection; }
|
|
248
|
+
const sources = inspection.selectorSources ?? { global: inspection.selectors, project: {} };
|
|
249
|
+
const skills = inspection.skills.length ? inspection.skills : selectResourcesByLayers([sources.global.skills, sources.project.skills, sources.role?.skills, sources.call?.skills], discovered.skills);
|
|
250
|
+
const extensions = inspection.extensions.length ? inspection.extensions : selectResourcesByLayers([sources.global.extensions, sources.project.extensions, sources.role?.extensions, sources.call?.extensions].map((layer) => layer?.map((selector) => canonicalExtensionSelector(selector, selectorCwd))), discovered.extensions);
|
|
251
|
+
return skills === inspection.skills && extensions === inspection.extensions ? inspection : { ...inspection, skills, extensions };
|
|
252
|
+
}
|
|
253
|
+
export async function withResolvedAttemptResources(attempt: AgentAttemptSummary, cwd: string): Promise<AgentAttemptSummary> {
|
|
254
|
+
const resourceSelectors = await resolveResourceInspection(attempt.setup.resourceSelectors, cwd, attempt.setup.cwd || cwd);
|
|
255
|
+
if (resourceSelectors === attempt.setup.resourceSelectors) return attempt;
|
|
256
|
+
return { ...attempt, setup: { ...attempt.setup, ...(resourceSelectors === undefined ? {} : { resourceSelectors }) } };
|
|
257
|
+
}
|
|
258
|
+
export async function withResolvedResources(run: PersistedRun, cwd: string): Promise<PersistedRun> {
|
|
259
|
+
const agents = await Promise.all(run.agents.map(async (agent) => {
|
|
260
|
+
const details = agent.attemptDetails;
|
|
261
|
+
const last = details?.at(-1);
|
|
262
|
+
if (!details || !last) return agent;
|
|
263
|
+
const resourceSelectors = await resolveResourceInspection(last.setup.resourceSelectors, cwd, last.setup.cwd || cwd);
|
|
264
|
+
if (resourceSelectors === last.setup.resourceSelectors) return agent;
|
|
265
|
+
return { ...agent, attemptDetails: [...details.slice(0, -1), { ...last, setup: { ...last.setup, ...(resourceSelectors === undefined ? {} : { resourceSelectors }) } }] };
|
|
266
|
+
}));
|
|
267
|
+
if (agents.every((agent, index) => agent === run.agents[index])) return run;
|
|
268
|
+
return { ...run, agents };
|
|
269
|
+
}
|
|
270
|
+
function withoutAgentActivities(run: PersistedRun): PersistedRun {
|
|
271
|
+
const agents = run.agents.map((agent) => {
|
|
272
|
+
if (agent.activity === undefined) return agent;
|
|
273
|
+
const next = { ...agent };
|
|
274
|
+
delete next.activity;
|
|
275
|
+
return next;
|
|
276
|
+
});
|
|
277
|
+
return agents.every((agent, index) => agent === run.agents[index]) ? run : { ...run, agents };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function loadTrajectoryRun(store: RunStore, includeTranscripts = true): Promise<TrajectoryRun> {
|
|
281
|
+
const value = await store.load();
|
|
282
|
+
const summary = await store.loadSummary().catch(() => undefined);
|
|
283
|
+
const prompts = await store.systemPrompts().catch(() => []);
|
|
284
|
+
const run = withoutAgentActivities(await withResolvedResources(withPiToolDescriptions(applySystemPrompts(value.run, prompts)), store.cwd));
|
|
285
|
+
return { run, snapshot: value.snapshot, awaiting: await store.awaitingCheckpoints(), ...(summary?.createdAt === undefined ? {} : { createdAt: summary.createdAt }), transcripts: includeTranscripts ? await runTranscripts(run) : {} };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function loadTrajectoryRuns(cwd: string, sessionId: string, home = homedir(), overlay?: (run: PersistedRun) => PersistedRun): Promise<readonly TrajectoryRun[]> {
|
|
289
|
+
const loaded: TrajectoryRun[] = [];
|
|
290
|
+
for (const runId of await listRunIds(cwd, sessionId, home, false)) {
|
|
291
|
+
try {
|
|
292
|
+
const value = await loadTrajectoryRun(new RunStore(cwd, sessionId, runId, home));
|
|
293
|
+
loaded.push(overlay ? { ...value, run: overlay(value.run) } : value);
|
|
294
|
+
} catch { /* Ignore corrupt or concurrently removed runs. */ }
|
|
295
|
+
}
|
|
296
|
+
return loaded;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function fileMtime(path: string): Promise<number | undefined> { return stat(path).then((value) => value.mtimeMs).catch(() => undefined); }
|
|
300
|
+
type CachedRunEntry<Value extends { run: PersistedRun }, Signature> = { value: Value; stateMtimeMs: number; journalMtimeMs: number; transcripts: ReadonlyMap<string, Signature> };
|
|
301
|
+
/** Lists the session's runs, reloading one only when its state, journal, or a transcript (per `transcriptSignature`) changed. */
|
|
302
|
+
function createCachedRunLoader<Value extends { run: PersistedRun }, Signature>(cwd: string, sessionId: string, home: string, load: (store: RunStore) => Promise<Value>, transcriptSignature: (path: string) => Promise<Signature>, overlay: ((run: PersistedRun) => PersistedRun) | undefined): () => Promise<readonly Value[]> {
|
|
303
|
+
const cache = new Map<string, CachedRunEntry<Value, Signature>>();
|
|
304
|
+
const changed = async (store: RunStore, entry: CachedRunEntry<Value, Signature>): Promise<boolean> => {
|
|
305
|
+
if (await fileMtime(join(store.directory, "state.json")) !== entry.stateMtimeMs) return true;
|
|
306
|
+
if (await fileMtime(join(store.directory, "journal.json")) !== entry.journalMtimeMs) return true;
|
|
307
|
+
for (const [agentId, path] of transcriptPaths(entry.value.run)) if (await transcriptSignature(path) !== entry.transcripts.get(agentId)) return true;
|
|
308
|
+
return false;
|
|
309
|
+
};
|
|
310
|
+
return async () => {
|
|
311
|
+
const ids = await listRunIds(cwd, sessionId, home, false);
|
|
312
|
+
const current = new Set(ids);
|
|
313
|
+
for (const runId of cache.keys()) if (!current.has(runId)) cache.delete(runId);
|
|
314
|
+
const loaded: Value[] = [];
|
|
315
|
+
for (const runId of ids) {
|
|
316
|
+
const store = new RunStore(cwd, sessionId, runId, home);
|
|
317
|
+
try {
|
|
318
|
+
let entry = cache.get(runId);
|
|
319
|
+
if (!entry || await changed(store, entry)) {
|
|
320
|
+
const value = await load(store);
|
|
321
|
+
const transcripts = new Map<string, Signature>();
|
|
322
|
+
for (const [agentId, path] of transcriptPaths(value.run)) transcripts.set(agentId, await transcriptSignature(path));
|
|
323
|
+
entry = { value, stateMtimeMs: (await fileMtime(join(store.directory, "state.json"))) ?? 0, journalMtimeMs: (await fileMtime(join(store.directory, "journal.json"))) ?? 0, transcripts };
|
|
324
|
+
cache.set(runId, entry);
|
|
325
|
+
}
|
|
326
|
+
loaded.push(overlay ? { ...entry.value, run: overlay(entry.value.run) } : entry.value);
|
|
327
|
+
} catch { cache.delete(runId); /* Ignore corrupt or concurrently removed runs. */ }
|
|
328
|
+
}
|
|
329
|
+
return loaded;
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function createTrajectoryRunLoader(cwd: string, sessionId: string, home = homedir(), overlay?: (run: PersistedRun) => PersistedRun): TrajectoryRunLoader {
|
|
334
|
+
return createCachedRunLoader(cwd, sessionId, home, (store) => loadTrajectoryRun(store), fileMtime, overlay);
|
|
335
|
+
}
|
|
336
|
+
function transcriptRevision(info: { mtimeNs: bigint; size: bigint }): number {
|
|
337
|
+
const digest = createHash("sha256").update(`${String(info.mtimeNs)}:${String(info.size)}`).digest();
|
|
338
|
+
return Math.max(1, digest.readUIntBE(0, 6));
|
|
339
|
+
}
|
|
340
|
+
async function transcriptMetadata(path: string | undefined): Promise<TrajectoryTranscriptMetadata> {
|
|
341
|
+
if (path === undefined) return { revision: 0, status: "missing" };
|
|
342
|
+
try {
|
|
343
|
+
const info = await stat(path, { bigint: true });
|
|
344
|
+
const bytes = info.size > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(info.size);
|
|
345
|
+
if (!info.isFile()) return { revision: transcriptRevision(info), status: "failed" };
|
|
346
|
+
const revision = transcriptRevision(info);
|
|
347
|
+
if (info.size === 0n) return { revision, status: "empty", bytes: 0, timing: [] };
|
|
348
|
+
try {
|
|
349
|
+
const entries = await readTranscript(path, true);
|
|
350
|
+
return { revision, status: "available", bytes, timing: entries.filter(isTimingTranscriptEntry) };
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (error instanceof OversizedTranscriptError) return { revision, status: "oversized", bytes, timing: [] };
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (isNodeError(error, "ENOENT")) return { revision: 0, status: "missing" };
|
|
357
|
+
return { revision: 0, status: "failed" };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function metadataForRun(value: TrajectoryRun): Promise<TrajectoryRunMetadata> {
|
|
361
|
+
const transcripts: Record<string, TrajectoryTranscriptMetadata> = {};
|
|
362
|
+
for (const [agentId, path] of transcriptPaths(value.run)) transcripts[agentId] = await transcriptMetadata(path);
|
|
363
|
+
return { ...value, transcripts };
|
|
364
|
+
}
|
|
365
|
+
async function fileSignature(path: string): Promise<string | undefined> {
|
|
366
|
+
try {
|
|
367
|
+
const info = await stat(path, { bigint: true });
|
|
368
|
+
return info.isFile() ? `${String(info.mtimeNs)}:${String(info.size)}` : undefined;
|
|
369
|
+
} catch {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
export function createTrajectoryRunMetadataLoader(cwd: string, sessionId: string, home = homedir(), overlay?: (run: PersistedRun) => PersistedRun): TrajectoryRunMetadataLoader {
|
|
374
|
+
return createCachedRunLoader(cwd, sessionId, home, async (store) => metadataForRun(await loadTrajectoryRun(store, false)), fileSignature, overlay);
|
|
375
|
+
}
|
|
376
|
+
type CachedTrajectoryTranscriptMetadata = { path: string | undefined; signature: string | undefined; value: TrajectoryTranscriptMetadata };
|
|
377
|
+
export function createTrajectorySubagentMetadataLoader(cwd: string, sessionId: string, agentDir: string, overlay?: (subagent: TrajectorySubagent) => TrajectorySubagent): TrajectorySubagentMetadataLoader {
|
|
378
|
+
const loader = createTrajectorySubagentLoader(cwd, sessionId, agentDir, undefined, false);
|
|
379
|
+
const cache = new Map<string, CachedTrajectoryTranscriptMetadata>();
|
|
380
|
+
return async () => {
|
|
381
|
+
const values = await loader();
|
|
382
|
+
const current = new Set(values.map((value) => value.id));
|
|
383
|
+
for (const id of cache.keys()) if (!current.has(id)) cache.delete(id);
|
|
384
|
+
return Promise.all(values.map(async (value) => {
|
|
385
|
+
const currentValue = overlay ? overlay(value) : value;
|
|
386
|
+
const path = sessionFile(currentValue.attempt?.session);
|
|
387
|
+
const signature = path === undefined ? undefined : await fileSignature(path);
|
|
388
|
+
let entry = cache.get(value.id);
|
|
389
|
+
if (!entry || entry.path !== path || entry.signature !== signature) {
|
|
390
|
+
entry = { path, signature, value: await transcriptMetadata(path) };
|
|
391
|
+
cache.set(value.id, entry);
|
|
392
|
+
}
|
|
393
|
+
return { ...currentValue, transcript: entry.value };
|
|
394
|
+
}));
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function safeTrajectorySubagentId(id: string): boolean { return id !== "." && id !== ".." && /^[A-Za-z0-9._-]+$/.test(id); }
|
|
398
|
+
export function createTrajectoryTranscriptLoader(cwd: string, sessionId: string, home = homedir(), agentDir: string, resolveSubagentSession?: TrajectorySubagentSessionResolver): TrajectoryTranscriptLoader {
|
|
399
|
+
return async (request) => {
|
|
400
|
+
let path: string | undefined;
|
|
401
|
+
try {
|
|
402
|
+
if (request.runId !== undefined && request.agentId !== undefined && request.subagentId === undefined) {
|
|
403
|
+
const value = await new RunStore(cwd, sessionId, request.runId, home).load();
|
|
404
|
+
path = sessionFile(value.run.agents.find((agent) => agent.id === request.agentId)?.attemptDetails?.at(-1)?.session);
|
|
405
|
+
} else if (request.subagentId !== undefined && request.runId === undefined && request.agentId === undefined) {
|
|
406
|
+
if (!safeTrajectorySubagentId(request.subagentId)) return { status: "missing", revision: 0, entries: [], error: "Transcript not found" };
|
|
407
|
+
const rawStatus = await readSubagentJson(join(agentDir, "subagents", request.subagentId, "status.json"));
|
|
408
|
+
const status = statusValue(rawStatus);
|
|
409
|
+
const statusRecord = object(rawStatus) ? rawStatus : undefined;
|
|
410
|
+
const statusCwd = statusRecord === undefined ? undefined : statusRecord.cwd;
|
|
411
|
+
if (!status || status.sessionId !== sessionId || typeof statusCwd === "string" && !sameFilesystemPath(statusCwd, cwd)) return { status: "missing", revision: 0, entries: [], error: "Transcript not found" };
|
|
412
|
+
path = sessionFile(resolveSubagentSession?.(request.subagentId) ?? status.attemptDetails?.at(-1)?.session);
|
|
413
|
+
}
|
|
414
|
+
} catch (error) {
|
|
415
|
+
return isNodeError(error, "ENOENT") ? { status: "missing", revision: 0, entries: [], error: "Transcript not found" } : { status: "failed", revision: 0, entries: [], error: "Transcript failed" };
|
|
416
|
+
}
|
|
417
|
+
const metadata = await transcriptMetadata(path);
|
|
418
|
+
if (request.revision !== undefined && request.revision !== metadata.revision) return { ...metadata, entries: [], error: "Transcript revision is stale" };
|
|
419
|
+
if (metadata.status === "missing") return { ...metadata, entries: [], error: "Transcript not found" };
|
|
420
|
+
if (metadata.status === "empty") return { ...metadata, entries: [] };
|
|
421
|
+
if (path === undefined) return { status: "missing", revision: 0, entries: [], error: "Transcript not found" };
|
|
422
|
+
if (metadata.status === "failed" || metadata.status === "oversized") return { ...metadata, entries: [], error: metadata.status === "oversized" ? "Transcript is too large" : "Transcript failed" };
|
|
423
|
+
try {
|
|
424
|
+
const entries = await readTranscript(path, true);
|
|
425
|
+
if (Buffer.byteLength(JSON.stringify(entries)) > TRAJECTORY_MAX_TRANSCRIPT_BYTES) return { ...metadata, status: "oversized", entries: [], error: "Transcript is too large" };
|
|
426
|
+
return { ...metadata, entries };
|
|
427
|
+
} catch { return { ...metadata, status: "failed", entries: [], error: "Transcript failed" }; }
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
type CachedTrajectorySubagent = { value: TrajectorySubagent; fileMtimes: ReadonlyMap<string, number | undefined>; transcriptMtime: number | undefined };
|
|
431
|
+
const TRAJECTORY_SUBAGENT_FILES = ["status.json", "request.json", "result.json", "failure.json"] as const;
|
|
432
|
+
const TRAJECTORY_SUBAGENT_ATTENTION_ORDER: Readonly<Record<SubagentStatus["state"], number>> = { running: 0, failed: 1, stopped: 2, completed: 3 };
|
|
433
|
+
async function readSubagentJson(path: string, optional = false, maxBytes?: number): Promise<unknown> {
|
|
434
|
+
try {
|
|
435
|
+
if (maxBytes !== undefined) { const info = await stat(path); if (!info.isFile() || info.size > maxBytes) return undefined; }
|
|
436
|
+
return JSON.parse(await readFile(path, "utf8")) as unknown;
|
|
437
|
+
}
|
|
438
|
+
catch (error) { if (optional && isNodeError(error, "ENOENT")) return undefined; throw error; }
|
|
439
|
+
}
|
|
440
|
+
async function subagentFileBytes(path: string): Promise<number | undefined> {
|
|
441
|
+
try { const info = await stat(path); return info.isFile() ? info.size : undefined; } catch { return undefined; }
|
|
442
|
+
}
|
|
443
|
+
function subagentArtifact(path: string, bytes: number | undefined): TrajectorySubagentArtifact | undefined { return bytes === undefined ? undefined : { truncated: true, path, bytes }; }
|
|
444
|
+
function boundedSubagentJson(value: unknown): JsonValue | undefined { return jsonValue(value) ? value : undefined; }
|
|
445
|
+
function withoutProgressActivity(progress: SubagentProgress): SubagentProgress {
|
|
446
|
+
const rest = { ...progress };
|
|
447
|
+
delete rest.activity;
|
|
448
|
+
return rest;
|
|
449
|
+
}
|
|
450
|
+
async function loadTrajectorySubagent(directory: string, cwd: string, sessionId: string, includeTranscript = true): Promise<TrajectorySubagent | "foreign" | undefined> {
|
|
451
|
+
try {
|
|
452
|
+
const id = basename(directory);
|
|
453
|
+
const status = statusValue(await readSubagentJson(join(directory, "status.json")));
|
|
454
|
+
if (!status || status.id !== id) return undefined;
|
|
455
|
+
if (status.sessionId !== sessionId) return "foreign";
|
|
456
|
+
const progress = status.progress === undefined ? undefined : withoutProgressActivity(status.progress);
|
|
457
|
+
const request = normalizeSubagentRunRequest(await readSubagentJson(join(directory, "request.json")));
|
|
458
|
+
const resultPath = join(directory, "result.json");
|
|
459
|
+
const failurePath = join(directory, "failure.json");
|
|
460
|
+
const resultBytes = await subagentFileBytes(resultPath);
|
|
461
|
+
const failureBytes = await subagentFileBytes(failurePath);
|
|
462
|
+
const resultValue = await readSubagentJson(resultPath, true, DEFAULT_MAX_BYTES);
|
|
463
|
+
const failureValue = await readSubagentJson(failurePath, true, DEFAULT_MAX_BYTES);
|
|
464
|
+
const resultTruncated = resultValue === undefined && resultBytes !== undefined && resultBytes > DEFAULT_MAX_BYTES;
|
|
465
|
+
const failureTruncated = failureValue === undefined && failureBytes !== undefined && failureBytes > DEFAULT_MAX_BYTES;
|
|
466
|
+
if (!resultTruncated && resultValue !== undefined && boundedSubagentJson(resultValue) === undefined || !failureTruncated && failureValue !== undefined && subagentErrorValue(failureValue) === undefined) return undefined;
|
|
467
|
+
const resultArtifact = resultTruncated ? subagentArtifact(resultPath, resultBytes) : undefined;
|
|
468
|
+
const failureArtifact = failureTruncated ? subagentArtifact(failurePath, failureBytes) : undefined;
|
|
469
|
+
const result = resultArtifact ?? (resultValue === undefined ? undefined : boundedSubagentJson(resultValue));
|
|
470
|
+
const failure = failureArtifact ?? (failureValue === undefined ? undefined : subagentErrorValue(failureValue));
|
|
471
|
+
const rawAttempt = status.attemptDetails?.at(-1);
|
|
472
|
+
const attempt = rawAttempt === undefined ? undefined : await withResolvedAttemptResources(rawAttempt, cwd);
|
|
473
|
+
const tools = progress?.state?.tools ?? attempt?.setup.tools ?? [];
|
|
474
|
+
const toolDefinitions = toolDefinitionsFor(tools, piToolCatalog(cwd));
|
|
475
|
+
const model = progress?.state?.model ?? attempt?.setup.model;
|
|
476
|
+
const transcriptPath = sessionFile(attempt?.session);
|
|
477
|
+
const transcript = !includeTranscript ? [] : transcriptPath === undefined ? [] : await readTranscript(transcriptPath);
|
|
478
|
+
return {
|
|
479
|
+
id, sessionId, cwd, mode: request.mode ?? "background", state: status.state, request, tools, transcript,
|
|
480
|
+
...(request.label === undefined ? {} : { label: request.label }),
|
|
481
|
+
...(request.role === undefined ? {} : { role: request.role }),
|
|
482
|
+
...(status.startedAt === undefined ? {} : { startedAt: status.startedAt }),
|
|
483
|
+
...(status.finishedAt === undefined ? {} : { finishedAt: status.finishedAt }),
|
|
484
|
+
...(status.attempts === undefined ? {} : { attempts: status.attempts }),
|
|
485
|
+
...(status.error === undefined ? {} : { error: status.error }),
|
|
486
|
+
...(status.worktree === undefined ? {} : { worktree: status.worktree }),
|
|
487
|
+
...(model === undefined ? {} : { model }),
|
|
488
|
+
...(toolDefinitions.length ? { toolDefinitions } : {}),
|
|
489
|
+
...(progress === undefined ? {} : { progress }),
|
|
490
|
+
...(attempt === undefined ? {} : { attempt }),
|
|
491
|
+
...(result === undefined ? {} : { result }),
|
|
492
|
+
...(failure === undefined ? {} : { failure }),
|
|
493
|
+
};
|
|
494
|
+
} catch { return undefined; }
|
|
495
|
+
}
|
|
496
|
+
async function subagentCacheEntryChanged(directory: string, entry: CachedTrajectorySubagent): Promise<boolean> {
|
|
497
|
+
for (const file of TRAJECTORY_SUBAGENT_FILES) if (await fileMtime(join(directory, file)) !== entry.fileMtimes.get(file)) return true;
|
|
498
|
+
const transcriptPath = sessionFile(entry.value.attempt?.session);
|
|
499
|
+
return transcriptPath !== undefined && await fileMtime(transcriptPath) !== entry.transcriptMtime;
|
|
500
|
+
}
|
|
501
|
+
export function createTrajectorySubagentLoader(cwd: string, sessionId: string, agentDir: string, overlay?: (subagent: TrajectorySubagent) => TrajectorySubagent, includeTranscript = true): TrajectorySubagentLoader {
|
|
502
|
+
const root = join(agentDir, "subagents");
|
|
503
|
+
const cache = new Map<string, CachedTrajectorySubagent>();
|
|
504
|
+
const negativeCache = new Map<string, number | undefined>();
|
|
505
|
+
return async () => {
|
|
506
|
+
let entries;
|
|
507
|
+
try { entries = await readdir(root, { withFileTypes: true }); }
|
|
508
|
+
catch (error) { if (isNodeError(error, "ENOENT")) return []; throw error; }
|
|
509
|
+
const ids = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
510
|
+
const current = new Set(ids);
|
|
511
|
+
for (const id of cache.keys()) if (!current.has(id)) cache.delete(id);
|
|
512
|
+
for (const id of negativeCache.keys()) if (!current.has(id)) negativeCache.delete(id);
|
|
513
|
+
const loaded: TrajectorySubagent[] = [];
|
|
514
|
+
for (const id of ids) {
|
|
515
|
+
const directory = join(root, id);
|
|
516
|
+
const statusPath = join(directory, "status.json");
|
|
517
|
+
try {
|
|
518
|
+
const statusMtime = await fileMtime(statusPath);
|
|
519
|
+
if (negativeCache.has(id) && negativeCache.get(id) === statusMtime) continue;
|
|
520
|
+
negativeCache.delete(id);
|
|
521
|
+
let entry = cache.get(id);
|
|
522
|
+
if (!entry || await subagentCacheEntryChanged(directory, entry)) {
|
|
523
|
+
const value = await loadTrajectorySubagent(directory, cwd, sessionId, includeTranscript);
|
|
524
|
+
if (value === "foreign") { cache.delete(id); negativeCache.set(id, statusMtime); continue; }
|
|
525
|
+
if (!value) { cache.delete(id); continue; }
|
|
526
|
+
const fileMtimes = new Map<string, number | undefined>();
|
|
527
|
+
for (const file of TRAJECTORY_SUBAGENT_FILES) fileMtimes.set(file, await fileMtime(join(directory, file)));
|
|
528
|
+
const transcriptPath = sessionFile(value.attempt?.session);
|
|
529
|
+
entry = { value, fileMtimes, transcriptMtime: transcriptPath === undefined ? undefined : await fileMtime(transcriptPath) };
|
|
530
|
+
cache.set(id, entry);
|
|
531
|
+
}
|
|
532
|
+
loaded.push(overlay ? overlay(entry.value) : entry.value);
|
|
533
|
+
} catch { cache.delete(id); /* Ignore corrupt or concurrently removed subagents. */ }
|
|
534
|
+
}
|
|
535
|
+
const attention = navigatorAttentionSortByState(loaded, (entry) => entry.state, () => undefined, TRAJECTORY_SUBAGENT_ATTENTION_ORDER);
|
|
536
|
+
return attention.sort((left, right) => {
|
|
537
|
+
if (left.state !== right.state) return 0;
|
|
538
|
+
const leftTime = left.finishedAt ?? left.startedAt ?? -1;
|
|
539
|
+
const rightTime = right.finishedAt ?? right.startedAt ?? -1;
|
|
540
|
+
return rightTime - leftTime || left.id.localeCompare(right.id);
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
}
|