@agentionai/agents 1.11.0 → 1.13.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.
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.OpenAiAgent = void 0;
7
7
  exports.lowestReasoningEffort = lowestReasoningEffort;
8
+ exports.wrapErrorBodyFetch = wrapErrorBodyFetch;
9
+ exports.describeOpenAIError = describeOpenAIError;
8
10
  const openai_1 = __importDefault(require("openai"));
9
11
  const BaseAgent_1 = require("../BaseAgent");
10
12
  const AgentEvent_1 = require("../AgentEvent");
@@ -38,6 +40,81 @@ function lowestReasoningEffort(model) {
38
40
  const group = model_types_1.OPENAI_REASONING_SUPPORT.find((entry) => entry.models.includes(base));
39
41
  return group?.efforts[0];
40
42
  }
43
+ /**
44
+ * `fetch` wrapper that rewrites a non-OpenAI-shaped error body into the shape
45
+ * the SDK can read.
46
+ *
47
+ * `APIError.generate` takes the message from `body.error` and throws the rest
48
+ * away (`openai/core/error.js`), so a backend that reports failures as
49
+ * `{"detail": "..."}` — which the ChatGPT Codex endpoint does, for all four of
50
+ * its body validations plus auth failures — surfaces as the useless
51
+ * `400 status code (no body)`. Nesting the original body under `error` puts the
52
+ * real reason back in the thrown error.
53
+ *
54
+ * Only touches error responses; successful (streaming) responses pass straight
55
+ * through untouched.
56
+ */
57
+ function wrapErrorBodyFetch(baseFetch = fetch) {
58
+ return async (input, init) => {
59
+ const res = await baseFetch(input, init);
60
+ if (res.ok)
61
+ return res;
62
+ const text = await res.text().catch(() => "");
63
+ let body = text;
64
+ try {
65
+ const parsed = JSON.parse(text);
66
+ if (parsed && typeof parsed === "object" && !("error" in parsed)) {
67
+ body = JSON.stringify({
68
+ error: {
69
+ message: typeof parsed.detail === "string"
70
+ ? parsed.detail
71
+ : JSON.stringify(parsed),
72
+ ...parsed,
73
+ },
74
+ });
75
+ }
76
+ }
77
+ catch {
78
+ // Not JSON (an HTML error page, say) — hand the text back unchanged so
79
+ // the SDK reports it as the message.
80
+ }
81
+ // Reading the body consumed it, so the Response has to be rebuilt. Drop the
82
+ // length/encoding headers, which no longer describe the new payload.
83
+ const headers = new Headers(res.headers);
84
+ headers.delete("content-length");
85
+ headers.delete("content-encoding");
86
+ // `globalThis.Response`, not `Response`: this module imports the Responses
87
+ // API's `Response` *type*, which shadows the global class name here.
88
+ return new globalThis.Response(body, {
89
+ status: res.status,
90
+ statusText: res.statusText,
91
+ headers,
92
+ });
93
+ };
94
+ }
95
+ /**
96
+ * Pull a human-readable message out of an OpenAI-shaped error.
97
+ *
98
+ * `api.openai.com` answers with `{ error: { message, code } }`, but not every
99
+ * host behind this SDK does — the ChatGPT Codex backend reports its validation
100
+ * failures as `{ detail: "Instructions are required" }`. Reading
101
+ * `error.error.message` blindly turns those into a `TypeError` that hides the
102
+ * real cause, so every field is probed defensively and the SDK's own `message`
103
+ * is the last resort.
104
+ */
105
+ function describeOpenAIError(error) {
106
+ const err = error;
107
+ const body = err?.error;
108
+ const fromBody = typeof body === "string"
109
+ ? body
110
+ : (body?.message ?? body?.detail ?? undefined);
111
+ return {
112
+ message: fromBody ?? err?.detail ?? err?.message ?? "Unknown error",
113
+ code: typeof body === "object" ? body?.code : undefined,
114
+ status: err?.status,
115
+ body: body ?? err?.detail,
116
+ };
117
+ }
41
118
  /**
42
119
  * Agent for OpenAI models using the Responses API.
43
120
  *
@@ -54,17 +131,39 @@ function lowestReasoningEffort(model) {
54
131
  * ```
55
132
  */
