@tt-a1i/openpi 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -46
- package/SETUP.md +29 -7
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +155 -0
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +59 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +106 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1064 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
- package/extensions/ai-providers/cursor/provider.ts +1175 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +38 -3
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +484 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +47 -24
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +12 -2
- package/extensions/file-mutation-display/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +65 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +82 -6
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +153 -86
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +138 -130
- package/extensions/shared/activity-status.ts +30 -0
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +457 -21
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +83 -34
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +313 -62
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +450 -70
- package/extensions/subagents/src/domain.ts +21 -1
- package/extensions/subagents/src/manager.ts +39 -2
- package/extensions/subagents/src/prompt.ts +49 -7
- package/extensions/subagents/src/result-artifact.ts +36 -0
- package/extensions/subagents/src/result-delivery.ts +39 -14
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/ui-customization/footer.ts +16 -45
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +257 -0
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +459 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +175 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +501 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +79 -5
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +70 -16
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +214 -76
- package/extensions/workflows/retention.ts +599 -0
- package/extensions/workflows/runner.ts +389 -345
- package/extensions/workflows/sandbox-child.cjs +25 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +34 -14
- package/skills/subagents/REFERENCE.md +190 -0
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +6 -4
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +664 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +1069 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +467 -0
- package/web/runtime/pi-runtime.ts +1206 -0
- package/web/runtime/types.ts +102 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1700 -0
- package/web/ui/index.html +142 -0
- package/web/ui/styles.css +680 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -303
|
@@ -11,19 +11,18 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
|
-
createAgentSession,
|
|
15
|
-
DefaultResourceLoader,
|
|
16
|
-
defineTool,
|
|
17
|
-
SessionManager,
|
|
18
|
-
SettingsManager,
|
|
19
14
|
type AgentSession,
|
|
20
15
|
type AgentSessionEvent,
|
|
21
16
|
type AgentSessionEventListener,
|
|
17
|
+
createAgentSession,
|
|
18
|
+
DefaultResourceLoader,
|
|
22
19
|
type ExtensionAPI,
|
|
23
20
|
type ExtensionContext,
|
|
21
|
+
SessionManager,
|
|
22
|
+
SettingsManager,
|
|
24
23
|
type ToolDefinition,
|
|
25
24
|
} from "@earendil-works/pi-coding-agent";
|
|
26
|
-
import {
|
|
25
|
+
import { AgentToolRenderLedger } from "../shared/agent-tool-renderer.ts";
|
|
27
26
|
import {
|
|
28
27
|
bindChildSessionExtensions,
|
|
29
28
|
childToolPolicy,
|
|
@@ -31,23 +30,27 @@ import {
|
|
|
31
30
|
shutdownAndDisposeChildSession,
|
|
32
31
|
} from "../shared/child-session.ts";
|
|
33
32
|
import { createToolCallTimeoutGuard } from "../shared/tool-call-timeout.ts";
|
|
34
|
-
import {
|
|
33
|
+
import { type AgentUsage, emptyUsage, type TranscriptEntry } from "./model.ts";
|
|
34
|
+
import {
|
|
35
|
+
childToolsWithStructuredOutput,
|
|
36
|
+
createStructuredOutputTool,
|
|
37
|
+
STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION,
|
|
38
|
+
} from "../shared/structured-output.ts";
|
|
39
|
+
import { buildWorkflowAgentPrompt } from "./prompt.ts";
|
|
40
|
+
import {
|
|
41
|
+
AgentProgressProjection,
|
|
42
|
+
type ProgressAssistantMessage,
|
|
43
|
+
transcriptFromMessages,
|
|
44
|
+
} from "./progress-projection.ts";
|
|
35
45
|
import {
|
|
36
46
|
createReplayFilesystemBoundary,
|
|
37
47
|
type ReplayFilesystemBoundaryOptions,
|
|
38
48
|
} from "./replay-safety.ts";
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION,
|
|
42
|
-
STRUCTURED_OUTPUT_TOOL_DESCRIPTION,
|
|
43
|
-
} from "./prompt.ts";
|
|
44
|
-
import { safeStringify, truncateUtf8 } from "./serialization.ts";
|
|
49
|
+
import { truncateUtf8 } from "./serialization.ts";
|
|
50
|
+
import { bindWorkflowToolRenderer } from "./tool-renderer.ts";
|
|
45
51
|
|
|
46
52
|
const AGENT_OUTPUT_MAX_BYTES = 64 * 1024;
|
|
47
|
-
export const
|
|
48
|
-
const TRANSCRIPT_ENTRY_MAX_BYTES = 16 * 1024;
|
|
49
|
-
const TRANSCRIPT_TOTAL_MAX_BYTES = 256 * 1024;
|
|
50
|
-
const TRANSCRIPT_MAX_ENTRIES = 200;
|
|
53
|
+
export const MODEL_PROGRESS_TIMEOUT_MS = 45_000;
|
|
51
54
|
|
|
52
55
|
export type WorkflowModel = NonNullable<ExtensionContext["model"]>;
|
|
53
56
|
export type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
@@ -108,8 +111,8 @@ export interface RunAgentOptions {
|
|
|
108
111
|
replayFilesystemBoundary?: ReplayFilesystemBoundaryOptions;
|
|
109
112
|
/** Test-only override for the per-tool execution timeout. */
|
|
110
113
|
toolCallTimeoutMs?: number;
|
|
111
|
-
/** Test-only override for the
|
|
112
|
-
|
|
114
|
+
/** Test-only override for the per-provider-turn model-progress timeout. */
|
|
115
|
+
modelProgressTimeoutMs?: number;
|
|
113
116
|
/** Test-only override for the end-to-end abort/shutdown deadline. */
|
|
114
117
|
shutdownTimeoutMs?: number;
|
|
115
118
|
/** Test seam for lifecycle races; production always uses createAgentSession. */
|
|
@@ -138,9 +141,7 @@ export function workflowChildTools(
|
|
|
138
141
|
tools: readonly string[] | undefined,
|
|
139
142
|
structured: boolean,
|
|
140
143
|
) {
|
|
141
|
-
return tools
|
|
142
|
-
? [...new Set([...tools, ...(structured ? ["structured_output"] : [])])]
|
|
143
|
-
: undefined;
|
|
144
|
+
return childToolsWithStructuredOutput(tools, structured);
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
interface WorkflowToolSession {
|
|
@@ -175,69 +176,9 @@ export function guardWorkflowChildTools(
|
|
|
175
176
|
});
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
|
|
179
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
180
|
-
const seen = new WeakSet<object>();
|
|
181
|
-
let nodes = 0;
|
|
182
|
-
const validate = (current: unknown, depth: number): boolean => {
|
|
183
|
-
if (++nodes > 10_000 || depth > 24) return false;
|
|
184
|
-
if (
|
|
185
|
-
current === null ||
|
|
186
|
-
typeof current === "string" ||
|
|
187
|
-
typeof current === "boolean"
|
|
188
|
-
) {
|
|
189
|
-
return true;
|
|
190
|
-
}
|
|
191
|
-
if (typeof current === "number") return Number.isFinite(current);
|
|
192
|
-
if (Array.isArray(current)) {
|
|
193
|
-
return current.every((item) => validate(item, depth + 1));
|
|
194
|
-
}
|
|
195
|
-
if (typeof current !== "object") return false;
|
|
196
|
-
if (seen.has(current)) return false;
|
|
197
|
-
seen.add(current);
|
|
198
|
-
return Object.keys(current).every((key) => {
|
|
199
|
-
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
return validate((current as Record<string, unknown>)[key], depth + 1);
|
|
203
|
-
});
|
|
204
|
-
};
|
|
205
|
-
return validate(value, 0);
|
|
206
|
-
}
|
|
179
|
+
type AssistantMessage = ProgressAssistantMessage;
|
|
207
180
|
|
|
208
|
-
|
|
209
|
-
function jsonSchemaToTypebox(schema: unknown): TSchema {
|
|
210
|
-
if (!isJsonSchema(schema)) {
|
|
211
|
-
throw new Error("structured output schema must be a bounded JSON object");
|
|
212
|
-
}
|
|
213
|
-
return Type.Unsafe(schema);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* One-shot terminating tool injected when a schema is supplied: the subagent
|
|
218
|
-
* calls it as its final action and we capture the validated object.
|
|
219
|
-
*/
|
|
220
|
-
function makeStructuredOutputTool(
|
|
221
|
-
schema: unknown,
|
|
222
|
-
capture: (value: unknown) => void,
|
|
223
|
-
): ToolDefinition {
|
|
224
|
-
return defineTool({
|
|
225
|
-
name: "structured_output",
|
|
226
|
-
label: "Structured Output",
|
|
227
|
-
description: STRUCTURED_OUTPUT_TOOL_DESCRIPTION,
|
|
228
|
-
parameters: jsonSchemaToTypebox(schema),
|
|
229
|
-
async execute(_toolCallId, params) {
|
|
230
|
-
capture(params);
|
|
231
|
-
return {
|
|
232
|
-
content: [{ type: "text", text: "Recorded structured result." }],
|
|
233
|
-
details: params,
|
|
234
|
-
terminate: true,
|
|
235
|
-
};
|
|
236
|
-
},
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
|
|
181
|
+
export { transcriptFromMessages };
|
|
241
182
|
|
|
242
183
|
export interface AssistantSettlement {
|
|
243
184
|
stopReason: AssistantMessage["stopReason"];
|
|
@@ -273,28 +214,6 @@ export function agentFailureMessage(
|
|
|
273
214
|
return settlement?.errorMessage ?? promptErrorMessage ?? "Agent failed";
|
|
274
215
|
}
|
|
275
216
|
|
|
276
|
-
function finalOutput(messages: AgentMessage[]): string {
|
|
277
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
278
|
-
const msg = messages[i];
|
|
279
|
-
if (msg.role !== "assistant") continue;
|
|
280
|
-
const text = msg.content
|
|
281
|
-
.filter((part) => part.type === "text")
|
|
282
|
-
.map((part) => part.text)
|
|
283
|
-
.join("\n")
|
|
284
|
-
.trim();
|
|
285
|
-
if (text) return text;
|
|
286
|
-
}
|
|
287
|
-
return "";
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function safeJson(value: unknown): string {
|
|
291
|
-
return safeStringify(value, {
|
|
292
|
-
maxBytes: TRANSCRIPT_ENTRY_MAX_BYTES,
|
|
293
|
-
maxDepth: 12,
|
|
294
|
-
maxNodes: 2_000,
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
217
|
/** Record lifecycle timings without inferring completion from message timestamps. */
|
|
299
218
|
export function recordToolExecutionTiming(
|
|
300
219
|
timings: Map<string, ToolExecutionTiming>,
|
|
@@ -319,133 +238,6 @@ export function recordToolExecutionTiming(
|
|
|
319
238
|
});
|
|
320
239
|
}
|
|
321
240
|
|
|
322
|
-
function toolMetadata(
|
|
323
|
-
toolCallId: string,
|
|
324
|
-
timings: ReadonlyMap<string, ToolExecutionTiming>,
|
|
325
|
-
) {
|
|
326
|
-
const timing = timings.get(toolCallId);
|
|
327
|
-
return {
|
|
328
|
-
toolCallId: truncateUtf8(toolCallId, 1024),
|
|
329
|
-
...(timing?.startedAt === undefined ? {} : { startedAt: timing.startedAt }),
|
|
330
|
-
...(timing?.finishedAt === undefined
|
|
331
|
-
? {}
|
|
332
|
-
: { finishedAt: timing.finishedAt }),
|
|
333
|
-
...(timing?.durationMs === undefined
|
|
334
|
-
? {}
|
|
335
|
-
: { durationMs: timing.durationMs }),
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/** Convert pi messages into a compact, serializable transcript for the UI. */
|
|
340
|
-
export function transcriptFromMessages(
|
|
341
|
-
messages: AgentMessage[],
|
|
342
|
-
toolTimings: ReadonlyMap<string, ToolExecutionTiming> = new Map(),
|
|
343
|
-
): TranscriptEntry[] {
|
|
344
|
-
const entries: TranscriptEntry[] = [];
|
|
345
|
-
for (const message of messages) {
|
|
346
|
-
if (message.role === "user") {
|
|
347
|
-
const text =
|
|
348
|
-
typeof message.content === "string"
|
|
349
|
-
? message.content
|
|
350
|
-
: message.content
|
|
351
|
-
.map((part) =>
|
|
352
|
-
part.type === "text" ? part.text : `[image: ${part.mimeType}]`,
|
|
353
|
-
)
|
|
354
|
-
.join("\n");
|
|
355
|
-
if (text.trim()) {
|
|
356
|
-
entries.push({ role: "user", text, timestamp: message.timestamp });
|
|
357
|
-
}
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (message.role === "assistant") {
|
|
362
|
-
for (const part of message.content) {
|
|
363
|
-
if (part.type === "text" && part.text.trim()) {
|
|
364
|
-
entries.push({
|
|
365
|
-
role: "assistant",
|
|
366
|
-
text: part.text,
|
|
367
|
-
timestamp: message.timestamp,
|
|
368
|
-
});
|
|
369
|
-
} else if (part.type === "thinking" && part.thinking.trim()) {
|
|
370
|
-
entries.push({
|
|
371
|
-
role: "thinking",
|
|
372
|
-
text: part.thinking,
|
|
373
|
-
timestamp: message.timestamp,
|
|
374
|
-
});
|
|
375
|
-
} else if (part.type === "toolCall") {
|
|
376
|
-
entries.push({
|
|
377
|
-
role: "tool",
|
|
378
|
-
name: part.name,
|
|
379
|
-
text: safeJson(part.arguments),
|
|
380
|
-
timestamp: message.timestamp,
|
|
381
|
-
...toolMetadata(part.id, toolTimings),
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (message.role !== "toolResult") continue;
|
|
389
|
-
const text = message.content
|
|
390
|
-
.map((part) =>
|
|
391
|
-
part.type === "text" ? part.text : `[image: ${part.mimeType}]`,
|
|
392
|
-
)
|
|
393
|
-
.join("\n");
|
|
394
|
-
entries.push({
|
|
395
|
-
role: "toolResult",
|
|
396
|
-
name: message.toolName,
|
|
397
|
-
text,
|
|
398
|
-
isError: message.isError,
|
|
399
|
-
timestamp: message.timestamp,
|
|
400
|
-
...toolMetadata(message.toolCallId, toolTimings),
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
const selected =
|
|
404
|
-
entries.length <= TRANSCRIPT_MAX_ENTRIES
|
|
405
|
-
? entries
|
|
406
|
-
: [entries[0], ...entries.slice(-(TRANSCRIPT_MAX_ENTRIES - 1))];
|
|
407
|
-
const bounded: TranscriptEntry[] = [];
|
|
408
|
-
let totalBytes = 0;
|
|
409
|
-
for (const entry of selected) {
|
|
410
|
-
const remaining = TRANSCRIPT_TOTAL_MAX_BYTES - totalBytes;
|
|
411
|
-
if (remaining <= 0) break;
|
|
412
|
-
const text = truncateUtf8(
|
|
413
|
-
entry.text,
|
|
414
|
-
Math.min(TRANSCRIPT_ENTRY_MAX_BYTES, remaining),
|
|
415
|
-
);
|
|
416
|
-
totalBytes += Buffer.byteLength(text, "utf8");
|
|
417
|
-
bounded.push({
|
|
418
|
-
...entry,
|
|
419
|
-
text:
|
|
420
|
-
text === entry.text ? text : `${text}\n[transcript entry truncated]`,
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
if (bounded.length < entries.length) {
|
|
424
|
-
bounded.push({
|
|
425
|
-
role: "toolResult",
|
|
426
|
-
name: "transcript",
|
|
427
|
-
text: `[transcript truncated: retained ${bounded.length} of ${entries.length} entries]`,
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
return bounded;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function computeUsage(messages: AgentMessage[]): AgentUsage {
|
|
434
|
-
const usage = emptyUsage();
|
|
435
|
-
for (const msg of messages) {
|
|
436
|
-
if (msg.role !== "assistant") continue;
|
|
437
|
-
usage.turns++;
|
|
438
|
-
const u = msg.usage;
|
|
439
|
-
if (!u) continue;
|
|
440
|
-
usage.input += u.input || 0;
|
|
441
|
-
usage.output += u.output || 0;
|
|
442
|
-
usage.cacheRead += u.cacheRead || 0;
|
|
443
|
-
usage.cacheWrite += u.cacheWrite || 0;
|
|
444
|
-
usage.cost += u.cost?.total || 0;
|
|
445
|
-
}
|
|
446
|
-
return usage;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
241
|
function errorText(error: unknown): string {
|
|
450
242
|
return (error instanceof Error ? error.message : String(error)).slice(
|
|
451
243
|
0,
|
|
@@ -459,35 +251,82 @@ function formatTimeout(timeoutMs: number) {
|
|
|
459
251
|
: `${timeoutMs} ms`;
|
|
460
252
|
}
|
|
461
253
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
254
|
+
export function resolveModelProgressTimeoutMs(
|
|
255
|
+
settingsManager: SettingsManager,
|
|
256
|
+
override?: number,
|
|
257
|
+
) {
|
|
258
|
+
if (override !== undefined) return override;
|
|
259
|
+
const configured =
|
|
260
|
+
settingsManager.getProjectSettings().httpIdleTimeoutMs ??
|
|
261
|
+
settingsManager.getGlobalSettings().httpIdleTimeoutMs;
|
|
262
|
+
return typeof configured === "number" && Number.isFinite(configured)
|
|
263
|
+
? Math.max(MODEL_PROGRESS_TIMEOUT_MS, Math.floor(configured))
|
|
264
|
+
: MODEL_PROGRESS_TIMEOUT_MS;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Abort any provider turn that stops producing model-visible progress. */
|
|
268
|
+
export function createModelProgressWatchdog(
|
|
269
|
+
onTimeout: (error: Error) => Promise<unknown>,
|
|
465
270
|
options: { timeoutMs?: number; model?: string } = {},
|
|
466
271
|
) {
|
|
467
|
-
const timeoutMs = options.timeoutMs ??
|
|
272
|
+
const timeoutMs = options.timeoutMs ?? MODEL_PROGRESS_TIMEOUT_MS;
|
|
468
273
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
274
|
+
let activeTurn = false;
|
|
275
|
+
let closed = false;
|
|
276
|
+
let rejectTimeout!: (error: Error) => void;
|
|
469
277
|
const timeout = new Promise<never>((_resolve, reject) => {
|
|
278
|
+
rejectTimeout = reject;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const clear = () => {
|
|
282
|
+
if (timer) clearTimeout(timer);
|
|
283
|
+
timer = undefined;
|
|
284
|
+
};
|
|
285
|
+
const schedule = () => {
|
|
286
|
+
clear();
|
|
287
|
+
if (!activeTurn || closed) return;
|
|
470
288
|
// This timer owns the awaited watchdog outcome. Keep it referenced so a
|
|
471
289
|
// short-lived Node 22 process cannot exit with the promise still pending.
|
|
472
290
|
timer = setTimeout(() => {
|
|
473
291
|
timer = undefined;
|
|
292
|
+
activeTurn = false;
|
|
293
|
+
closed = true;
|
|
474
294
|
const model = options.model ? ` for ${options.model}` : "";
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
`Agent received no assistant response event${model} within ${formatTimeout(timeoutMs)}; the provider request may be stalled. Retry the workflow.`,
|
|
478
|
-
),
|
|
295
|
+
const error = new Error(
|
|
296
|
+
`Agent provider turn${model} produced no model-visible progress for ${formatTimeout(timeoutMs)}; the provider request may be stalled. Retry the workflow.`,
|
|
479
297
|
);
|
|
480
|
-
|
|
298
|
+
rejectTimeout(error);
|
|
299
|
+
try {
|
|
300
|
+
void onTimeout(error).catch(() => {});
|
|
301
|
+
} catch {
|
|
302
|
+
// The timeout result remains authoritative even if abort throws before
|
|
303
|
+
// returning its promise; bounded shutdown below gets another chance.
|
|
304
|
+
}
|
|
481
305
|
}, timeoutMs);
|
|
482
|
-
}
|
|
483
|
-
|
|
306
|
+
};
|
|
307
|
+
const armTurn = () => {
|
|
308
|
+
if (closed) return;
|
|
309
|
+
activeTurn = true;
|
|
310
|
+
schedule();
|
|
311
|
+
};
|
|
312
|
+
const markProgress = () => {
|
|
313
|
+
if (!activeTurn || closed) return;
|
|
314
|
+
schedule();
|
|
315
|
+
};
|
|
316
|
+
const completeTurn = () => {
|
|
317
|
+
activeTurn = false;
|
|
318
|
+
clear();
|
|
319
|
+
};
|
|
484
320
|
const cancel = () => {
|
|
485
|
-
|
|
486
|
-
|
|
321
|
+
closed = true;
|
|
322
|
+
activeTurn = false;
|
|
323
|
+
clear();
|
|
487
324
|
};
|
|
488
325
|
|
|
489
326
|
return {
|
|
490
|
-
|
|
327
|
+
armTurn,
|
|
328
|
+
markProgress,
|
|
329
|
+
completeTurn,
|
|
491
330
|
cancel,
|
|
492
331
|
async waitFor<T>(operation: Promise<T>) {
|
|
493
332
|
try {
|
|
@@ -499,13 +338,24 @@ export function createFirstResponseWatchdog(
|
|
|
499
338
|
};
|
|
500
339
|
}
|
|
501
340
|
|
|
502
|
-
function
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
341
|
+
function isModelVisibleProgress(event: AgentSessionEvent) {
|
|
342
|
+
if (event.type !== "message_update" || event.message.role !== "assistant") {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
// Raw transport heartbeats never become AgentSession events. Empty stream,
|
|
346
|
+
// text, and thinking starts likewise cannot keep a provider turn alive.
|
|
347
|
+
const update = event.assistantMessageEvent;
|
|
348
|
+
if (
|
|
349
|
+
update.type === "text_delta" ||
|
|
350
|
+
update.type === "thinking_delta" ||
|
|
351
|
+
update.type === "toolcall_delta"
|
|
352
|
+
) {
|
|
353
|
+
return update.delta.length > 0;
|
|
354
|
+
}
|
|
355
|
+
if (update.type === "text_end" || update.type === "thinking_end") {
|
|
356
|
+
return update.content.length > 0;
|
|
357
|
+
}
|
|
358
|
+
return update.type === "toolcall_start" || update.type === "toolcall_end";
|
|
509
359
|
}
|
|
510
360
|
|
|
511
361
|
export async function runAgent(
|
|
@@ -516,11 +366,51 @@ export async function runAgent(
|
|
|
516
366
|
let customTools: ToolDefinition[] | undefined;
|
|
517
367
|
let session: AgentSession | undefined;
|
|
518
368
|
let unsubscribeToolGuards: (() => void) | undefined;
|
|
369
|
+
let aborted = false;
|
|
370
|
+
let terminalCause: "abort" | "model-progress-timeout" | undefined;
|
|
371
|
+
let modelProgressTimeoutMessage: string | undefined;
|
|
372
|
+
let abortOperation: Promise<unknown> | undefined;
|
|
373
|
+
let rejectForAbort: ((error: Error) => void) | undefined;
|
|
374
|
+
let rejectForProjectionFailure: ((error: Error) => void) | undefined;
|
|
375
|
+
const abortRace = new Promise<never>((_resolve, reject) => {
|
|
376
|
+
rejectForAbort = reject;
|
|
377
|
+
});
|
|
378
|
+
const projectionFailureRace = new Promise<never>((_resolve, reject) => {
|
|
379
|
+
rejectForProjectionFailure = reject;
|
|
380
|
+
});
|
|
381
|
+
// The same promise covers startup and prompting. Keep it observed even when
|
|
382
|
+
// a pre-aborted run returns before either race is installed.
|
|
383
|
+
void abortRace.catch(() => {});
|
|
384
|
+
void projectionFailureRace.catch(() => {});
|
|
385
|
+
const abortError = () =>
|
|
386
|
+
options.signal?.reason instanceof Error
|
|
387
|
+
? options.signal.reason
|
|
388
|
+
: new Error("Agent was aborted");
|
|
389
|
+
const onAbort = () => {
|
|
390
|
+
if (aborted) return;
|
|
391
|
+
aborted = true;
|
|
392
|
+
terminalCause ??= "abort";
|
|
393
|
+
if (session) {
|
|
394
|
+
try {
|
|
395
|
+
abortOperation ??= session.abort();
|
|
396
|
+
void abortOperation.catch(() => {});
|
|
397
|
+
} catch (error) {
|
|
398
|
+
abortOperation = Promise.reject(error);
|
|
399
|
+
void abortOperation.catch(() => {});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
rejectForAbort?.(abortError());
|
|
403
|
+
};
|
|
404
|
+
if (options.signal) {
|
|
405
|
+
if (options.signal.aborted) onAbort();
|
|
406
|
+
else options.signal.addEventListener("abort", onAbort, { once: true });
|
|
407
|
+
}
|
|
408
|
+
|
|
519
409
|
try {
|
|
520
410
|
customTools =
|
|
521
411
|
options.schema !== undefined
|
|
522
412
|
? [
|
|
523
|
-
|
|
413
|
+
createStructuredOutputTool(options.schema, (value) => {
|
|
524
414
|
if (!settled) structured = value;
|
|
525
415
|
}),
|
|
526
416
|
]
|
|
@@ -529,7 +419,8 @@ export async function runAgent(
|
|
|
529
419
|
options.tools,
|
|
530
420
|
customTools !== undefined,
|
|
531
421
|
);
|
|
532
|
-
(
|
|
422
|
+
if (aborted) throw abortError();
|
|
423
|
+
const sessionCreation = (options.sessionFactory ?? createAgentSession)({
|
|
533
424
|
cwd: options.cwd,
|
|
534
425
|
...(options.model ? { model: options.model } : {}),
|
|
535
426
|
...(options.thinkingLevel
|
|
@@ -541,8 +432,28 @@ export async function runAgent(
|
|
|
541
432
|
options.sessionManager ?? SessionManager.inMemory(options.cwd),
|
|
542
433
|
...(customTools ? { customTools } : {}),
|
|
543
434
|
...childToolPolicy(childTools),
|
|
544
|
-
})
|
|
545
|
-
|
|
435
|
+
});
|
|
436
|
+
// Promise.race cannot cancel the factory. If cancellation wins, retain
|
|
437
|
+
// ownership of a late session and dispose it without re-entering startup.
|
|
438
|
+
void sessionCreation
|
|
439
|
+
.then(
|
|
440
|
+
({ session: lateSession }) => {
|
|
441
|
+
if (!aborted) return;
|
|
442
|
+
return shutdownAndDisposeChildSession(lateSession, {
|
|
443
|
+
abort: true,
|
|
444
|
+
timeoutMs: options.shutdownTimeoutMs,
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
() => undefined,
|
|
448
|
+
)
|
|
449
|
+
.catch(() => {});
|
|
450
|
+
({ session } = await Promise.race([sessionCreation, abortRace]));
|
|
451
|
+
if (aborted) throw abortError();
|
|
452
|
+
await Promise.race([
|
|
453
|
+
bindChildSessionExtensions(session, childTools),
|
|
454
|
+
abortRace,
|
|
455
|
+
]);
|
|
456
|
+
if (aborted) throw abortError();
|
|
546
457
|
unsubscribeToolGuards = guardWorkflowChildTools(
|
|
547
458
|
session,
|
|
548
459
|
options.toolCallTimeoutMs,
|
|
@@ -551,13 +462,29 @@ export async function runAgent(
|
|
|
551
462
|
} catch (error) {
|
|
552
463
|
settled = true;
|
|
553
464
|
unsubscribeToolGuards?.();
|
|
465
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
554
466
|
const cleanup = session
|
|
555
467
|
? await shutdownAndDisposeChildSession(session, {
|
|
556
468
|
abort: true,
|
|
469
|
+
abortOperation,
|
|
557
470
|
timeoutMs: options.shutdownTimeoutMs,
|
|
558
471
|
})
|
|
559
472
|
: undefined;
|
|
560
473
|
const cleanupError = cleanup?.errors.join("; ");
|
|
474
|
+
if (aborted) {
|
|
475
|
+
return {
|
|
476
|
+
ok: false,
|
|
477
|
+
output: "",
|
|
478
|
+
error: cleanupError
|
|
479
|
+
? `Agent was aborted; Cleanup failed: ${cleanupError}`
|
|
480
|
+
: "Agent was aborted",
|
|
481
|
+
aborted: true,
|
|
482
|
+
usage: emptyUsage(),
|
|
483
|
+
model: options.model?.id,
|
|
484
|
+
contextWindow: options.model?.contextWindow,
|
|
485
|
+
transcript: [],
|
|
486
|
+
};
|
|
487
|
+
}
|
|
561
488
|
return {
|
|
562
489
|
ok: false,
|
|
563
490
|
output: "",
|
|
@@ -571,6 +498,7 @@ export async function runAgent(
|
|
|
571
498
|
}
|
|
572
499
|
|
|
573
500
|
const childSession = session;
|
|
501
|
+
const toolRenderer = new AgentToolRenderLedger();
|
|
574
502
|
let usage = emptyUsage();
|
|
575
503
|
let modelId = childSession.model?.id ?? options.model?.id;
|
|
576
504
|
let contextWindow = childSession.model?.contextWindow;
|
|
@@ -578,21 +506,50 @@ export async function runAgent(
|
|
|
578
506
|
let promptErrorMessage: string | undefined;
|
|
579
507
|
const toolTimings = new Map<string, ToolExecutionTiming>();
|
|
580
508
|
|
|
581
|
-
const
|
|
582
|
-
const
|
|
583
|
-
|
|
509
|
+
const captureToolRenderData = (messages: readonly AgentMessage[]) => {
|
|
510
|
+
for (const message of messages) {
|
|
511
|
+
if (message.role === "assistant") {
|
|
512
|
+
for (const part of message.content) {
|
|
513
|
+
if (part.type !== "toolCall") continue;
|
|
514
|
+
toolRenderer.start(
|
|
515
|
+
part.id,
|
|
516
|
+
part.name,
|
|
517
|
+
part.arguments,
|
|
518
|
+
childSession.getToolDefinition(part.name),
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
} else if (message.role === "toolResult") {
|
|
522
|
+
toolRenderer.end(
|
|
523
|
+
message.toolCallId,
|
|
524
|
+
message.toolName,
|
|
525
|
+
message,
|
|
526
|
+
message.isError,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
};
|
|
584
531
|
|
|
532
|
+
const refreshModel = (latestAssistant?: AssistantMessage) => {
|
|
585
533
|
const sessionModel = childSession.model;
|
|
586
534
|
modelId = sessionModel?.id ?? modelId;
|
|
587
535
|
contextWindow = sessionModel?.contextWindow ?? contextWindow;
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
536
|
+
if (!latestAssistant) return;
|
|
537
|
+
const responseMatchesSession =
|
|
538
|
+
!sessionModel ||
|
|
539
|
+
(latestAssistant.provider === sessionModel.provider &&
|
|
540
|
+
latestAssistant.model === sessionModel.id);
|
|
541
|
+
const reportedId = latestAssistant.responseModel ?? latestAssistant.model;
|
|
542
|
+
const reportedModel = responseMatchesSession
|
|
543
|
+
? options.modelRegistry.find(latestAssistant.provider, reportedId)
|
|
544
|
+
: undefined;
|
|
545
|
+
if (reportedModel) {
|
|
546
|
+
modelId = reportedModel.id;
|
|
547
|
+
contextWindow = reportedModel.contextWindow;
|
|
595
548
|
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const sampleContextTokens = () => {
|
|
552
|
+
const context = childSession.getContextUsage();
|
|
596
553
|
if (
|
|
597
554
|
typeof context?.contextWindow === "number" &&
|
|
598
555
|
Number.isFinite(context.contextWindow) &&
|
|
@@ -600,127 +557,194 @@ export async function runAgent(
|
|
|
600
557
|
) {
|
|
601
558
|
contextWindow = context.contextWindow;
|
|
602
559
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
// latest response rather than a hardcoded/configured guess.
|
|
610
|
-
const responseMatchesSession =
|
|
611
|
-
!sessionModel ||
|
|
612
|
-
(msg.provider === sessionModel.provider &&
|
|
613
|
-
msg.model === sessionModel.id);
|
|
614
|
-
const reportedId = msg.responseModel ?? msg.model;
|
|
615
|
-
const reportedModel = responseMatchesSession
|
|
616
|
-
? options.modelRegistry.find(msg.provider, reportedId)
|
|
617
|
-
: undefined;
|
|
618
|
-
if (reportedModel) {
|
|
619
|
-
modelId = reportedModel.id;
|
|
620
|
-
contextWindow = reportedModel.contextWindow;
|
|
621
|
-
}
|
|
622
|
-
break;
|
|
560
|
+
if (
|
|
561
|
+
typeof context?.tokens === "number" &&
|
|
562
|
+
Number.isFinite(context.tokens) &&
|
|
563
|
+
context.tokens >= 0
|
|
564
|
+
) {
|
|
565
|
+
return context.tokens;
|
|
623
566
|
}
|
|
567
|
+
return context?.tokens === null ? null : undefined;
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const projection = new AgentProgressProjection();
|
|
571
|
+
let lastProjection = projection.snapshot(toolTimings);
|
|
572
|
+
|
|
573
|
+
const snapshotProjection = () => {
|
|
574
|
+
const snapshot = projection.snapshot(toolTimings);
|
|
575
|
+
usage = snapshot.usage;
|
|
576
|
+
refreshModel(snapshot.latestAssistant);
|
|
577
|
+
lastProjection = snapshot;
|
|
578
|
+
return snapshot;
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const reconcileProjection = () => {
|
|
582
|
+
projection.replace(childSession.messages, sampleContextTokens());
|
|
583
|
+
return snapshotProjection();
|
|
624
584
|
};
|
|
625
585
|
|
|
626
|
-
|
|
627
|
-
|
|
586
|
+
const emitProgress = () => {
|
|
587
|
+
const snapshot = snapshotProjection();
|
|
588
|
+
options.onProgress?.({
|
|
589
|
+
preview: snapshot.preview,
|
|
590
|
+
usage,
|
|
591
|
+
model: modelId,
|
|
592
|
+
contextWindow,
|
|
593
|
+
transcript: bindWorkflowToolRenderer(snapshot.transcript, toolRenderer),
|
|
594
|
+
});
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
let armModelProgress = () => {};
|
|
598
|
+
let markModelProgress = () => {};
|
|
599
|
+
let completeModelTurn = () => {};
|
|
600
|
+
let cancelModelProgressWatchdog = () => {};
|
|
601
|
+
let compactionReconcileQueued = false;
|
|
602
|
+
const queueCompactionReconcile = () => {
|
|
603
|
+
if (compactionReconcileQueued) return;
|
|
604
|
+
compactionReconcileQueued = true;
|
|
605
|
+
queueMicrotask(() => {
|
|
606
|
+
compactionReconcileQueued = false;
|
|
607
|
+
if (settled) return;
|
|
608
|
+
// Pi may synchronously remove a restored overflow assistant immediately
|
|
609
|
+
// after compaction_end. Read canonical state after that mutation.
|
|
610
|
+
try {
|
|
611
|
+
reconcileProjection();
|
|
612
|
+
emitProgress();
|
|
613
|
+
} catch (error) {
|
|
614
|
+
promptErrorMessage ??= `Failed to reconcile agent progress after compaction: ${errorText(error)}`;
|
|
615
|
+
rejectForProjectionFailure?.(new Error(promptErrorMessage));
|
|
616
|
+
try {
|
|
617
|
+
abortOperation ??= childSession.abort();
|
|
618
|
+
void abortOperation.catch(() => {});
|
|
619
|
+
} catch {
|
|
620
|
+
// The projection failure remains authoritative; bounded cleanup gets
|
|
621
|
+
// another chance to abort and dispose the child below.
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
};
|
|
628
626
|
const unsubscribe = childSession.subscribe((event) => {
|
|
629
627
|
if (settled) return;
|
|
630
|
-
if (
|
|
628
|
+
if (event.type === "turn_start") armModelProgress();
|
|
629
|
+
if (event.type === "tool_execution_start") {
|
|
630
|
+
toolRenderer.start(
|
|
631
|
+
event.toolCallId,
|
|
632
|
+
event.toolName,
|
|
633
|
+
event.args,
|
|
634
|
+
childSession.getToolDefinition(event.toolName),
|
|
635
|
+
);
|
|
636
|
+
} else if (event.type === "tool_execution_update") {
|
|
637
|
+
toolRenderer.update(
|
|
638
|
+
event.toolCallId,
|
|
639
|
+
event.toolName,
|
|
640
|
+
event.args,
|
|
641
|
+
event.partialResult,
|
|
642
|
+
);
|
|
643
|
+
} else if (event.type === "tool_execution_end") {
|
|
644
|
+
toolRenderer.end(
|
|
645
|
+
event.toolCallId,
|
|
646
|
+
event.toolName,
|
|
647
|
+
event.result,
|
|
648
|
+
event.isError,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
if (isModelVisibleProgress(event)) markModelProgress();
|
|
652
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
653
|
+
completeModelTurn();
|
|
654
|
+
}
|
|
631
655
|
if (event.type === "message_end") {
|
|
632
656
|
assistantSettlement = observeAssistantSettlement(
|
|
633
657
|
assistantSettlement,
|
|
634
658
|
event.message,
|
|
635
659
|
);
|
|
660
|
+
// Pi publishes the finalized message before tool lifecycle delivery.
|
|
661
|
+
// Hydrate native rendering from this one message without reading history.
|
|
662
|
+
captureToolRenderData([event.message]);
|
|
663
|
+
projection.append(event.message);
|
|
636
664
|
}
|
|
637
665
|
if (
|
|
638
666
|
event.type === "tool_execution_start" ||
|
|
639
667
|
event.type === "tool_execution_end"
|
|
640
668
|
) {
|
|
641
669
|
recordToolExecutionTiming(toolTimings, event);
|
|
642
|
-
} else if (
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
) {
|
|
670
|
+
} else if (event.type === "compaction_end") {
|
|
671
|
+
queueCompactionReconcile();
|
|
672
|
+
return;
|
|
673
|
+
} else if (event.type !== "message_end") {
|
|
646
674
|
return;
|
|
647
675
|
}
|
|
648
|
-
|
|
649
|
-
options.onProgress?.({
|
|
650
|
-
preview: finalOutput(childSession.messages),
|
|
651
|
-
usage,
|
|
652
|
-
model: modelId,
|
|
653
|
-
contextWindow,
|
|
654
|
-
transcript: transcriptFromMessages(childSession.messages, toolTimings),
|
|
655
|
-
});
|
|
676
|
+
emitProgress();
|
|
656
677
|
});
|
|
657
678
|
|
|
658
|
-
let aborted = false;
|
|
659
|
-
let abortOperation: Promise<unknown> | undefined;
|
|
660
|
-
let rejectForAbort: ((error: Error) => void) | undefined;
|
|
661
|
-
const abortRace = new Promise<never>((_resolve, reject) => {
|
|
662
|
-
rejectForAbort = reject;
|
|
663
|
-
});
|
|
664
|
-
const onAbort = () => {
|
|
665
|
-
if (aborted) return;
|
|
666
|
-
aborted = true;
|
|
667
|
-
try {
|
|
668
|
-
abortOperation ??= childSession.abort();
|
|
669
|
-
void abortOperation.catch(() => {});
|
|
670
|
-
} catch (error) {
|
|
671
|
-
abortOperation = Promise.reject(error);
|
|
672
|
-
void abortOperation.catch(() => {});
|
|
673
|
-
}
|
|
674
|
-
rejectForAbort?.(
|
|
675
|
-
options.signal?.reason instanceof Error
|
|
676
|
-
? options.signal.reason
|
|
677
|
-
: new Error("Agent was aborted"),
|
|
678
|
-
);
|
|
679
|
-
};
|
|
680
|
-
if (options.signal) {
|
|
681
|
-
if (options.signal.aborted) onAbort();
|
|
682
|
-
else options.signal.addEventListener("abort", onAbort, { once: true });
|
|
683
|
-
}
|
|
684
|
-
|
|
685
679
|
let output = "";
|
|
686
680
|
let transcript: TranscriptEntry[] = [];
|
|
687
681
|
let cleanupErrors: string[] = [];
|
|
688
682
|
try {
|
|
683
|
+
// Operator reuse may restore messages before this activation subscribes.
|
|
684
|
+
// Hydrate both bounded projections once; ordinary progress never rescans it.
|
|
685
|
+
projection.replace(childSession.messages, sampleContextTokens());
|
|
686
|
+
captureToolRenderData(childSession.messages);
|
|
687
|
+
snapshotProjection();
|
|
689
688
|
if (!aborted) {
|
|
690
|
-
const watchdog =
|
|
691
|
-
() => {
|
|
689
|
+
const watchdog = createModelProgressWatchdog(
|
|
690
|
+
(error) => {
|
|
691
|
+
terminalCause ??= "model-progress-timeout";
|
|
692
|
+
if (terminalCause === "model-progress-timeout") {
|
|
693
|
+
modelProgressTimeoutMessage ??= error.message;
|
|
694
|
+
}
|
|
692
695
|
abortOperation ??= childSession.abort();
|
|
693
696
|
void abortOperation.catch(() => {});
|
|
694
697
|
return abortOperation;
|
|
695
698
|
},
|
|
696
699
|
{
|
|
697
|
-
timeoutMs:
|
|
700
|
+
timeoutMs: resolveModelProgressTimeoutMs(
|
|
701
|
+
options.settingsManager,
|
|
702
|
+
options.modelProgressTimeoutMs,
|
|
703
|
+
),
|
|
698
704
|
model: modelId,
|
|
699
705
|
},
|
|
700
706
|
);
|
|
701
|
-
|
|
702
|
-
|
|
707
|
+
armModelProgress = watchdog.armTurn;
|
|
708
|
+
markModelProgress = watchdog.markProgress;
|
|
709
|
+
completeModelTurn = watchdog.completeTurn;
|
|
710
|
+
cancelModelProgressWatchdog = watchdog.cancel;
|
|
703
711
|
await Promise.race([
|
|
704
712
|
watchdog.waitFor(
|
|
705
713
|
childSession.prompt(buildWorkflowAgentPrompt(options.prompt)),
|
|
706
714
|
),
|
|
707
715
|
abortRace,
|
|
716
|
+
projectionFailureRace,
|
|
708
717
|
]);
|
|
709
718
|
}
|
|
710
719
|
} catch (error) {
|
|
711
|
-
promptErrorMessage
|
|
720
|
+
promptErrorMessage ??= errorText(error);
|
|
712
721
|
} finally {
|
|
713
|
-
|
|
722
|
+
cancelModelProgressWatchdog();
|
|
714
723
|
options.signal?.removeEventListener("abort", onAbort);
|
|
715
724
|
settled = true;
|
|
716
725
|
unsubscribe();
|
|
717
726
|
unsubscribeToolGuards?.();
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
727
|
+
try {
|
|
728
|
+
const finalProjection = reconcileProjection();
|
|
729
|
+
output = truncateUtf8(finalProjection.preview, AGENT_OUTPUT_MAX_BYTES);
|
|
730
|
+
transcript = bindWorkflowToolRenderer(
|
|
731
|
+
finalProjection.transcript,
|
|
732
|
+
toolRenderer,
|
|
733
|
+
);
|
|
734
|
+
} catch (error) {
|
|
735
|
+
promptErrorMessage ??= `Failed to reconcile final agent progress: ${errorText(error)}`;
|
|
736
|
+
// A broken canonical accessor must not prevent owned-session cleanup.
|
|
737
|
+
// Preserve the last projection that was fully observed instead.
|
|
738
|
+
try {
|
|
739
|
+
output = truncateUtf8(lastProjection.preview, AGENT_OUTPUT_MAX_BYTES);
|
|
740
|
+
transcript = bindWorkflowToolRenderer(
|
|
741
|
+
lastProjection.transcript,
|
|
742
|
+
toolRenderer,
|
|
743
|
+
);
|
|
744
|
+
} catch (fallbackError) {
|
|
745
|
+
promptErrorMessage += `; failed to render last progress: ${errorText(fallbackError)}`;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
724
748
|
const cleanup = await shutdownAndDisposeChildSession(childSession, {
|
|
725
749
|
abort: aborted || promptErrorMessage !== undefined,
|
|
726
750
|
abortOperation,
|
|
@@ -734,7 +758,11 @@ export async function runAgent(
|
|
|
734
758
|
? `Cleanup failed: ${cleanupErrors.join("; ")}`
|
|
735
759
|
: undefined;
|
|
736
760
|
|
|
737
|
-
if (
|
|
761
|
+
if (
|
|
762
|
+
terminalCause === "abort" ||
|
|
763
|
+
(terminalCause === undefined &&
|
|
764
|
+
assistantSettlement?.stopReason === "aborted")
|
|
765
|
+
) {
|
|
738
766
|
return {
|
|
739
767
|
ok: false,
|
|
740
768
|
output,
|
|
@@ -751,7 +779,9 @@ export async function runAgent(
|
|
|
751
779
|
}
|
|
752
780
|
|
|
753
781
|
const failureMessage =
|
|
754
|
-
|
|
782
|
+
(terminalCause === "model-progress-timeout"
|
|
783
|
+
? modelProgressTimeoutMessage
|
|
784
|
+
: agentFailureMessage(assistantSettlement, promptErrorMessage)) ??
|
|
755
785
|
cleanupError;
|
|
756
786
|
if (failureMessage !== undefined) {
|
|
757
787
|
return {
|
|
@@ -781,6 +811,20 @@ export async function runAgent(
|
|
|
781
811
|
};
|
|
782
812
|
}
|
|
783
813
|
|
|
814
|
+
if (options.schema === undefined && assistantSettlement === undefined) {
|
|
815
|
+
return {
|
|
816
|
+
ok: false,
|
|
817
|
+
output,
|
|
818
|
+
structured,
|
|
819
|
+
error: "Agent finished without an assistant response.",
|
|
820
|
+
aborted: false,
|
|
821
|
+
usage,
|
|
822
|
+
model: modelId,
|
|
823
|
+
contextWindow,
|
|
824
|
+
transcript,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
784
828
|
return {
|
|
785
829
|
ok: true,
|
|
786
830
|
output,
|