@tt-a1i/openpi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +643 -0
  2. package/SETUP.md +74 -0
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/openpi-package.png +0 -0
  5. package/assets/readme-hero-mobile.svg +72 -0
  6. package/assets/readme-hero.svg +118 -0
  7. package/assets/readme-runtime-mobile.svg +91 -0
  8. package/assets/readme-runtime.svg +111 -0
  9. package/extensions/ask-user/handoff.ts +205 -0
  10. package/extensions/ask-user/index.ts +1110 -0
  11. package/extensions/ask-user/limits.ts +89 -0
  12. package/extensions/ask-user/prompt.ts +76 -0
  13. package/extensions/background-terminals/index.ts +653 -0
  14. package/extensions/background-terminals/src/domain.ts +99 -0
  15. package/extensions/background-terminals/src/manager.ts +989 -0
  16. package/extensions/background-terminals/src/output.ts +84 -0
  17. package/extensions/background-terminals/src/prompt.ts +195 -0
  18. package/extensions/background-terminals/src/result-delivery.ts +43 -0
  19. package/extensions/background-terminals/src/runtime.ts +36 -0
  20. package/extensions/background-terminals/src/ui/output-view.ts +55 -0
  21. package/extensions/background-terminals/src/ui/ps.ts +642 -0
  22. package/extensions/background-terminals/src/ui/tool-result.ts +146 -0
  23. package/extensions/background-terminals/src/watch.ts +192 -0
  24. package/extensions/context-pivot/index.ts +222 -0
  25. package/extensions/copy-all/index.ts +65 -0
  26. package/extensions/cron/index.ts +173 -0
  27. package/extensions/cron/schedule.ts +127 -0
  28. package/extensions/file-mutation-display/index.ts +105 -0
  29. package/extensions/file-mutation-display/render.ts +107 -0
  30. package/extensions/file-search/index.ts +515 -0
  31. package/extensions/file-search/src/args.ts +129 -0
  32. package/extensions/file-search/src/binaries.ts +419 -0
  33. package/extensions/file-search/src/output.ts +142 -0
  34. package/extensions/file-search/src/process.ts +309 -0
  35. package/extensions/file-search/src/prompt.ts +53 -0
  36. package/extensions/git-info/index.ts +272 -0
  37. package/extensions/git-info/src/changed-files-view.ts +414 -0
  38. package/extensions/git-info/src/process.ts +107 -0
  39. package/extensions/git-info/src/refresh-coordinator.ts +13 -0
  40. package/extensions/git-info/src/runtime.ts +28 -0
  41. package/extensions/goal/controller.ts +794 -0
  42. package/extensions/goal/index.ts +521 -0
  43. package/extensions/goal/prompts.ts +122 -0
  44. package/extensions/goal/state.ts +763 -0
  45. package/extensions/goal/ui.ts +158 -0
  46. package/extensions/model-info/index.ts +234 -0
  47. package/extensions/plan-mode/bash-policy.ts +313 -0
  48. package/extensions/plan-mode/index.ts +539 -0
  49. package/extensions/post-edit/index.ts +129 -0
  50. package/extensions/sessions/LICENSE.upstream +21 -0
  51. package/extensions/sessions/git-stats.ts +226 -0
  52. package/extensions/sessions/index.ts +1092 -0
  53. package/extensions/sessions/sessions.ts +385 -0
  54. package/extensions/setup/index.ts +408 -0
  55. package/extensions/shared/activity-status.ts +65 -0
  56. package/extensions/shared/below-editor-navigation.ts +343 -0
  57. package/extensions/shared/child-session.ts +352 -0
  58. package/extensions/shared/context-utilization.ts +47 -0
  59. package/extensions/shared/dashboard-state.ts +102 -0
  60. package/extensions/shared/plan-mode-state.ts +65 -0
  61. package/extensions/shared/setup-config.ts +971 -0
  62. package/extensions/shared/subagent-roles.ts +22 -0
  63. package/extensions/shared/terminal-text.ts +38 -0
  64. package/extensions/shared/tool-call-timeout.ts +104 -0
  65. package/extensions/shared/worktree.ts +526 -0
  66. package/extensions/subagents/index.ts +1225 -0
  67. package/extensions/subagents/navigation.ts +121 -0
  68. package/extensions/subagents/src/agent-types.ts +543 -0
  69. package/extensions/subagents/src/backend.ts +63 -0
  70. package/extensions/subagents/src/backends/pi.ts +493 -0
  71. package/extensions/subagents/src/backends/stub.ts +296 -0
  72. package/extensions/subagents/src/by-the-way.ts +21 -0
  73. package/extensions/subagents/src/domain.ts +271 -0
  74. package/extensions/subagents/src/format.ts +48 -0
  75. package/extensions/subagents/src/manager.ts +769 -0
  76. package/extensions/subagents/src/prompt.ts +190 -0
  77. package/extensions/subagents/src/result-delivery.ts +20 -0
  78. package/extensions/subagents/src/runtime.ts +51 -0
  79. package/extensions/subagents/src/ui/takeover.ts +615 -0
  80. package/extensions/subagents/src/ui/transcript.ts +293 -0
  81. package/extensions/subagents/src/ui/wait-result.ts +89 -0
  82. package/extensions/suggestions/index.ts +172 -0
  83. package/extensions/suggestions/src/config.ts +12 -0
  84. package/extensions/suggestions/src/predictor.ts +147 -0
  85. package/extensions/suggestions/src/prompt.ts +20 -0
  86. package/extensions/suggestions/src/transcript.ts +233 -0
  87. package/extensions/suggestions/src/ui.ts +224 -0
  88. package/extensions/tasks/index.ts +512 -0
  89. package/extensions/tasks/tasks.ts +649 -0
  90. package/extensions/tasks/ui.ts +421 -0
  91. package/extensions/turn-time/index.ts +61 -0
  92. package/extensions/ui-customization/footer.ts +512 -0
  93. package/extensions/ui-customization/index.ts +217 -0
  94. package/extensions/workflows/acceptance.ts +298 -0
  95. package/extensions/workflows/artifacts.ts +225 -0
  96. package/extensions/workflows/controller.ts +210 -0
  97. package/extensions/workflows/dashboard.ts +1226 -0
  98. package/extensions/workflows/index.ts +1884 -0
  99. package/extensions/workflows/journal.ts +188 -0
  100. package/extensions/workflows/meta.ts +250 -0
  101. package/extensions/workflows/model.ts +423 -0
  102. package/extensions/workflows/navigation.ts +93 -0
  103. package/extensions/workflows/prompt.ts +212 -0
  104. package/extensions/workflows/replay-safety.ts +577 -0
  105. package/extensions/workflows/runner.ts +786 -0
  106. package/extensions/workflows/sandbox-child.cjs +402 -0
  107. package/extensions/workflows/sandbox.ts +397 -0
  108. package/extensions/workflows/serialization.ts +162 -0
  109. package/extensions/workflows/worktree-handoff.ts +216 -0
  110. package/package.json +87 -0
  111. package/scripts/prepare-effect-tsgo.mjs +16 -0
  112. package/skills/background-terminals/SKILL.md +30 -0
  113. package/skills/subagents/SKILL.md +15 -0
  114. package/themes/github-dark-default.json +89 -0