56
133
  class OpenAiAgent extends BaseAgent_1.BaseAgent {
134
+ /**
135
+ * Whether a non-streaming call must be issued as a stream and collapsed.
136
+ * `false` here; `CodexAgent` overrides it, since that backend refuses
137
+ * `stream: false` outright.
138
+ */
139
+ get forceStreaming() {
140
+ return false;
141
+ }
142
+ /**
143
+ * Last chance to reshape a request body before it goes out. Identity here —
144
+ * `CodexAgent` overrides it to satisfy that backend's extra validations.
145
+ */
146
+ transformRequestParams(params) {
147
+ return params;
148
+ }
57
149
  constructor(config, history) {
150
+ // Cast: `BaseAgentConfig.apiKey` is `string`, while this agent also accepts
151
+ // a token-returning function. BaseAgent never reads the field — it only
152
+ // declares it — so widening the base config for one provider would be the
153
+ // more invasive fix.
58
154
  super({ ...config, vendor: "openai" }, history);
59
155
  /** Count of tool calls in current execution */
60
156
  this.currentToolCallCount = 0;
157
+ // Merge flat config (deprecated) with nested vendorConfig
158
+ // Flat config takes precedence for backward compatibility
159
+ const vendorConfig = config.vendorConfig?.openai || {};
160
+ const baseURL = config.baseURL ?? vendorConfig.baseURL;
61
161
  this.client = new openai_1.default({
62
162
  apiKey: config.apiKey,
163
+ baseURL,
63
164
  defaultHeaders: config.defaultHeaders,
165
+ fetch: config.fetch,
64
166
  });
65
- // Merge flat config (deprecated) with nested vendorConfig
66
- // Flat config takes precedence for backward compatibility
67
- const vendorConfig = config.vendorConfig?.openai || {};
68
167
  const disableParallelToolUse = config.disableParallelToolUse ??
69
168
  vendorConfig.disableParallelToolUse ??
70
169
  false;
@@ -87,6 +186,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
87
186
  user,
88
187
  builtInTools,
89
188
  apiKey: config.apiKey,
189
+ baseURL,
90
190
  temperature: config.temperature,
91
191
  topP: config.topP,
92
192
  seed: config.seed,
@@ -111,6 +211,8 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
111
211
  id: model.id,
112
212
  created: model.created ? new Date(model.created * 1000) : undefined,
113
213
  ownedBy: model.owned_by,
214
+ // Cast: this implementation always returns OpenAI's own cards; a
215
+ // subclass that reports a different shape overrides the whole method.
114
216
  raw: model,
115
217
  }));
116
218
  }
@@ -118,6 +220,11 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
118
220
  throw new AgentError_1.ExecutionError(`Failed to list OpenAI models: ${error instanceof Error ? error.message : "Unknown error"}`);
119
221
  }
120
222
  }
