@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,436 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { createPiRuntimeSessionAdapter, isEmptyAbortedAssistant, isTurnBoundaryStart, isTurnEnd, normalizePiMessage, type PiRuntimeSessionAdapter } from "./pi-runtime-adapter.js";
|
|
3
|
+
import type { RuntimeAgentProgress, RuntimeAgentProviderFailure, RuntimeAgentProviderRecovery, RuntimeAgentRunRequest, RuntimeAgentRunResult, RuntimeAgentRunner, RuntimeJsonSchema, RuntimeJsonValue, RuntimeTool, RuntimeToolCall, RuntimeUsage } from "./runtime/agent-runner.js";
|
|
4
|
+
import { RuntimeAgentProviderError } from "./runtime/agent-runner.js";
|
|
5
|
+
import { errorText, jsonValue } from "./utils.js";
|
|
6
|
+
import { WorkflowError, type AgentTransport, type AgentTransportContext, type JsonValue, type LiveSessionHandoff, type PreparedAgentSession, type WorkflowAgentMessage, type WorkflowAgentSession, type WorkflowAgentSessionEvent } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const providerContinuationPrompt = "The provider error was transient. Continue the task from your current state.";
|
|
9
|
+
const handoffContinuationPrompt = "Continue the task from the current session state.";
|
|
10
|
+
|
|
11
|
+
type PiRuntimeAgentRunnerCallbacks = {
|
|
12
|
+
readonly onSession?: (session: WorkflowAgentSession, handoff: LiveSessionHandoff, prepared: Readonly<PreparedAgentSession>) => void | Promise<void>;
|
|
13
|
+
readonly onSessionReady?: () => void | Promise<void>;
|
|
14
|
+
readonly onBeforeComplete?: () => void | Promise<void>;
|
|
15
|
+
readonly onComplete?: (result: RuntimeAgentRunResult, session: WorkflowAgentSession) => void | Promise<void>;
|
|
16
|
+
readonly onFailure?: (error: WorkflowError, session: WorkflowAgentSession) => void | Promise<void>;
|
|
17
|
+
readonly onSystemPrompt?: (entry: { readonly sessionId: string; readonly turn: number; readonly prompt: string }) => void | Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface PiRuntimeAgentRunnerOptions {
|
|
21
|
+
readonly transport: AgentTransport;
|
|
22
|
+
readonly prepared: Readonly<PreparedAgentSession>;
|
|
23
|
+
readonly context: Readonly<AgentTransportContext>;
|
|
24
|
+
readonly handoff: LiveSessionHandoff;
|
|
25
|
+
readonly callbacks?: PiRuntimeAgentRunnerCallbacks;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type PiToolContext = Parameters<ToolDefinition["execute"]>[4];
|
|
29
|
+
|
|
30
|
+
export function runtimeToolFromPiDefinition(definition: ToolDefinition, context: PiToolContext): RuntimeTool {
|
|
31
|
+
const parameters = definition.parameters;
|
|
32
|
+
if (!jsonValue(parameters)) throw new WorkflowError("INVALID_METADATA", `Pi tool ${definition.name} has a non-JSON parameter schema`);
|
|
33
|
+
return {
|
|
34
|
+
name: definition.name,
|
|
35
|
+
description: definition.description,
|
|
36
|
+
parameters: parameters as RuntimeJsonSchema,
|
|
37
|
+
async execute(call: RuntimeToolCall) {
|
|
38
|
+
const result: unknown = await definition.execute(call.id, call.input, call.signal, undefined, context);
|
|
39
|
+
const isError = typeof result === "object" && result !== null && "isError" in result && (result as { readonly isError?: unknown }).isError === true;
|
|
40
|
+
if (!jsonValue(result)) throw new WorkflowError("RESULT_INVALID", `Pi tool ${definition.name} returned a non-JSON result`);
|
|
41
|
+
return { value: runtimeValue(result), ...(isError ? { isError: true } : {}) };
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hasToolCall(message: unknown): boolean {
|
|
47
|
+
return typeof message === "object" && message !== null && Array.isArray((message as { content?: unknown }).content) && (message as { content: unknown[] }).content.some((part) => typeof part === "object" && part !== null && (part as { type?: unknown }).type === "toolCall");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isHandoffAbort(message: WorkflowAgentMessage | undefined): boolean { return message?.role === "assistant" && message.stopReason === "error" && Array.isArray(message.content) && message.content.length === 0 && typeof message.errorMessage === "string" && /abort/i.test(message.errorMessage); }
|
|
51
|
+
function isTerminalAssistant(message: WorkflowAgentMessage | undefined): boolean { return Boolean(message) && message?.stopReason !== "aborted" && !hasToolCall(message); }
|
|
52
|
+
function runtimeUsage(session: WorkflowAgentSession): RuntimeUsage {
|
|
53
|
+
const stats = session.getSessionStats();
|
|
54
|
+
const values = [stats.tokens.input, stats.tokens.output, stats.tokens.cacheRead, stats.tokens.cacheWrite, stats.cost];
|
|
55
|
+
if (values.some((value) => !Number.isFinite(value))) return { availability: "unavailable" };
|
|
56
|
+
return { availability: "complete", input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, costUsd: stats.cost };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function providerFailure(session: WorkflowAgentSession, message: WorkflowAgentMessage | undefined): RuntimeAgentProviderFailure | undefined {
|
|
60
|
+
if (message?.stopReason !== "error") return undefined;
|
|
61
|
+
const state = session.getState();
|
|
62
|
+
return { provider: state.model.provider, model: state.model.model, error: message.errorMessage ?? "Workflow agent session ended with a terminal provider error" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function runtimeValue(value: JsonValue): RuntimeJsonValue { return value; }
|
|
66
|
+
function unstructuredResult(value: RuntimeJsonValue | undefined): string | undefined {
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.result !== "string") return undefined;
|
|
68
|
+
return value.result;
|
|
69
|
+
}
|
|
70
|
+
function remaining(timeoutMs: number | null | undefined, started: number): number | null | undefined { return timeoutMs === null || timeoutMs === undefined ? timeoutMs : Math.max(1, timeoutMs - (Date.now() - started)); }
|
|
71
|
+
function providerLimited(error: unknown): boolean {
|
|
72
|
+
if (!error || typeof error !== "object") return false;
|
|
73
|
+
const candidate = error as { status?: unknown; code?: unknown };
|
|
74
|
+
return candidate.status === 429 || candidate.code === 429 || candidate.code === "rate_limit_exceeded" || candidate.code === "RATE_LIMITED";
|
|
75
|
+
}
|
|
76
|
+
function isToolError(result: unknown): boolean { return typeof result === "object" && result !== null && "isError" in result && (result as { readonly isError?: unknown }).isError === true; }
|
|
77
|
+
function piToolFromRuntime(definition: RuntimeTool, signal: AbortSignal): ToolDefinition {
|
|
78
|
+
if (typeof definition.name !== "string" || !definition.name || typeof definition.description !== "string" || !jsonValue(definition.parameters) || typeof definition.execute !== "function") throw new WorkflowError("INVALID_METADATA", `Runtime tool ${definition.name || "<unnamed>"} is not JSON-compatible`);
|
|
79
|
+
return {
|
|
80
|
+
name: definition.name,
|
|
81
|
+
label: definition.name,
|
|
82
|
+
description: definition.description,
|
|
83
|
+
parameters: definition.parameters,
|
|
84
|
+
async execute(toolCallId, input, toolSignal) {
|
|
85
|
+
if (!jsonValue(input)) throw new WorkflowError("INVALID_METADATA", `Runtime tool ${definition.name} received a non-JSON input`);
|
|
86
|
+
const result = await definition.execute({ id: toolCallId, input: runtimeValue(input), signal: toolSignal ?? signal });
|
|
87
|
+
if (!jsonValue(result.value)) throw new WorkflowError("RESULT_INVALID", `Runtime tool ${definition.name} returned a non-JSON result`);
|
|
88
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result.value) }], details: result.value, ...(result.isError ? { isError: true } : {}) };
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function preparedForRuntimeRequest(prepared: Readonly<PreparedAgentSession>, customTools: readonly RuntimeTool[], signal: AbortSignal, captureResult: (value: RuntimeJsonValue) => void, resultTool: ToolDefinition | undefined = prepared.resultTool): Readonly<PreparedAgentSession> {
|
|
94
|
+
const names = new Set([...prepared.tools, ...(prepared.customTools ?? []).map(({ name }) => name), ...(resultTool ? [resultTool.name] : [])]);
|
|
95
|
+
const additions = customTools.map((tool) => {
|
|
96
|
+
if (names.has(tool.name)) throw new WorkflowError("INTERNAL_ERROR", `Runtime tool ${tool.name} conflicts with the prepared Pi tool policy`);
|
|
97
|
+
names.add(tool.name);
|
|
98
|
+
return piToolFromRuntime(tool, signal);
|
|
99
|
+
});
|
|
100
|
+
const nativeResultTool = resultTool;
|
|
101
|
+
const wrappedResultTool = nativeResultTool ? Object.freeze({
|
|
102
|
+
...nativeResultTool,
|
|
103
|
+
async execute(toolCallId: string, input: unknown, toolSignal: AbortSignal | undefined, onUpdate: Parameters<ToolDefinition["execute"]>[3], context: Parameters<ToolDefinition["execute"]>[4]) {
|
|
104
|
+
const result = await nativeResultTool.execute(toolCallId, input, toolSignal, onUpdate, context);
|
|
105
|
+
if (!isToolError(result) && jsonValue(input)) captureResult(runtimeValue(structuredClone(input)));
|
|
106
|
+
return result;
|
|
107
|
+
},
|
|
108
|
+
}) : undefined;
|
|
109
|
+
if (additions.length === 0 && wrappedResultTool === undefined) return prepared;
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
...prepared,
|
|
112
|
+
...(additions.length === 0 ? {} : { customTools: Object.freeze([...(prepared.customTools ?? []), ...additions]) }),
|
|
113
|
+
...(wrappedResultTool === undefined ? {} : { resultTool: wrappedResultTool }),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function isRuntimeAgentProviderError(error: unknown): error is RuntimeAgentProviderError { return error instanceof RuntimeAgentProviderError; }
|
|
118
|
+
|
|
119
|
+
export function normalizePiRuntimeError(error: unknown, signal: AbortSignal, setupFailed = false): WorkflowError {
|
|
120
|
+
if (error instanceof WorkflowError) return error;
|
|
121
|
+
if (error instanceof RuntimeAgentProviderError) return new WorkflowError("AGENT_FAILED", error.failure.error);
|
|
122
|
+
return new WorkflowError(signal.aborted && setupFailed ? "CANCELLED" : "AGENT_FAILED", errorText(error));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function withTimeout(work: Promise<{ assistant?: WorkflowAgentMessage }>, timeoutMs: number | null | undefined, signal: AbortSignal, session: WorkflowAgentSession): Promise<{ assistant?: WorkflowAgentMessage }> {
|
|
126
|
+
if (signal.aborted) throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
127
|
+
let timer: NodeJS.Timeout | undefined;
|
|
128
|
+
let abort: (() => void) | undefined;
|
|
129
|
+
const state = { interrupted: false };
|
|
130
|
+
const timeout = timeoutMs === undefined || timeoutMs === null ? new Promise<never>(() => undefined) : new Promise<never>((_, reject) => {
|
|
131
|
+
timer = setTimeout(() => { state.interrupted = true; reject(new WorkflowError("AGENT_TIMEOUT", "Agent attempt timed out")); }, timeoutMs);
|
|
132
|
+
});
|
|
133
|
+
const cancelled = new Promise<never>((_, reject) => {
|
|
134
|
+
abort = () => { state.interrupted = true; reject(new WorkflowError("CANCELLED", "Agent cancelled")); };
|
|
135
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
136
|
+
});
|
|
137
|
+
try {
|
|
138
|
+
return await Promise.race([work, timeout, cancelled]);
|
|
139
|
+
} finally {
|
|
140
|
+
if (timer) clearTimeout(timer);
|
|
141
|
+
if (abort) signal.removeEventListener("abort", abort);
|
|
142
|
+
if (state.interrupted) await session.abort();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function promptWithProviderPause(session: WorkflowAgentSession, textValue: string, timeoutMs: number | null | undefined, started: number, request: Readonly<RuntimeAgentRunRequest>): Promise<{ assistant?: WorkflowAgentMessage }> {
|
|
147
|
+
const promptTimeout = remaining(timeoutMs, started);
|
|
148
|
+
for (;;) {
|
|
149
|
+
try { return await withTimeout(session.prompt(textValue), promptTimeout, request.signal, session); }
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (!request.onProviderLimit || !providerLimited(error)) throw error;
|
|
152
|
+
await request.onProviderLimit();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export class PiRuntimeAgentRunner implements RuntimeAgentRunner {
|
|
158
|
+
readonly id = "pi";
|
|
159
|
+
// Prepared Pi definitions remain native; request-neutral tools are adapted before session creation.
|
|
160
|
+
readonly capabilities = { customTools: true, structuredResults: true, steering: true, handoff: true, usage: "complete" as const };
|
|
161
|
+
readonly #options: PiRuntimeAgentRunnerOptions;
|
|
162
|
+
constructor(options: PiRuntimeAgentRunnerOptions) { this.#options = options; }
|
|
163
|
+
run(request: Readonly<RuntimeAgentRunRequest>): Promise<RuntimeAgentRunResult> { return this.#runOnce(request); }
|
|
164
|
+
|
|
165
|
+
async #runOnce(request: Readonly<RuntimeAgentRunRequest>): Promise<RuntimeAgentRunResult> {
|
|
166
|
+
if (request.signal.aborted) throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
167
|
+
if (request.timeoutMs !== undefined && request.timeoutMs !== null && (!Number.isInteger(request.timeoutMs) || request.timeoutMs <= 0)) throw new WorkflowError("INVALID_METADATA", "timeoutMs must be null or a positive integer");
|
|
168
|
+
const started = Date.now();
|
|
169
|
+
const prepared = this.#options.prepared;
|
|
170
|
+
if (request.cwd !== prepared.cwd || request.model.provider !== prepared.model.provider || request.model.model !== prepared.model.model || request.model.thinking !== prepared.model.thinking || request.enabledTools.length !== prepared.tools.length || request.enabledTools.some((tool, index) => tool !== prepared.tools[index])) throw new WorkflowError("INTERNAL_ERROR", "Pi runtime request does not match the prepared session");
|
|
171
|
+
let session: WorkflowAgentSession | undefined;
|
|
172
|
+
let structuredResult: RuntimeJsonValue | undefined;
|
|
173
|
+
const resultTool = prepared.resultTool;
|
|
174
|
+
if (!resultTool) throw new WorkflowError("INTERNAL_ERROR", "Workflow agent session is missing its workflow_result tool");
|
|
175
|
+
const preparedForSession = preparedForRuntimeRequest(prepared, request.customTools, request.signal, (value) => { structuredResult = value; }, resultTool);
|
|
176
|
+
const callbacks = this.#options.callbacks;
|
|
177
|
+
const handoff = this.#options.handoff;
|
|
178
|
+
let adapter: PiRuntimeSessionAdapter | undefined;
|
|
179
|
+
let unsubscribe: (() => void) | undefined;
|
|
180
|
+
let disposeStarted = false;
|
|
181
|
+
let sessionReady = false;
|
|
182
|
+
let successfulInvocation = false;
|
|
183
|
+
let progress = Promise.resolve();
|
|
184
|
+
const progressState: { failed: boolean; error: unknown } = { failed: false, error: undefined };
|
|
185
|
+
const eventNotifications = new Set<Promise<void>>();
|
|
186
|
+
let systemPromptTurn = 0;
|
|
187
|
+
let systemPromptWrite = Promise.resolve();
|
|
188
|
+
let systemPromptWriteError: unknown;
|
|
189
|
+
let turnStarted = false;
|
|
190
|
+
let turnPolicyFailure: WorkflowError | undefined;
|
|
191
|
+
let lastAssistant: WorkflowAgentMessage | undefined;
|
|
192
|
+
let handoffBoundaryAssistant: WorkflowAgentMessage | undefined;
|
|
193
|
+
let handoffAbortHandled = false;
|
|
194
|
+
|
|
195
|
+
const hasResult = (): boolean => structuredResult !== undefined;
|
|
196
|
+
// Session event observations are best effort, matching the legacy synchronous subscription.
|
|
197
|
+
const trackEvent = (notification: Promise<void>): void => {
|
|
198
|
+
eventNotifications.add(notification);
|
|
199
|
+
void notification.then(() => { eventNotifications.delete(notification); }, () => { eventNotifications.delete(notification); });
|
|
200
|
+
};
|
|
201
|
+
const flushEvents = async (): Promise<void> => {
|
|
202
|
+
while (eventNotifications.size > 0) {
|
|
203
|
+
const pending = [...eventNotifications];
|
|
204
|
+
await Promise.allSettled(pending);
|
|
205
|
+
for (const notification of pending) eventNotifications.delete(notification);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const report = (value: RuntimeAgentProgress | (() => RuntimeAgentProgress)): void => {
|
|
209
|
+
const progressHandler = request.onProgress;
|
|
210
|
+
if (!progressHandler || progressState.failed) return;
|
|
211
|
+
const materialized = typeof value === "function" ? value() : value;
|
|
212
|
+
progress = progress.then(() => {
|
|
213
|
+
if (progressState.failed) return;
|
|
214
|
+
return progressHandler(materialized);
|
|
215
|
+
}).then(() => undefined).catch((error: unknown) => {
|
|
216
|
+
if (progressState.failed) return;
|
|
217
|
+
progressState.failed = true;
|
|
218
|
+
progressState.error = error;
|
|
219
|
+
if (session) void session.abort().catch(() => undefined);
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
const flushProgress = async (): Promise<void> => {
|
|
223
|
+
await progress;
|
|
224
|
+
if (progressState.failed) throw progressState.error;
|
|
225
|
+
};
|
|
226
|
+
const throwIfProgressFailed = (): void => { if (progressState.failed) throw progressState.error; };
|
|
227
|
+
const recordSystemPrompt = (entry: { readonly sessionId: string; readonly turn: number; readonly prompt: string }): void => {
|
|
228
|
+
systemPromptWrite = systemPromptWrite.then(() => callbacks?.onSystemPrompt?.(entry)).catch((error: unknown) => { systemPromptWriteError ??= error; });
|
|
229
|
+
};
|
|
230
|
+
const flushSystemPrompts = async (): Promise<void> => {
|
|
231
|
+
await systemPromptWrite;
|
|
232
|
+
if (systemPromptWriteError !== undefined) throw new WorkflowError("INTERNAL_ERROR", `Failed to persist effective system prompt: ${errorText(systemPromptWriteError)}`);
|
|
233
|
+
};
|
|
234
|
+
const policy = request.turnPolicy;
|
|
235
|
+
const turnPolicyError = (error: unknown): WorkflowError => error instanceof WorkflowError ? error : new WorkflowError("BUDGET_EXHAUSTED", errorText(error));
|
|
236
|
+
const beginTurn = (abortOnFailure = true): void => {
|
|
237
|
+
if (!policy || turnStarted) return;
|
|
238
|
+
try { policy.beforeTurn(); turnStarted = true; }
|
|
239
|
+
catch (error) {
|
|
240
|
+
turnPolicyFailure ??= turnPolicyError(error);
|
|
241
|
+
if (abortOnFailure && session) void session.abort().catch(() => undefined);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const throwIfTurnPolicyFailed = (): void => { const failure = turnPolicyFailure; if (failure) throw failure; };
|
|
245
|
+
const completeTurn = (final: boolean, steerInstruction: boolean, force = false): void => {
|
|
246
|
+
if (!policy) { turnStarted = false; return; }
|
|
247
|
+
if (turnPolicyFailure && !force) { turnStarted = false; return; }
|
|
248
|
+
try {
|
|
249
|
+
const instruction = policy.afterTurn(runtimeUsage(session as WorkflowAgentSession), final, steerInstruction);
|
|
250
|
+
if (steerInstruction && instruction && session) void session.steer(instruction).catch(() => undefined);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
turnPolicyFailure ??= turnPolicyError(error);
|
|
253
|
+
if (session) void session.abort().catch(() => undefined);
|
|
254
|
+
}
|
|
255
|
+
turnStarted = false;
|
|
256
|
+
};
|
|
257
|
+
const acceptAssistant = (candidate: WorkflowAgentMessage | undefined): void => {
|
|
258
|
+
if (isEmptyAbortedAssistant(candidate)) {
|
|
259
|
+
const previous = session?.getLastAssistant();
|
|
260
|
+
if (previous && !isEmptyAbortedAssistant(previous)) lastAssistant = previous;
|
|
261
|
+
} else if (candidate) lastAssistant = candidate;
|
|
262
|
+
};
|
|
263
|
+
const throwIfCancelled = (): void => { if (request.signal.aborted) throw new WorkflowError("CANCELLED", "Agent cancelled"); };
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
session = await this.#options.transport.createSession(preparedForSession, this.#options.context);
|
|
267
|
+
if (session.reference.transport !== this.#options.transport.id) {
|
|
268
|
+
const actualTransport = session.reference.transport;
|
|
269
|
+
disposeStarted = true;
|
|
270
|
+
await session.dispose();
|
|
271
|
+
session = undefined;
|
|
272
|
+
throw new WorkflowError("INTERNAL_ERROR", `Agent transport ${this.#options.transport.id} created a session for ${actualTransport}`);
|
|
273
|
+
}
|
|
274
|
+
sessionReady = true;
|
|
275
|
+
const createdAdapter = createPiRuntimeSessionAdapter(session, handoff);
|
|
276
|
+
adapter = createdAdapter;
|
|
277
|
+
await callbacks?.onSession?.(session, handoff, preparedForSession);
|
|
278
|
+
const activeSession = session;
|
|
279
|
+
const runtimeHandoff = request.handoff ?? adapter.handoff;
|
|
280
|
+
const preparedTools = new Set([...preparedForSession.tools, ...(preparedForSession.customTools ?? []).map(({ name }) => name), ...(preparedForSession.resultTool ? [preparedForSession.resultTool.name] : [])]);
|
|
281
|
+
if (session.getState().tools.some((tool) => !preparedTools.has(tool))) throw new WorkflowError("INTERNAL_ERROR", `Agent transport ${this.#options.transport.id} widened the prepared tool policy`);
|
|
282
|
+
await callbacks?.onSessionReady?.();
|
|
283
|
+
const handleSessionEvent = async (event: WorkflowAgentSessionEvent): Promise<void> => {
|
|
284
|
+
const observation = adapter?.observe(event);
|
|
285
|
+
if (!observation || !session) return;
|
|
286
|
+
const observedEvent = observation.event;
|
|
287
|
+
if (isTurnEnd(observedEvent.type)) handoffBoundaryAssistant = observedEvent.message?.role === "assistant" ? observedEvent.message : session.getLastAssistant() ?? lastAssistant;
|
|
288
|
+
if (runtimeHandoff !== adapter?.handoff) runtimeHandoff.observe({ type: observedEvent.type });
|
|
289
|
+
if (isTurnEnd(observedEvent.type) && runtimeHandoff.state === "takeover-pending") void session.abort().catch(() => undefined);
|
|
290
|
+
if (observedEvent.type === "agent_start" && session.getState().systemPrompt !== undefined) {
|
|
291
|
+
systemPromptTurn += 1;
|
|
292
|
+
recordSystemPrompt({ sessionId: session.reference.sessionId, turn: systemPromptTurn, prompt: session.getState().systemPrompt ?? "" });
|
|
293
|
+
}
|
|
294
|
+
if (isTurnBoundaryStart(observedEvent.type) || (observedEvent.type === "message_start" && observedEvent.message?.role === "assistant")) beginTurn();
|
|
295
|
+
if (observedEvent.type === "message_end" && observedEvent.message?.role === "assistant") {
|
|
296
|
+
acceptAssistant(observedEvent.message);
|
|
297
|
+
const final = !hasToolCall(observedEvent.message) || hasResult();
|
|
298
|
+
completeTurn(final, !final);
|
|
299
|
+
}
|
|
300
|
+
if (observation.report) report(() => observation.progress);
|
|
301
|
+
};
|
|
302
|
+
if (session.subscribeAsync) unsubscribe = session.subscribeAsync((event) => { const notification = handleSessionEvent(event); trackEvent(notification); return notification; });
|
|
303
|
+
else unsubscribe = session.subscribe((event) => { trackEvent(handleSessionEvent(event)); });
|
|
304
|
+
report(() => createdAdapter.snapshot(false));
|
|
305
|
+
await request.onControl?.({ handoff: runtimeHandoff, steer: (message) => activeSession.steer(message) });
|
|
306
|
+
|
|
307
|
+
const promptOnce = async (prompt: string, turnAlreadyStarted = false): Promise<void> => {
|
|
308
|
+
if (!turnAlreadyStarted) beginTurn();
|
|
309
|
+
throwIfTurnPolicyFailed();
|
|
310
|
+
const response = await promptWithProviderPause(session as WorkflowAgentSession, prompt, request.timeoutMs, started, request);
|
|
311
|
+
acceptAssistant(normalizePiMessage(response.assistant));
|
|
312
|
+
await flushEvents();
|
|
313
|
+
};
|
|
314
|
+
const recoverTerminal = async (): Promise<boolean> => {
|
|
315
|
+
let continued = false;
|
|
316
|
+
for (;;) {
|
|
317
|
+
if (hasResult()) return continued;
|
|
318
|
+
const failure = providerFailure(session as WorkflowAgentSession, lastAssistant);
|
|
319
|
+
if (!failure) return continued;
|
|
320
|
+
if (!request.onProviderError) throw new RuntimeAgentProviderError(failure, undefined, false);
|
|
321
|
+
let recovery: RuntimeAgentProviderRecovery;
|
|
322
|
+
try { recovery = await request.onProviderError(failure); }
|
|
323
|
+
catch (error) { throw new RuntimeAgentProviderError({ ...failure, error: `${failure.error} (provider recovery failed: ${errorText(error)})` }, undefined, true); }
|
|
324
|
+
if (recovery !== "retry") throw new RuntimeAgentProviderError(failure, recovery, true);
|
|
325
|
+
continued = true;
|
|
326
|
+
try { await promptOnce(providerContinuationPrompt); }
|
|
327
|
+
catch (error) { acceptAssistant(session?.getLastAssistant() ?? lastAssistant); if (!hasResult()) throw error; }
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
const promptAndRecover = async (prompt: string, turnAlreadyStarted = false): Promise<void> => {
|
|
331
|
+
let promptFailed = false;
|
|
332
|
+
let promptError: unknown;
|
|
333
|
+
try { await promptOnce(prompt, turnAlreadyStarted); }
|
|
334
|
+
catch (error) { acceptAssistant(session?.getLastAssistant() ?? lastAssistant); promptFailed = true; promptError = error; }
|
|
335
|
+
const handoffWasAttempted = runtimeHandoff.state !== "local-running";
|
|
336
|
+
throwIfCancelled();
|
|
337
|
+
throwIfProgressFailed();
|
|
338
|
+
const suppressHandoffAbort = handoffWasAttempted && !handoffAbortHandled && isHandoffAbort(lastAssistant);
|
|
339
|
+
if (suppressHandoffAbort) handoffAbortHandled = true;
|
|
340
|
+
const recovered = suppressHandoffAbort ? false : await recoverTerminal();
|
|
341
|
+
const preHandoffAssistant = handoffBoundaryAssistant;
|
|
342
|
+
handoffBoundaryAssistant = undefined;
|
|
343
|
+
throwIfCancelled();
|
|
344
|
+
await runtimeHandoff.waitForResume();
|
|
345
|
+
throwIfCancelled();
|
|
346
|
+
const resumed = session?.getLastAssistant();
|
|
347
|
+
const preservePreHandoffResult = runtimeHandoff.transferred && isTerminalAssistant(preHandoffAssistant);
|
|
348
|
+
if (preservePreHandoffResult) lastAssistant = preHandoffAssistant;
|
|
349
|
+
else acceptAssistant(resumed);
|
|
350
|
+
let handoffError: unknown;
|
|
351
|
+
let handoffRecovered = false;
|
|
352
|
+
if (runtimeHandoff.transferred && !preservePreHandoffResult && !hasResult() && (!resumed || resumed.stopReason === "aborted" || isHandoffAbort(resumed) || hasToolCall(resumed))) {
|
|
353
|
+
try { await promptOnce(handoffContinuationPrompt); }
|
|
354
|
+
catch (error) { handoffError = error; }
|
|
355
|
+
}
|
|
356
|
+
if (handoffWasAttempted && !runtimeHandoff.transferred && promptFailed && !hasResult()) {
|
|
357
|
+
try { await promptOnce(handoffContinuationPrompt); handoffRecovered = true; }
|
|
358
|
+
catch (error) { handoffError = error; }
|
|
359
|
+
}
|
|
360
|
+
if (handoffError && !hasResult()) throw handoffError instanceof Error ? handoffError : new Error(errorText(handoffError));
|
|
361
|
+
if (promptFailed && !hasResult() && !recovered && !handoffRecovered && !runtimeHandoff.transferred) throw promptError instanceof Error ? promptError : new Error(errorText(promptError));
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
throwIfProgressFailed();
|
|
365
|
+
beginTurn(false);
|
|
366
|
+
throwIfTurnPolicyFailed();
|
|
367
|
+
await promptAndRecover(request.task, true);
|
|
368
|
+
completeTurn(hasResult() || !hasToolCall(lastAssistant), false, true);
|
|
369
|
+
if (turnPolicyFailure) throw turnPolicyFailure;
|
|
370
|
+
if (!hasResult()) {
|
|
371
|
+
throwIfProgressFailed();
|
|
372
|
+
beginTurn(false);
|
|
373
|
+
throwIfTurnPolicyFailed();
|
|
374
|
+
await promptAndRecover("Submit the final result now by calling workflow_result exactly once. Do not return prose.", true);
|
|
375
|
+
completeTurn(true, false, true);
|
|
376
|
+
}
|
|
377
|
+
if (!hasResult()) {
|
|
378
|
+
throwIfProgressFailed();
|
|
379
|
+
beginTurn(false);
|
|
380
|
+
throwIfTurnPolicyFailed();
|
|
381
|
+
await promptAndRecover("Your result was missing or invalid. Repair it by calling workflow_result exactly once with a schema-valid value.", true);
|
|
382
|
+
completeTurn(true, false, true);
|
|
383
|
+
}
|
|
384
|
+
if (!hasResult()) throw new WorkflowError("RESULT_INVALID", "Agent did not submit a valid workflow_result after one repair");
|
|
385
|
+
await callbacks?.onBeforeComplete?.();
|
|
386
|
+
report(() => createdAdapter.snapshot(true));
|
|
387
|
+
await flushProgress();
|
|
388
|
+
await flushEvents();
|
|
389
|
+
await flushSystemPrompts();
|
|
390
|
+
unsubscribe();
|
|
391
|
+
createdAdapter.dispose();
|
|
392
|
+
adapter = undefined;
|
|
393
|
+
const value = request.resultSchema === undefined ? unstructuredResult(structuredResult) : structuredResult;
|
|
394
|
+
if (value === undefined) throw new WorkflowError("RESULT_INVALID", "Agent did not submit a valid workflow_result after one repair");
|
|
395
|
+
const result: RuntimeAgentRunResult = { value, usage: runtimeUsage(session), reference: { transport: session.reference.transport, ...(session.reference.locator === undefined ? {} : { locator: runtimeValue(session.reference.locator) }) } };
|
|
396
|
+
successfulInvocation = true;
|
|
397
|
+
await callbacks?.onComplete?.(result, session);
|
|
398
|
+
disposeStarted = true;
|
|
399
|
+
await session.dispose();
|
|
400
|
+
session = undefined;
|
|
401
|
+
return result;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
const typed = turnPolicyFailure ?? normalizePiRuntimeError(error, request.signal);
|
|
404
|
+
if (session && sessionReady) {
|
|
405
|
+
const activeSession = session;
|
|
406
|
+
const activeAdapter = adapter;
|
|
407
|
+
report(() => activeAdapter?.snapshot(true) ?? { usage: runtimeUsage(activeSession), toolCalls: [], state: activeSession.getState(), persist: true });
|
|
408
|
+
//NOTE: best-effort telemetry flush during teardown; the agent failure is rethrown after this block, so dropping the flush must not mask the primary error.
|
|
409
|
+
await flushProgress().catch(() => undefined);
|
|
410
|
+
await flushEvents().catch(() => undefined);
|
|
411
|
+
await flushSystemPrompts().catch(() => undefined);
|
|
412
|
+
unsubscribe?.();
|
|
413
|
+
activeAdapter?.dispose();
|
|
414
|
+
adapter = undefined;
|
|
415
|
+
if (!successfulInvocation && !turnPolicyFailure && typed.code !== "BUDGET_EXHAUSTED") {
|
|
416
|
+
try { request.turnPolicy?.afterTurn(runtimeUsage(activeSession), true); }
|
|
417
|
+
catch (budgetFailure) { turnPolicyFailure ??= turnPolicyError(budgetFailure); }
|
|
418
|
+
}
|
|
419
|
+
let callbackFailure: unknown;
|
|
420
|
+
if (!successfulInvocation) {
|
|
421
|
+
try { await callbacks?.onFailure?.(turnPolicyFailure ?? typed, activeSession); }
|
|
422
|
+
catch (failure) { callbackFailure = failure; }
|
|
423
|
+
}
|
|
424
|
+
if (!disposeStarted) {
|
|
425
|
+
await activeSession.dispose().catch(() => undefined);
|
|
426
|
+
}
|
|
427
|
+
if (callbackFailure !== undefined) throw callbackFailure instanceof Error ? callbackFailure : new Error(errorText(callbackFailure));
|
|
428
|
+
}
|
|
429
|
+
throw turnPolicyFailure ?? (error instanceof Error ? error : new Error(errorText(error)));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function createPiRuntimeAgentRunner(options: PiRuntimeAgentRunnerOptions): RuntimeAgentRunner { return new PiRuntimeAgentRunner(options); }
|
|
435
|
+
|
|
436
|
+
export type { PiRuntimeAgentRunnerCallbacks };
|