@@ -0,0 +1,786 @@
1
+ /**
2
+ * Workflow subagent runner.
3
+ *
4
+ * Each `agent()` call in a workflow script becomes one isolated in-process
5
+ * AgentSession created here: in-memory session, normal trust-aware resources
6
+ * and extensions, recursive orchestration/user-prompt tools denied, and an
7
+ * optional one-shot `structured_output` tool when a schema is supplied.
8
+ *
9
+ * `runAgent()` never throws: every failure mode (session creation, provider
10
+ * errors, aborts, missing structured output) settles into an `AgentOutcome`.
11
+ */
12
+
13
+ import {
14
+ createAgentSession,
15
+ DefaultResourceLoader,
16
+ defineTool,
17
+ SessionManager,
18
+ SettingsManager,
19
+ type AgentSession,
20
+ type AgentSessionEvent,
21
+ type AgentSessionEventListener,
22
+ type ExtensionAPI,
23
+ type ExtensionContext,
24
+ type ToolDefinition,
25
+ } from "@earendil-works/pi-coding-agent";
26
+ import { Type, type TSchema } from "typebox";
27
+ import {
28
+ bindChildSessionExtensions,
29
+ childToolPolicy,
30
+ createChildResources,
31
+ shutdownAndDisposeChildSession,
32
+ } from "../shared/child-session.ts";
33
+ import { createToolCallTimeoutGuard } from "../shared/tool-call-timeout.ts";
34
+ import { emptyUsage, type AgentUsage, type TranscriptEntry } from "./model.ts";
35
+ import {
36
+ createReplayFilesystemBoundary,
37
+ type ReplayFilesystemBoundaryOptions,
38
+ } from "./replay-safety.ts";
39
+ import {
40
+ buildWorkflowAgentPrompt,
41
+ STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION,
42
+ STRUCTURED_OUTPUT_TOOL_DESCRIPTION,
43
+ } from "./prompt.ts";
44
+ import { safeStringify, truncateUtf8 } from "./serialization.ts";
45
+
46
+ const AGENT_OUTPUT_MAX_BYTES = 64 * 1024;
47
+ export const FIRST_RESPONSE_TIMEOUT_MS = 45_000;
48
+ const TRANSCRIPT_ENTRY_MAX_BYTES = 16 * 1024;
49
+ const TRANSCRIPT_TOTAL_MAX_BYTES = 256 * 1024;
50
+ const TRANSCRIPT_MAX_ENTRIES = 200;
51
+
52
+ export type WorkflowModel = NonNullable<ExtensionContext["model"]>;
53
+ export type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
54
+ type AgentMessage = AgentSession["messages"][number];
55
+ type ToolTimingEvent = Extract<
56
+ AgentSessionEvent,
57
+ { type: "tool_execution_start" | "tool_execution_end" }
58
+ >;
59
+
60
+ export interface ToolExecutionTiming {
61
+ startedAt?: number;
62
+ finishedAt?: number;
63
+ durationMs?: number;
64
+ }
65
+
66
+ export interface AgentOutcome {
67
+ ok: boolean;
68
+ /** Final assistant text (may be empty when only structured output was produced). */
69
+ output: string;
70
+ /** Captured structured_output payload when a schema was supplied. */
71
+ structured?: unknown;
72
+ error?: string;
73
+ aborted: boolean;
74
+ usage: AgentUsage;
75
+ model?: string;
76
+ contextWindow?: number;
77
+ transcript: TranscriptEntry[];
78
+ }
79
+
80
+ export interface AgentProgress {
81
+ preview: string;
82
+ usage: AgentUsage;
83
+ model?: string;
84
+ contextWindow?: number;
85
+ transcript: TranscriptEntry[];
86
+ }
87
+
88
+ export type WorkflowAgentSessionFactory = (
89
+ options: Parameters<typeof createAgentSession>[0],
90
+ ) => Promise<{ session: AgentSession }>;
91
+
92
+ export interface RunAgentOptions {
93
+ prompt: string;
94
+ schema?: unknown;
95
+ model?: WorkflowModel;
96
+ thinkingLevel?: ThinkingLevel;
97
+ cwd: string;
98
+ loader: DefaultResourceLoader;
99
+ settingsManager: SettingsManager;
100
+ modelRegistry: ExtensionContext["modelRegistry"];
101
+ /** Agent Type allowlist; childToolPolicy can only narrow capabilities. */
102
+ tools?: readonly string[];
103
+ signal?: AbortSignal;
104
+ onProgress?: (progress: AgentProgress) => void;
105
+ /** Canonical repository boundary required before this call can be journaled. */
106
+ replayFilesystemBoundary?: ReplayFilesystemBoundaryOptions;
107
+ /** Test-only override for the per-tool execution timeout. */
108
+ toolCallTimeoutMs?: number;
109
+ /** Test-only override for the first assistant response-event timeout. */
110
+ firstResponseTimeoutMs?: number;
111
+ /** Test-only override for the end-to-end abort/shutdown deadline. */
112
+ shutdownTimeoutMs?: number;
113
+ /** Test seam for lifecycle races; production always uses createAgentSession. */
114
+ sessionFactory?: WorkflowAgentSessionFactory;
115
+ }
116
+
117
+ /** Build a fresh extension runtime for each concurrent workflow child. */
118
+ export function createWorkflowResources(
119
+ cwd: string,
120
+ variant: "plain" | "structured",
121
+ projectTrusted: boolean,
122
+ agentTypePrompt?: string,
123
+ ) {
124
+ const appendSystemPrompt = [
125
+ ...(agentTypePrompt ? [agentTypePrompt] : []),
126
+ ...(variant === "structured" ? [STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION] : []),
127
+ ];
128
+ return createChildResources({
129
+ cwd,
130
+ projectTrusted,
131
+ ...(appendSystemPrompt.length > 0 ? { appendSystemPrompt } : {}),
132
+ });
133
+ }
134
+
135
+ export function workflowChildTools(
136
+ tools: readonly string[] | undefined,
137
+ structured: boolean,
138
+ ) {
139
+ return tools
140
+ ? [...new Set([...tools, ...(structured ? ["structured_output"] : [])])]
141
+ : undefined;
142
+ }
143
+
144
+ interface WorkflowToolSession {
145
+ getAllTools(): Array<{
146
+ name: string;
147
+ sourceInfo?: { path: string; source: string; origin: string };
148
+ }>;
149
+ getToolDefinition(name: string): ToolDefinition | undefined;
150
+ subscribe(listener: AgentSessionEventListener): () => void;
151
+ }
152
+
153
+ /** Guard current tools and tools registered by extensions at later agent starts. */
154
+ export function guardWorkflowChildTools(
155
+ session: WorkflowToolSession,
156
+ timeoutMs?: number,
157
+ replayFilesystemBoundary?: ReplayFilesystemBoundaryOptions,
158
+ ) {
159
+ const boundary = replayFilesystemBoundary
160
+ ? createReplayFilesystemBoundary(replayFilesystemBoundary)
161
+ : undefined;
162
+ const timeout = createToolCallTimeoutGuard(timeoutMs);
163
+ const apply = () => {
164
+ // Keep the replay boundary outermost: if the timeout rejects before a
165
+ // cooperative tool finishes aborting, path revalidation still completes
166
+ // before the child can use or journal the timeout result.
167
+ timeout.apply(session);
168
+ boundary?.apply(session);
169
+ };
170
+ apply();
171
+ return session.subscribe((event) => {
172
+ if (event.type === "agent_start") apply();
173
+ });
174
+ }
175
+
176
+ function isJsonSchema(value: unknown): value is TSchema {
177
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
178
+ const seen = new WeakSet<object>();
179
+ let nodes = 0;
180
+ const validate = (current: unknown, depth: number): boolean => {
181
+ if (++nodes > 10_000 || depth > 24) return false;
182
+ if (
183
+ current === null ||
184
+ typeof current === "string" ||
185
+ typeof current === "boolean"
186
+ ) {
187
+ return true;
188
+ }
189
+ if (typeof current === "number") return Number.isFinite(current);
190
+ if (Array.isArray(current)) {
191
+ return current.every((item) => validate(item, depth + 1));
192
+ }
193
+ if (typeof current !== "object") return false;
194
+ if (seen.has(current)) return false;
195
+ seen.add(current);
196
+ return Object.keys(current).every((key) => {
197
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
198
+ return false;
199
+ }
200
+ return validate((current as Record<string, unknown>)[key], depth + 1);
201
+ });
202
+ };
203
+ return validate(value, 0);
204
+ }
205
+
206
+ /** Preserve the caller's full JSON Schema instead of lossy keyword conversion. */
207
+ function jsonSchemaToTypebox(schema: unknown): TSchema {
208
+ if (!isJsonSchema(schema)) {
209
+ throw new Error("structured output schema must be a bounded JSON object");
210
+ }
211
+ return Type.Unsafe(schema);
212
+ }
213
+
214
+ /**
215
+ * One-shot terminating tool injected when a schema is supplied: the subagent
216
+ * calls it as its final action and we capture the validated object.
217
+ */
218
+ function makeStructuredOutputTool(
219
+ schema: unknown,
220
+ capture: (value: unknown) => void,
221
+ ): ToolDefinition {
222
+ return defineTool({
223
+ name: "structured_output",
224
+ label: "Structured Output",
225
+ description: STRUCTURED_OUTPUT_TOOL_DESCRIPTION,
226
+ parameters: jsonSchemaToTypebox(schema),
227
+ async execute(_toolCallId, params) {
228
+ capture(params);
229
+ return {
230
+ content: [{ type: "text", text: "Recorded structured result." }],
231
+ details: params,
232
+ terminate: true,
233
+ };
234
+ },
235
+ });
236
+ }
237
+
238
+ type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
239
+
240
+ export interface AssistantSettlement {
241
+ stopReason: AssistantMessage["stopReason"];
242
+ errorMessage?: string;
243
+ }
244
+
245
+ /**
246
+ * Observe assistant message_end events instead of rescanning mutable Pi state:
247
+ * overflow recovery temporarily removes the failed assistant from that state.
248
+ */
249
+ export function observeAssistantSettlement(
250
+ previous: AssistantSettlement | undefined,
251
+ message: AgentMessage | undefined,
252
+ ) {
253
+ if (message?.role !== "assistant") return previous;
254
+ return {
255
+ stopReason: message.stopReason,
256
+ errorMessage: message.errorMessage,
257
+ };
258
+ }
259
+
260
+ export function agentFailureMessage(
261
+ settlement: AssistantSettlement | undefined,
262
+ promptErrorMessage?: string,
263
+ ) {
264
+ if (
265
+ settlement?.stopReason !== "error" &&
266
+ settlement?.errorMessage === undefined &&
267
+ promptErrorMessage === undefined
268
+ ) {
269
+ return undefined;
270
+ }
271
+ return settlement?.errorMessage ?? promptErrorMessage ?? "Agent failed";
272
+ }
273
+
274
+ function finalOutput(messages: AgentMessage[]): string {
275
+ for (let i = messages.length - 1; i >= 0; i--) {
276
+ const msg = messages[i];
277
+ if (msg.role !== "assistant") continue;
278
+ const text = msg.content
279
+ .filter((part) => part.type === "text")
280
+ .map((part) => part.text)
281
+ .join("\n")
282
+ .trim();
283
+ if (text) return text;
284
+ }
285
+ return "";
286
+ }
287
+
288
+ function safeJson(value: unknown): string {
289
+ return safeStringify(value, {
290
+ maxBytes: TRANSCRIPT_ENTRY_MAX_BYTES,
291
+ maxDepth: 12,
292
+ maxNodes: 2_000,
293
+ });
294
+ }
295
+
296
+ /** Record lifecycle timings without inferring completion from message timestamps. */
297
+ export function recordToolExecutionTiming(
298
+ timings: Map<string, ToolExecutionTiming>,
299
+ event: ToolTimingEvent,
300
+ observedAt = Date.now(),
301
+ ) {
302
+ const previous = timings.get(event.toolCallId);
303
+ if (event.type === "tool_execution_start") {
304
+ if (previous?.startedAt !== undefined) return;
305
+ timings.set(event.toolCallId, { ...previous, startedAt: observedAt });
306
+ return;
307
+ }
308
+ if (previous?.finishedAt !== undefined) return;
309
+ const durationMs =
310
+ previous?.startedAt === undefined
311
+ ? undefined
312
+ : Math.max(0, observedAt - previous.startedAt);
313
+ timings.set(event.toolCallId, {
314
+ ...previous,
315
+ finishedAt: observedAt,
316
+ ...(durationMs === undefined ? {} : { durationMs }),
317
+ });
318
+ }
319
+
320
+ function toolMetadata(
321
+ toolCallId: string,
322
+ timings: ReadonlyMap<string, ToolExecutionTiming>,
323
+ ) {
324
+ const timing = timings.get(toolCallId);
325
+ return {
326
+ toolCallId: truncateUtf8(toolCallId, 1024),
327
+ ...(timing?.startedAt === undefined ? {} : { startedAt: timing.startedAt }),
328
+ ...(timing?.finishedAt === undefined
329
+ ? {}
330
+ : { finishedAt: timing.finishedAt }),
331
+ ...(timing?.durationMs === undefined
332
+ ? {}
333
+ : { durationMs: timing.durationMs }),
334
+ };
335
+ }
336
+
337
+ /** Convert pi messages into a compact, serializable transcript for the UI. */
338
+ export function transcriptFromMessages(
339
+ messages: AgentMessage[],
340
+ toolTimings: ReadonlyMap<string, ToolExecutionTiming> = new Map(),
341
+ ): TranscriptEntry[] {
342
+ const entries: TranscriptEntry[] = [];
343
+ for (const message of messages) {
344
+ if (message.role === "user") {
345
+ const text =
346
+ typeof message.content === "string"
347
+ ? message.content
348
+ : message.content
349
+ .map((part) =>
350
+ part.type === "text" ? part.text : `[image: ${part.mimeType}]`,
351
+ )
352
+ .join("\n");
353
+ if (text.trim()) {
354
+ entries.push({ role: "user", text, timestamp: message.timestamp });
355
+ }
356
+ continue;
357
+ }
358
+
359
+ if (message.role === "assistant") {
360
+ for (const part of message.content) {
361
+ if (part.type === "text" && part.text.trim()) {
362
+ entries.push({
363
+ role: "assistant",
364
+ text: part.text,
365
+ timestamp: message.timestamp,
366
+ });
367
+ } else if (part.type === "thinking" && part.thinking.trim()) {
368
+ entries.push({
369
+ role: "thinking",
370
+ text: part.thinking,
371
+ timestamp: message.timestamp,
372
+ });
373
+ } else if (part.type === "toolCall") {
374
+ entries.push({
375
+ role: "tool",
376
+ name: part.name,
377
+ text: safeJson(part.arguments),
378
+ timestamp: message.timestamp,
379
+ ...toolMetadata(part.id, toolTimings),
380
+ });
381
+ }
382
+ }
383
+ continue;
384
+ }
385
+
386
+ if (message.role !== "toolResult") continue;
387
+ const text = message.content
388
+ .map((part) =>
389
+ part.type === "text" ? part.text : `[image: ${part.mimeType}]`,
390
+ )
391
+ .join("\n");
392
+ entries.push({
393
+ role: "toolResult",
394
+ name: message.toolName,
395
+ text,
396
+ isError: message.isError,
397
+ timestamp: message.timestamp,
398
+ ...toolMetadata(message.toolCallId, toolTimings),
399
+ });
400
+ }
401
+ const selected =
402
+ entries.length <= TRANSCRIPT_MAX_ENTRIES
403
+ ? entries
404
+ : [entries[0], ...entries.slice(-(TRANSCRIPT_MAX_ENTRIES - 1))];
405
+ const bounded: TranscriptEntry[] = [];
406
+ let totalBytes = 0;
407
+ for (const entry of selected) {
408
+ const remaining = TRANSCRIPT_TOTAL_MAX_BYTES - totalBytes;
409
+ if (remaining <= 0) break;
410
+ const text = truncateUtf8(
411
+ entry.text,
412
+ Math.min(TRANSCRIPT_ENTRY_MAX_BYTES, remaining),
413
+ );
414
+ totalBytes += Buffer.byteLength(text, "utf8");
415
+ bounded.push({
416
+ ...entry,
417
+ text:
418
+ text === entry.text ? text : `${text}\n[transcript entry truncated]`,
419
+ });
420
+ }
421
+ if (bounded.length < entries.length) {
422
+ bounded.push({
423
+ role: "toolResult",
424
+ name: "transcript",
425
+ text: `[transcript truncated: retained ${bounded.length} of ${entries.length} entries]`,
426
+ });
427
+ }
428
+ return bounded;
429
+ }
430
+
431
+ function computeUsage(messages: AgentMessage[]): AgentUsage {
432
+ const usage = emptyUsage();
433
+ for (const msg of messages) {
434
+ if (msg.role !== "assistant") continue;
435
+ usage.turns++;
436
+ const u = msg.usage;
437
+ if (!u) continue;
438
+ usage.input += u.input || 0;
439
+ usage.output += u.output || 0;
440
+ usage.cacheRead += u.cacheRead || 0;
441
+ usage.cacheWrite += u.cacheWrite || 0;
442
+ usage.cost += u.cost?.total || 0;
443
+ }
444
+ return usage;
445
+ }
446
+
447
+ function errorText(error: unknown): string {
448
+ return (error instanceof Error ? error.message : String(error)).slice(
449
+ 0,
450
+ 16 * 1024,
451
+ );
452
+ }
453
+
454
+ function formatTimeout(timeoutMs: number) {
455
+ return timeoutMs % 1_000 === 0
456
+ ? `${timeoutMs / 1_000} seconds`
457
+ : `${timeoutMs} ms`;
458
+ }
459
+
460
+ /** Abort a provider call that opens but never emits its first assistant event. */
461
+ export function createFirstResponseWatchdog(
462
+ onTimeout: () => Promise<unknown>,
463
+ options: { timeoutMs?: number; model?: string } = {},
464
+ ) {
465
+ const timeoutMs = options.timeoutMs ?? FIRST_RESPONSE_TIMEOUT_MS;
466
+ let timer: ReturnType<typeof setTimeout> | undefined;
467
+ const timeout = new Promise<never>((_resolve, reject) => {
468
+ timer = setTimeout(() => {
469
+ timer = undefined;
470
+ const model = options.model ? ` for ${options.model}` : "";
471
+ reject(
472
+ new Error(
473
+ `Agent received no assistant response event${model} within ${formatTimeout(timeoutMs)}; the provider request may be stalled. Retry the workflow.`,
474
+ ),
475
+ );
476
+ void onTimeout().catch(() => {});
477
+ }, timeoutMs);
478
+ timer.unref?.();
479
+ });
480
+
481
+ const cancel = () => {
482
+ if (timer) clearTimeout(timer);
483
+ timer = undefined;
484
+ };
485
+
486
+ return {
487
+ markResponse: cancel,
488
+ async waitFor<T>(operation: Promise<T>) {
489
+ try {
490
+ return await Promise.race([operation, timeout]);
491
+ } finally {
492
+ cancel();
493
+ }
494
+ },
495
+ };
496
+ }
497
+
498
+ function isAssistantResponseEvent(event: AgentSessionEvent) {
499
+ return (
500
+ (event.type === "message_start" ||
501
+ event.type === "message_update" ||
502
+ event.type === "message_end") &&
503
+ event.message.role === "assistant"
504
+ );
505
+ }
506
+
507
+ export async function runAgent(
508
+ options: RunAgentOptions,
509
+ ): Promise<AgentOutcome> {
510
+ let structured: unknown;
511
+ let settled = false;
512
+ let customTools: ToolDefinition[] | undefined;
513
+ let session: AgentSession | undefined;
514
+ let unsubscribeToolGuards: (() => void) | undefined;
515
+ try {
516
+ customTools =
517
+ options.schema !== undefined
518
+ ? [
519
+ makeStructuredOutputTool(options.schema, (value) => {
520
+ if (!settled) structured = value;
521
+ }),
522
+ ]
523
+ : undefined;
524
+ const childTools = workflowChildTools(
525
+ options.tools,
526
+ customTools !== undefined,
527
+ );
528
+ ({ session } = await (options.sessionFactory ?? createAgentSession)({
529
+ cwd: options.cwd,
530
+ ...(options.model ? { model: options.model } : {}),
531
+ ...(options.thinkingLevel
532
+ ? { thinkingLevel: options.thinkingLevel }
533
+ : {}),
534
+ resourceLoader: options.loader,
535
+ settingsManager: options.settingsManager,
536
+ sessionManager: SessionManager.inMemory(options.cwd),
537
+ ...(customTools ? { customTools } : {}),
538
+ ...childToolPolicy(childTools),
539
+ }));
540
+ await bindChildSessionExtensions(session, childTools);
541
+ unsubscribeToolGuards = guardWorkflowChildTools(
542
+ session,
543
+ options.toolCallTimeoutMs,
544
+ options.replayFilesystemBoundary,
545
+ );
546
+ } catch (error) {
547
+ settled = true;
548
+ unsubscribeToolGuards?.();
549
+ const cleanup = session
550
+ ? await shutdownAndDisposeChildSession(session, {
551
+ abort: true,
552
+ timeoutMs: options.shutdownTimeoutMs,
553
+ })
554
+ : undefined;
555
+ const cleanupError = cleanup?.errors.join("; ");
556
+ return {
557
+ ok: false,
558
+ output: "",
559
+ error: `Failed to create agent session: ${errorText(error)}${cleanupError ? `; cleanup failed: ${cleanupError}` : ""}`,
560
+ aborted: false,
561
+ usage: emptyUsage(),
562
+ model: options.model?.id,
563
+ contextWindow: options.model?.contextWindow,
564
+ transcript: [],
565
+ };
566
+ }
567
+
568
+ const childSession = session;
569
+ let usage = emptyUsage();
570
+ let modelId = childSession.model?.id ?? options.model?.id;
571
+ let contextWindow = childSession.model?.contextWindow;
572
+ let assistantSettlement: AssistantSettlement | undefined;
573
+ let promptErrorMessage: string | undefined;
574
+ const toolTimings = new Map<string, ToolExecutionTiming>();
575
+
576
+ const sync = () => {
577
+ const messages = childSession.messages;
578
+ usage = computeUsage(messages);
579
+
580
+ const sessionModel = childSession.model;
581
+ modelId = sessionModel?.id ?? modelId;
582
+ contextWindow = sessionModel?.contextWindow ?? contextWindow;
583
+ const context = childSession.getContextUsage();
584
+ if (
585
+ typeof context?.tokens === "number" &&
586
+ Number.isFinite(context.tokens) &&
587
+ context.tokens >= 0
588
+ ) {
589
+ usage.contextTokens = context.tokens;
590
+ }
591
+ if (
592
+ typeof context?.contextWindow === "number" &&
593
+ Number.isFinite(context.contextWindow) &&
594
+ context.contextWindow > 0
595
+ ) {
596
+ contextWindow = context.contextWindow;
597
+ }
598
+
599
+ for (let i = messages.length - 1; i >= 0; i--) {
600
+ const msg = messages[i];
601
+ if (msg.role !== "assistant") continue;
602
+ // Some gateways report a concrete fallback model. Prefer its registry
603
+ // metadata when available so capacity tracks the model that served the
604
+ // latest response rather than a hardcoded/configured guess.
605
+ const responseMatchesSession =
606
+ !sessionModel ||
607
+ (msg.provider === sessionModel.provider &&
608
+ msg.model === sessionModel.id);
609
+ const reportedId = msg.responseModel ?? msg.model;
610
+ const reportedModel = responseMatchesSession
611
+ ? options.modelRegistry.find(msg.provider, reportedId)
612
+ : undefined;
613
+ if (reportedModel) {
614
+ modelId = reportedModel.id;
615
+ contextWindow = reportedModel.contextWindow;
616
+ }
617
+ break;
618
+ }
619
+ };
620
+
621
+ let markFirstResponse = () => {};
622
+ const unsubscribe = childSession.subscribe((event) => {
623
+ if (settled) return;
624
+ if (isAssistantResponseEvent(event)) markFirstResponse();
625
+ if (event.type === "message_end") {
626
+ assistantSettlement = observeAssistantSettlement(
627
+ assistantSettlement,
628
+ event.message,
629
+ );
630
+ }
631
+ if (
632
+ event.type === "tool_execution_start" ||
633
+ event.type === "tool_execution_end"
634
+ ) {
635
+ recordToolExecutionTiming(toolTimings, event);
636
+ } else if (
637
+ event.type !== "message_end" &&
638
+ event.type !== "compaction_end"
639
+ ) {
640
+ return;
641
+ }
642
+ sync();
643
+ options.onProgress?.({
644
+ preview: finalOutput(childSession.messages),
645
+ usage,
646
+ model: modelId,
647
+ contextWindow,
648
+ transcript: transcriptFromMessages(childSession.messages, toolTimings),
649
+ });
650
+ });
651
+
652
+ let aborted = false;
653
+ let abortOperation: Promise<unknown> | undefined;
654
+ let rejectForAbort: ((error: Error) => void) | undefined;
655
+ const abortRace = new Promise<never>((_resolve, reject) => {
656
+ rejectForAbort = reject;
657
+ });
658
+ const onAbort = () => {
659
+ if (aborted) return;
660
+ aborted = true;
661
+ try {
662
+ abortOperation ??= childSession.abort();
663
+ void abortOperation.catch(() => {});
664
+ } catch (error) {
665
+ abortOperation = Promise.reject(error);
666
+ void abortOperation.catch(() => {});
667
+ }
668
+ rejectForAbort?.(
669
+ options.signal?.reason instanceof Error
670
+ ? options.signal.reason
671
+ : new Error("Agent was aborted"),
672
+ );
673
+ };
674
+ if (options.signal) {
675
+ if (options.signal.aborted) onAbort();
676
+ else options.signal.addEventListener("abort", onAbort, { once: true });
677
+ }
678
+
679
+ let output = "";
680
+ let transcript: TranscriptEntry[] = [];
681
+ let cleanupErrors: string[] = [];
682
+ try {
683
+ if (!aborted) {
684
+ const watchdog = createFirstResponseWatchdog(
685
+ () => {
686
+ abortOperation ??= childSession.abort();
687
+ void abortOperation.catch(() => {});
688
+ return abortOperation;
689
+ },
690
+ {
691
+ timeoutMs: options.firstResponseTimeoutMs,
692
+ model: modelId,
693
+ },
694
+ );
695
+ markFirstResponse = watchdog.markResponse;
696
+ await Promise.race([
697
+ watchdog.waitFor(
698
+ childSession.prompt(buildWorkflowAgentPrompt(options.prompt)),
699
+ ),
700
+ abortRace,
701
+ ]);
702
+ }
703
+ } catch (error) {
704
+ promptErrorMessage = errorText(error);
705
+ } finally {
706
+ options.signal?.removeEventListener("abort", onAbort);
707
+ settled = true;
708
+ unsubscribe();
709
+ unsubscribeToolGuards?.();
710
+ sync();
711
+ output = truncateUtf8(
712
+ finalOutput(childSession.messages),
713
+ AGENT_OUTPUT_MAX_BYTES,
714
+ );
715
+ transcript = transcriptFromMessages(childSession.messages, toolTimings);
716
+ const cleanup = await shutdownAndDisposeChildSession(childSession, {
717
+ abort: aborted || promptErrorMessage !== undefined,
718
+ abortOperation,
719
+ timeoutMs: options.shutdownTimeoutMs,
720
+ });
721
+ cleanupErrors = cleanup.errors;
722
+ }
723
+
724
+ const cleanupError =
725
+ cleanupErrors.length > 0
726
+ ? `Cleanup failed: ${cleanupErrors.join("; ")}`
727
+ : undefined;
728
+
729
+ if (aborted || assistantSettlement?.stopReason === "aborted") {
730
+ return {
731
+ ok: false,
732
+ output,
733
+ structured,
734
+ error: cleanupError
735
+ ? `Agent was aborted; ${cleanupError}`
736
+ : "Agent was aborted",
737
+ aborted: true,
738
+ usage,
739
+ model: modelId,
740
+ contextWindow,
741
+ transcript,
742
+ };
743
+ }
744
+
745
+ const failureMessage =
746
+ agentFailureMessage(assistantSettlement, promptErrorMessage) ??
747
+ cleanupError;
748
+ if (failureMessage !== undefined) {
749
+ return {
750
+ ok: false,
751
+ output,
752
+ structured,
753
+ error: failureMessage,
754
+ aborted: false,
755
+ usage,
756
+ model: modelId,
757
+ contextWindow,
758
+ transcript,
759
+ };
760
+ }
761
+
762
+ if (options.schema !== undefined && structured === undefined) {
763
+ return {
764
+ ok: false,
765
+ output,
766
+ error:
767
+ "Agent finished without calling structured_output; no structured result matching the schema was produced.",
768
+ aborted: false,
769
+ usage,
770
+ model: modelId,
771
+ contextWindow,
772
+ transcript,
773
+ };
774
+ }
775
+
776
+ return {
777
+ ok: true,
778
+ output,
779
+ structured,
780
+ aborted: false,
781
+ usage,
782
+ model: modelId,
783
+ contextWindow,
784
+ transcript,
785
+ };
786
+ }