223
+ /** The configured key, resolving the function form if that is what was given. */
224
+ async resolveApiKey() {
225
+ const key = this.config.apiKey;
226
+ return typeof key === "function" ? await key() : (key ?? "");
227
+ }
121
228
  getToolDefinitions() {
122
229
  return Array.from(this.tools.values()).map((tool) => {
123
230
  const prompt = tool.getPrompt();
@@ -152,6 +259,66 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
152
259
  ...(this.config.builtInTools ?? []),
153
260
  ];
154
261
  }
262
+ /**
263
+ * Rebuild a terminal response's `output` from the items streamed alongside it.
264
+ *
265
+ * The Codex backend sends `response.completed` with `output: []` and no
266
+ * `output_text`, unlike the platform API which fills both in — the content
267
+ * only ever arrives as `response.output_item.done` events. Everything
268
+ * downstream (tool-call detection, the text written to history) reads
269
+ * `output`, so without this a Codex turn silently commits an empty assistant
270
+ * message and drops every tool call.
271
+ *
272
+ * A no-op wherever `output` is already populated, so the platform path is
273
+ * untouched.
274
+ */
275
+ repairStreamedOutput(response, streamedItems) {
276
+ if (response.output?.length || streamedItems.length === 0)
277
+ return response;
278
+ const output = streamedItems;
279
+ const outputText = output
280
+ .filter((item) => item.type === "message")
281
+ .flatMap((item) => ("content" in item ? (item.content ?? []) : []))
282
+ .filter((part) => part?.type === "output_text")
283
+ .map((part) => ("text" in part ? part.text : ""))
284
+ .join("");
285
+ return { ...response, output, output_text: outputText };
286
+ }
287
+ /**
288
+ * Issue a non-streaming Responses API call.
289
+ *
290
+ * When {@link forceStreaming} is set the request is streamed and the terminal
291
+ * event's `response` handed back instead — giving callers the same `Response`
292
+ * either way, at the cost of buffering the turn.
293
+ */
294
+ async createResponse(params, requestOptions) {
295
+ const body = this.transformRequestParams(params);
296
+ if (!this.forceStreaming) {
297
+ return this.client.responses.create({ ...body, stream: false }, requestOptions);
298
+ }
299
+ const stream = (await this.client.responses.create({ ...body, stream: true }, requestOptions));
300
+ let terminal;
301
+ const streamedItems = [];
302
+ for await (const event of stream) {
303
+ // Collected because the Codex backend leaves `output` empty on the
304
+ // terminal event — see repairStreamedOutput().
305
+ if (event.type === "response.output_item.done") {
306
+ streamedItems.push(event.item);
307
+ }
308
+ // `incomplete` and `failed` carry a Response too — handleResponse()
309
+ // already reads `status` off it, so let it report the reason rather than
310
+ // failing here with a vaguer message.
311
+ if (event.type === "response.completed" ||
312
+ event.type === "response.incomplete" ||
313
+ event.type === "response.failed") {
314
+ terminal = event.response;
315
+ }
316
+ }
317
+ if (!terminal) {
318
+ throw new AgentError_1.ExecutionError("OpenAI stream ended without a terminal response event");
319
+ }
320
+ return this.repairStreamedOutput(terminal, streamedItems);
321
+ }
155
322
  /**
156
323
  * Build the `reasoning` field for a Responses API request, as an object to
157
324
  * spread into the request params.
@@ -190,6 +357,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
190
357
  this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
191
358
  // Reset token usage for this execution
192
359
  this.resetTokenUsage();
360
+ this.resetPartialTurn();
193
361
  this.currentToolCallCount = 0;
194
362
  const inputPreview = typeof input === "string" ? input : JSON.stringify(input);
195
363
  // Start visualization reporting
@@ -216,7 +384,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
216
384
  try {
217
385
  const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
218
386
  this.startTurnTimer();
219
- const response = await this.client.responses.create({
387
+ const response = await this.createResponse({
220
388
  model: this.config.model,
221
389
  max_output_tokens: this.config.maxTokens,
222
390
  input: inputMessages,
@@ -241,16 +409,16 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
241
409
  throw abortError;
242
410
  }
243
411
  if (error && typeof error === "object" && "error" in error) {
244
- const openAIError = error;
245
- const apiError = new AgentError_1.ApiError(`OpenAI API error: ${openAIError.error.message || "Unknown error"}`, openAIError.status, openAIError.error);
246
- if (openAIError.error.code === "insufficient_quota") {
412
+ const openAIError = describeOpenAIError(error);
413
+ const apiError = new AgentError_1.ApiError(`OpenAI API error: ${openAIError.message}`, openAIError.status, openAIError.body);
414
+ if (openAIError.code === "insufficient_quota") {
247
415
  apiError.message =
248
416
  "OpenAI API quota exceeded. Please check your billing details.";
249
417
  }
250
418
  this.emit(AgentEvent_1.AgentEvent.ERROR, apiError);
251
419
  // Report error to viz
252
420
  if (this.vizEventId) {
253
- VizReporter_1.vizReporter.agentError(this.vizEventId, "ApiError", apiError.message, openAIError.error.code === "rate_limit_exceeded");
421
+ VizReporter_1.vizReporter.agentError(this.vizEventId, "ApiError", apiError.message, openAIError.code === "rate_limit_exceeded");
254
422
  this.vizEventId = undefined;
255
423
  }
256
424
  throw apiError;
@@ -343,7 +511,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
343
511
  try {
344
512
  const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
345
513
  this.startTurnTimer();
346
- const newResponse = await this.client.responses.create({
514
+ const newResponse = await this.createResponse({
347
515
  model: this.config.model,
348
516
  max_output_tokens: this.config.maxTokens,
349
517
  input: inputMessages,
@@ -360,8 +528,8 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
360
528
  }
361
529
  catch (error) {
362
530
  if (error && typeof error === "object" && "error" in error) {
363
- const openAIError = error;
364
- const apiError = new AgentError_1.ApiError(`OpenAI API error during tool response: ${openAIError.error.message || "Unknown error"}`, openAIError.status, openAIError.error);
531
+ const openAIError = describeOpenAIError(error);
532
+ const apiError = new AgentError_1.ApiError(`OpenAI API error during tool response: ${openAIError.message}`, openAIError.status, openAIError.body);
365
533
  this.emit(AgentEvent_1.AgentEvent.ERROR, apiError);
366
534
  throw apiError;
367
535
  }
@@ -461,6 +629,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
461
629
  async *executeStream(input, options) {
462
630
  this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
463
631
  this.resetTokenUsage();
632
+ this.resetPartialTurn();
464
633
  this.currentToolCallCount = 0;
465
634
  const inputPreview = typeof input === "string" ? input : JSON.stringify(input);
466
635
  if (VizConfig_1.vizConfig.isEnabled()) {
@@ -488,7 +657,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
488
657
  VizReporter_1.vizReporter.agentError(this.vizEventId, "AbortError", abortError.message, false);
489
658
  this.vizEventId = undefined;
490
659
  }
491
- throw abortError;
660
+ throw this.withPartialTurn(abortError);
492
661
  }
493
662
  if (error instanceof AgentError_1.AgentError) {
494
663
  this.emit(AgentEvent_1.AgentEvent.ERROR, error);
@@ -496,17 +665,17 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
496
665
  VizReporter_1.vizReporter.agentError(this.vizEventId, error.constructor.name, error.message, false);
497
666
  this.vizEventId = undefined;
498
667
  }
499
- throw error;
668
+ throw this.withPartialTurn(error);
500
669
  }
501
670
  if (error && typeof error === "object" && "error" in error) {
502
- const openAIError = error;
503
- const apiError = new AgentError_1.ApiError(`OpenAI API error: ${openAIError.error.message || "Unknown error"}`, openAIError.status, openAIError.error);
671
+ const openAIError = describeOpenAIError(error);
672
+ const apiError = new AgentError_1.ApiError(`OpenAI API error: ${openAIError.message}`, openAIError.status, openAIError.body);
504
673
  this.emit(AgentEvent_1.AgentEvent.ERROR, apiError);
505
674
  if (this.vizEventId) {
506
- VizReporter_1.vizReporter.agentError(this.vizEventId, "ApiError", apiError.message, openAIError.error.code === "rate_limit_exceeded");
675
+ VizReporter_1.vizReporter.agentError(this.vizEventId, "ApiError", apiError.message, openAIError.code === "rate_limit_exceeded");
507
676
  this.vizEventId = undefined;
508
677
  }
509
- throw apiError;
678
+ throw this.withPartialTurn(apiError);
510
679
  }
511
680
  const executionError = new AgentError_1.ExecutionError(`OpenAI error: ${error instanceof Error ? error.message : "Unknown error"}`);
512
681
  this.emit(AgentEvent_1.AgentEvent.ERROR, executionError);
@@ -514,7 +683,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
514
683
  VizReporter_1.vizReporter.agentError(this.vizEventId, "ExecutionError", executionError.message, false);
515
684
  this.vizEventId = undefined;
516
685
  }
517
- throw executionError;
686
+ throw this.withPartialTurn(executionError);
518
687
  }
519
688
  finally {
520
689
  this.history.endExecution();
@@ -523,7 +692,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
523
692
  async *streamTurn(options) {
524
693
  const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
525
694
  this.startTurnTimer();
526
- const stream = await this.client.responses.create({
695
+ const stream = await this.client.responses.create(this.transformRequestParams({
527
696
  model: this.config.model,
528
697
  max_output_tokens: this.config.maxTokens,
529
698
  input: inputMessages,
@@ -534,70 +703,131 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
534
703
  top_p: this.config.topP,
535
704
  user: this.config.user,
536
705
  ...this.buildReasoningParams("auto"),
537
- }, { signal: options?.signal });
706
+ }), { signal: options?.signal });
538
707
  let completedEvent = null;
539
- for await (const event of stream) {
540
- if (event.type === "response.output_text.delta") {
541
- this.markFirstToken();
542
- this.emit(AgentEvent_1.AgentEvent.CHUNK, event.delta);
543
- yield { type: "text", content: event.delta };
708
+ const streamedItems = [];
709
+ // The Responses API builds the committed turn out of `response.completed`,
710
+ // which only arrives on success, so the deltas are mirrored here as well:
711
+ // without them a stream that dies mid-flight leaves nothing behind at all,
712
+ // and a reasoning summary can be minutes of generation.
713
+ let textDelta = "";
714
+ let reasoningDelta = "";
715
+ const partialCalls = new Map();
716
+ // Set once this frame's assistant message reaches history.
717
+ let committed = false;
718
+ let failure;
719
+ try {
720
+ for await (const event of stream) {
721
+ if (event.type === "response.output_text.delta") {
722
+ this.markFirstToken();
723
+ // Accumulated as well as yielded purely so the `finally` below can hand
724
+ // it back if the stream dies: the committed turn is rebuilt from
725
+ // `response.completed`, which never arrives on a failure.
726
+ textDelta += event.delta;
727
+ this.emit(AgentEvent_1.AgentEvent.CHUNK, event.delta);
728
+ yield { type: "text", content: event.delta };
729
+ }
730
+ if (event.type === "response.reasoning_summary_text.delta") {
731
+ this.markFirstToken();
732
+ reasoningDelta += event.delta;
733
+ this.emit(AgentEvent_1.AgentEvent.REASONING_CHUNK, event.delta);
734
+ yield { type: "reasoning", content: event.delta };
735
+ }
736
+ if (event.type === "response.output_item.added") {
737
+ const item = event.item;
738
+ if (item.type === "function_call") {
739
+ partialCalls.set(event.output_index, {
740
+ id: item.call_id || item.id || "",
741
+ name: item.name ?? "",
742
+ arguments: "",
743
+ });
744
+ }
745
+ }
746
+ if (event.type === "response.function_call_arguments.delta") {
747
+ const acc = partialCalls.get(event.output_index);
748
+ if (acc)
749
+ acc.arguments += event.delta;
750
+ }
751
+ if (event.type === "response.output_item.done") {
752
+ // The Codex backend leaves `output` empty on the terminal event, so
753
+ // the finished items are kept here — see repairStreamedOutput().
754
+ streamedItems.push(event.item);
755
+ }
756
+ if (event.type === "response.completed") {
757
+ completedEvent = event;
758
+ if (event.response.usage) {
759
+ this.accumulateUsage(this.parseUsage(event.response.usage));
760
+ }
761
+ }
762
+ if (event.type === "response.incomplete") {
763
+ throw new AgentError_1.MaxTokensExceededError("Response incomplete: max tokens reached", this.config.maxTokens);
764
+ }
544
765
  }
545
- if (event.type === "response.reasoning_summary_text.delta") {
546
- this.markFirstToken();
547
- this.emit(AgentEvent_1.AgentEvent.REASONING_CHUNK, event.delta);
548
- yield { type: "reasoning", content: event.delta };
766
+ // The SDK's stream iterator swallows the abort and simply stops yielding.
767
+ // Without this the turn would fail as a malformed stream instead of a
768
+ // cancellation — checked here so the tokens already spent are reported.
769
+ (0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
770
+ if (!completedEvent) {
771
+ throw new AgentError_1.ExecutionError("OpenAI stream ended without a completed event");
549
772
  }
550
- if (event.type === "response.completed") {
551
- completedEvent = event;
552
- if (event.response.usage) {
553
- this.accumulateUsage(this.parseUsage(event.response.usage));
773
+ const response = this.repairStreamedOutput(completedEvent.response, streamedItems);
774
+ const toolCalls = response.output.filter((o) => o.type === "function_call");
775
+ if (toolCalls.length > 0) {
776
+ // As in handleResponse(): bail out before the assistant turn is written,
777
+ // so a cancelled run leaves no unanswered function call in history.
778
+ (0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
779
+ this.emit(AgentEvent_1.AgentEvent.TOOL_USE, toolCalls);
780
+ this.currentToolCallCount += toolCalls.length;
781
+ const functionCalls = toolCalls.map((tc) => ({
782
+ id: tc.id || tc.call_id,
783
+ call_id: tc.call_id,
784
+ name: tc.name,
785
+ arguments: tc.arguments,
786
+ }));
787
+ const assistantEntry = transformers_1.openAiTransformer.fromProviderMessage("assistant", response.output_text || "", functionCalls);
788
+ this.addToHistory(assistantEntry);
789
+ committed = true;
790
+ const toolResults = await this.handleToolUse(toolCalls, options);
791
+ for (const result of toolResults) {
792
+ this.addToHistory(transformers_1.openAiTransformer.toolResultEntry(result.call_id, result.output, false));
554
793
  }
794
+ yield* this.streamTurn(options);
555
795
  }
556
- if (event.type === "response.incomplete") {
557
- throw new AgentError_1.MaxTokensExceededError("Response incomplete: max tokens reached", this.config.maxTokens);
796
+ else {
797
+ const textContent = response.output_text || "";
798
+ const entry = transformers_1.openAiTransformer.fromProviderMessage("assistant", textContent);
799
+ this.addToHistory(entry);
800
+ committed = true;
801
+ this.emit(AgentEvent_1.AgentEvent.DONE, response, this.lastTokenUsage);
802
+ if (this.vizEventId) {
803
+ VizReporter_1.vizReporter.agentComplete(this.vizEventId, {
804
+ input: this.lastTokenUsage?.input_tokens || 0,
805
+ output: this.lastTokenUsage?.output_tokens || 0,
806
+ total: this.lastTokenUsage?.total_tokens || 0,
807
+ }, "end_turn", this.currentToolCallCount > 0, this.currentToolCallCount, textContent);
808
+ this.vizEventId = undefined;
809
+ }
558
810
  }
559
811
  }
560
- // The SDK's stream iterator swallows the abort and simply stops yielding.
561
- // Without this the turn would fail as a malformed stream instead of a
562
- // cancellation — checked here so the tokens already spent are reported.
563
- (0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
564
- if (!completedEvent) {
565
- throw new AgentError_1.ExecutionError("OpenAI stream ended without a completed event");
566
- }
567
- const response = completedEvent.response;
568
- const toolCalls = response.output.filter((o) => o.type === "function_call");
569
- if (toolCalls.length > 0) {
570
- // As in handleResponse(): bail out before the assistant turn is written,
571
- // so a cancelled run leaves no unanswered function call in history.
572
- (0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
573
- this.emit(AgentEvent_1.AgentEvent.TOOL_USE, toolCalls);
574
- this.currentToolCallCount += toolCalls.length;
575
- const functionCalls = toolCalls.map((tc) => ({
576
- id: tc.id || tc.call_id,
577
- call_id: tc.call_id,
578
- name: tc.name,
579
- arguments: tc.arguments,
580
- }));
581
- const assistantEntry = transformers_1.openAiTransformer.fromProviderMessage("assistant", response.output_text || "", functionCalls);
582
- this.addToHistory(assistantEntry);
583
- const toolResults = await this.handleToolUse(toolCalls, options);
584
- for (const result of toolResults) {
585
- this.addToHistory(transformers_1.openAiTransformer.toolResultEntry(result.call_id, result.output, false));
586
- }
587
- yield* this.streamTurn(options);
812
+ catch (error) {
813
+ failure = error;
814
+ throw error;
588
815
  }
589
- else {
590
- const textContent = response.output_text || "";
591
- const entry = transformers_1.openAiTransformer.fromProviderMessage("assistant", textContent);
592
- this.addToHistory(entry);
593
- this.emit(AgentEvent_1.AgentEvent.DONE, response, this.lastTokenUsage);
594
- if (this.vizEventId) {
595
- VizReporter_1.vizReporter.agentComplete(this.vizEventId, {
596
- input: this.lastTokenUsage?.input_tokens || 0,
597
- output: this.lastTokenUsage?.output_tokens || 0,
598
- total: this.lastTokenUsage?.total_tokens || 0,
599
- }, "end_turn", this.currentToolCallCount > 0, this.currentToolCallCount, textContent);
600
- this.vizEventId = undefined;
816
+ finally {
817
+ if (!committed) {
818
+ this.capturePartialTurn({
819
+ text: textDelta,
820
+ reasoning: reasoningDelta,
821
+ toolCalls: Array.from(partialCalls.entries())
822
+ .sort(([a], [b]) => a - b)
823
+ .map(([, tc]) => ({
824
+ id: tc.id,
825
+ name: tc.name,
826
+ arguments: tc.arguments,
827
+ })),
828
+ reason: this.partialTurnReason(failure, options?.signal),
829
+ error: failure,
830
+ });
601
831
  }
602
832
  }
603
833
  }