@openai/agents-core 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/editor.d.ts +38 -0
  2. package/dist/editor.js +3 -0
  3. package/dist/editor.js.map +1 -0
  4. package/dist/editor.mjs +2 -0
  5. package/dist/editor.mjs.map +1 -0
  6. package/dist/extensions/handoffFilters.js +4 -0
  7. package/dist/extensions/handoffFilters.js.map +1 -1
  8. package/dist/extensions/handoffFilters.mjs +4 -0
  9. package/dist/extensions/handoffFilters.mjs.map +1 -1
  10. package/dist/index.d.ts +8 -2
  11. package/dist/index.js +8 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +3 -1
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/items.d.ts +518 -4
  16. package/dist/items.js +22 -1
  17. package/dist/items.js.map +1 -1
  18. package/dist/items.mjs +22 -1
  19. package/dist/items.mjs.map +1 -1
  20. package/dist/memory/memorySession.d.ts +22 -0
  21. package/dist/memory/memorySession.js +64 -0
  22. package/dist/memory/memorySession.js.map +1 -0
  23. package/dist/memory/memorySession.mjs +60 -0
  24. package/dist/memory/memorySession.mjs.map +1 -0
  25. package/dist/memory/session.d.ts +36 -0
  26. package/dist/memory/session.js +3 -0
  27. package/dist/memory/session.js.map +1 -0
  28. package/dist/memory/session.mjs +2 -0
  29. package/dist/memory/session.mjs.map +1 -0
  30. package/dist/metadata.js +2 -2
  31. package/dist/metadata.mjs +2 -2
  32. package/dist/model.d.ts +10 -2
  33. package/dist/run.d.ts +88 -8
  34. package/dist/run.js +859 -347
  35. package/dist/run.js.map +1 -1
  36. package/dist/run.mjs +859 -347
  37. package/dist/run.mjs.map +1 -1
  38. package/dist/runContext.js +2 -2
  39. package/dist/runContext.js.map +1 -1
  40. package/dist/runContext.mjs +2 -2
  41. package/dist/runContext.mjs.map +1 -1
  42. package/dist/runImplementation.d.ts +37 -3
  43. package/dist/runImplementation.js +1116 -319
  44. package/dist/runImplementation.js.map +1 -1
  45. package/dist/runImplementation.mjs +1106 -317
  46. package/dist/runImplementation.mjs.map +1 -1
  47. package/dist/runState.d.ts +4453 -785
  48. package/dist/runState.js +62 -3
  49. package/dist/runState.js.map +1 -1
  50. package/dist/runState.mjs +62 -3
  51. package/dist/runState.mjs.map +1 -1
  52. package/dist/shell.d.ts +36 -0
  53. package/dist/shell.js +3 -0
  54. package/dist/shell.js.map +1 -0
  55. package/dist/shell.mjs +2 -0
  56. package/dist/shell.mjs.map +1 -0
  57. package/dist/tool.d.ts +62 -1
  58. package/dist/tool.js +30 -0
  59. package/dist/tool.js.map +1 -1
  60. package/dist/tool.mjs +28 -0
  61. package/dist/tool.mjs.map +1 -1
  62. package/dist/types/aliases.d.ts +3 -3
  63. package/dist/types/protocol.d.ts +5470 -1519
  64. package/dist/types/protocol.js +74 -1
  65. package/dist/types/protocol.js.map +1 -1
  66. package/dist/types/protocol.mjs +73 -0
  67. package/dist/types/protocol.mjs.map +1 -1
  68. package/dist/utils/applyDiff.d.ts +9 -0
  69. package/dist/utils/applyDiff.js +275 -0
  70. package/dist/utils/applyDiff.js.map +1 -0
  71. package/dist/utils/applyDiff.mjs +272 -0
  72. package/dist/utils/applyDiff.mjs.map +1 -0
  73. package/dist/utils/index.d.ts +1 -0
  74. package/dist/utils/index.js +3 -1
  75. package/dist/utils/index.js.map +1 -1
  76. package/dist/utils/index.mjs +1 -0
  77. package/dist/utils/index.mjs.map +1 -1
  78. package/dist/utils/serialize.js +12 -0
  79. package/dist/utils/serialize.js.map +1 -1
  80. package/dist/utils/serialize.mjs +12 -0
  81. package/dist/utils/serialize.mjs.map +1 -1
  82. package/dist/utils/smartString.d.ts +9 -0
  83. package/dist/utils/smartString.js +15 -0
  84. package/dist/utils/smartString.js.map +1 -1
  85. package/dist/utils/smartString.mjs +14 -3
  86. package/dist/utils/smartString.mjs.map +1 -1
  87. package/package.json +3 -3
package/dist/run.mjs CHANGED
@@ -7,8 +7,8 @@ import { RunHooks } from "./lifecycle.mjs";
7
7
  import logger from "./logger.mjs";
8
8
  import { serializeTool, serializeHandoff } from "./utils/serialize.mjs";
9
9
  import { GuardrailExecutionError, InputGuardrailTripwireTriggered, MaxTurnsExceededError, ModelBehaviorError, OutputGuardrailTripwireTriggered, UserError, } from "./errors.mjs";
10
- import { addStepToRunResult, executeInterruptedToolsAndSideEffects, executeToolsAndSideEffects, maybeResetToolChoice, processModelResponse, streamStepItemsToRunResult, } from "./runImplementation.mjs";
11
- import { getOrCreateTrace, resetCurrentSpan, setCurrentSpan, withNewSpanContext, withTrace, } from "./tracing/context.mjs";
10
+ import { addStepToRunResult, resolveInterruptedTurn, resolveTurnAfterModelResponse, maybeResetToolChoice, processModelResponse, streamStepItemsToRunResult, saveStreamInputToSession, saveStreamResultToSession, saveToSession, prepareInputItemsWithSession, } from "./runImplementation.mjs";
11
+ import { getOrCreateTrace, addErrorToCurrentSpan, resetCurrentSpan, setCurrentSpan, withNewSpanContext, withTrace, } from "./tracing/context.mjs";
12
12
  import { createAgentSpan, withGuardrailSpan } from "./tracing/index.mjs";
13
13
  import { Usage } from "./usage.mjs";
14
14
  import { RunAgentUpdatedStreamEvent, RunRawModelStreamEvent } from "./events.mjs";
@@ -16,142 +16,28 @@ import { RunState } from "./runState.mjs";
16
16
  import { StreamEventResponseCompleted } from "./types/protocol.mjs";
17
17
  import { convertAgentOutputTypeToSerializable } from "./utils/tools.mjs";
18
18
  import { gpt5ReasoningSettingsRequired, isGpt5Default } from "./defaultModel.mjs";
19
- const DEFAULT_MAX_TURNS = 10;
20
- /**
21
- * @internal
22
- */
23
- export function getTracing(tracingDisabled, traceIncludeSensitiveData) {
24
- if (tracingDisabled) {
25
- return false;
26
- }
27
- if (traceIncludeSensitiveData) {
28
- return true;
29
- }
30
- return 'enabled_without_data';
31
- }
32
- function toAgentInputList(originalInput) {
33
- if (typeof originalInput === 'string') {
34
- return [{ type: 'message', role: 'user', content: originalInput }];
35
- }
36
- return [...originalInput];
37
- }
38
- /**
39
- * Internal module for tracking the items in turns and ensuring that we don't send duplicate items.
40
- * This logic is vital for properly handling the items to send during multiple turns
41
- * when you use either `conversationId` or `previousResponseId`.
42
- * Both scenarios expect an agent loop to send only new items for each Responses API call.
43
- *
44
- * see also: https://platform.openai.com/docs/guides/conversation-state?api-mode=responses
45
- */
46
- class ServerConversationTracker {
47
- // Conversation ID:
48
- // - https://platform.openai.com/docs/guides/conversation-state?api-mode=responses#using-the-conversations-api
49
- // - https://platform.openai.com/docs/api-reference/conversations/create
50
- conversationId;
51
- // Previous Response ID:
52
- // https://platform.openai.com/docs/guides/conversation-state?api-mode=responses#passing-context-from-the-previous-response
53
- previousResponseId;
54
- // Using this flag because WeakSet does not provide a way to check its size
55
- sentInitialInput = false;
56
- // The items already sent to the model; using WeakSet for memory efficiency
57
- sentItems = new WeakSet();
58
- // The items received from the server; using WeakSet for memory efficiency
59
- serverItems = new WeakSet();
60
- constructor({ conversationId, previousResponseId, }) {
61
- this.conversationId = conversationId ?? undefined;
62
- this.previousResponseId = previousResponseId ?? undefined;
63
- }
64
- /**
65
- * Pre-populates tracker caches from an existing RunState when resuming server-managed runs.
66
- */
67
- primeFromState({ originalInput, generatedItems, modelResponses, }) {
68
- if (this.sentInitialInput) {
69
- return;
70
- }
71
- for (const item of toAgentInputList(originalInput)) {
72
- if (item && typeof item === 'object') {
73
- this.sentItems.add(item);
74
- }
75
- }
76
- this.sentInitialInput = true;
77
- const latestResponse = modelResponses[modelResponses.length - 1];
78
- for (const response of modelResponses) {
79
- for (const item of response.output) {
80
- if (item && typeof item === 'object') {
81
- this.serverItems.add(item);
82
- }
83
- }
84
- }
85
- if (!this.conversationId && latestResponse?.responseId) {
86
- this.previousResponseId = latestResponse.responseId;
87
- }
88
- for (const item of generatedItems) {
89
- const rawItem = item.rawItem;
90
- if (!rawItem || typeof rawItem !== 'object') {
91
- continue;
92
- }
93
- if (this.serverItems.has(rawItem)) {
94
- this.sentItems.add(rawItem);
95
- }
96
- }
97
- }
98
- trackServerItems(modelResponse) {
99
- if (!modelResponse) {
100
- return;
101
- }
102
- for (const item of modelResponse.output) {
103
- if (item && typeof item === 'object') {
104
- this.serverItems.add(item);
105
- }
106
- }
107
- if (!this.conversationId &&
108
- this.previousResponseId !== undefined &&
109
- modelResponse.responseId) {
110
- this.previousResponseId = modelResponse.responseId;
111
- }
19
+ import { encodeUint8ArrayToBase64 } from "./utils/base64.mjs";
20
+ import { isArrayBufferView, isNodeBuffer, isSerializedBufferSnapshot, } from "./utils/smartString.mjs";
21
+ export async function run(agent, input, options) {
22
+ const runner = getDefaultRunner();
23
+ if (options?.stream) {
24
+ return await runner.run(agent, input, options);
112
25
  }
113
- prepareInput(originalInput, generatedItems) {
114
- const inputItems = [];
115
- if (!this.sentInitialInput) {
116
- const initialItems = toAgentInputList(originalInput);
117
- for (const item of initialItems) {
118
- inputItems.push(item);
119
- if (item && typeof item === 'object') {
120
- this.sentItems.add(item);
121
- }
122
- }
123
- this.sentInitialInput = true;
124
- }
125
- for (const item of generatedItems) {
126
- if (item.type === 'tool_approval_item') {
127
- continue;
128
- }
129
- const rawItem = item.rawItem;
130
- if (!rawItem || typeof rawItem !== 'object') {
131
- continue;
132
- }
133
- if (this.sentItems.has(rawItem) || this.serverItems.has(rawItem)) {
134
- continue;
135
- }
136
- inputItems.push(rawItem);
137
- this.sentItems.add(rawItem);
138
- }
139
- return inputItems;
26
+ else {
27
+ return await runner.run(agent, input, options);
140
28
  }
141
29
  }
142
- export function getTurnInput(originalInput, generatedItems) {
143
- const rawItems = generatedItems
144
- .filter((item) => item.type !== 'tool_approval_item') // don't include approval items to avoid double function calls
145
- .map((item) => item.rawItem);
146
- return [...toAgentInputList(originalInput), ...rawItems];
147
- }
148
30
  /**
149
- * A Runner is responsible for running an agent workflow.
31
+ * Orchestrates agent execution, including guardrails, tool calls, session persistence, and
32
+ * tracing. Reuse a `Runner` instance when you want consistent configuration across multiple runs.
150
33
  */
151
34
  export class Runner extends RunHooks {
152
35
  config;
153
- inputGuardrailDefs;
154
- outputGuardrailDefs;
36
+ /**
37
+ * Creates a runner with optional defaults that apply to every subsequent run invocation.
38
+ *
39
+ * @param config - Overrides for models, guardrails, tracing, or session behavior.
40
+ */
155
41
  constructor(config = {}) {
156
42
  super();
157
43
  this.config = {
@@ -167,14 +53,270 @@ export class Runner extends RunHooks {
167
53
  traceId: config.traceId,
168
54
  groupId: config.groupId,
169
55
  traceMetadata: config.traceMetadata,
56
+ sessionInputCallback: config.sessionInputCallback,
57
+ callModelInputFilter: config.callModelInputFilter,
170
58
  };
171
59
  this.inputGuardrailDefs = (config.inputGuardrails ?? []).map(defineInputGuardrail);
172
60
  this.outputGuardrailDefs = (config.outputGuardrails ?? []).map(defineOutputGuardrail);
173
61
  }
62
+ async run(agent, input, options = {
63
+ stream: false,
64
+ context: undefined,
65
+ }) {
66
+ const resolvedOptions = options ?? { stream: false, context: undefined };
67
+ // Per-run options take precedence over runner defaults for session memory behavior.
68
+ const sessionInputCallback = resolvedOptions.sessionInputCallback ?? this.config.sessionInputCallback;
69
+ // Likewise allow callers to override callModelInputFilter on individual runs.
70
+ const callModelInputFilter = resolvedOptions.callModelInputFilter ?? this.config.callModelInputFilter;
71
+ const hasCallModelInputFilter = Boolean(callModelInputFilter);
72
+ const effectiveOptions = {
73
+ ...resolvedOptions,
74
+ sessionInputCallback,
75
+ callModelInputFilter,
76
+ };
77
+ const serverManagesConversation = Boolean(effectiveOptions.conversationId) ||
78
+ Boolean(effectiveOptions.previousResponseId);
79
+ // When the server tracks conversation history we defer to it for previous turns so local session
80
+ // persistence can focus solely on the new delta being generated in this process.
81
+ const session = effectiveOptions.session;
82
+ const resumingFromState = input instanceof RunState;
83
+ let sessionInputOriginalSnapshot = session && resumingFromState ? [] : undefined;
84
+ let sessionInputFilteredSnapshot = undefined;
85
+ // Tracks remaining persistence slots per AgentInputItem key so resumed sessions only write each original occurrence once.
86
+ let sessionInputPendingWriteCounts = session && resumingFromState ? new Map() : undefined;
87
+ // Keeps track of which inputs should be written back to session memory. `sourceItems` reflects
88
+ // the original objects (so we can respect resume counts) while `filteredItems`, when present,
89
+ // contains the filtered/redacted clones that must be persisted for history.
90
+ // The helper reconciles the filtered copies produced by callModelInputFilter with their original
91
+ // counterparts so resume-from-state bookkeeping stays consistent and duplicate references only
92
+ // consume a single persistence slot.
93
+ const recordSessionItemsForPersistence = (sourceItems, filteredItems) => {
94
+ const pendingWriteCounts = sessionInputPendingWriteCounts;
95
+ if (filteredItems !== undefined) {
96
+ if (!pendingWriteCounts) {
97
+ sessionInputFilteredSnapshot = filteredItems.map((item) => structuredClone(item));
98
+ return;
99
+ }
100
+ const persistableItems = [];
101
+ const sourceOccurrenceCounts = new WeakMap();
102
+ // Track how many times each original object appears so duplicate references only consume one persistence slot.
103
+ for (const source of sourceItems) {
104
+ if (!source || typeof source !== 'object') {
105
+ continue;
106
+ }
107
+ const nextCount = (sourceOccurrenceCounts.get(source) ?? 0) + 1;
108
+ sourceOccurrenceCounts.set(source, nextCount);
109
+ }
110
+ // Let filtered items without a one-to-one source match claim any remaining persistence count.
111
+ const consumeAnyPendingWriteSlot = () => {
112
+ for (const [key, remaining] of pendingWriteCounts) {
113
+ if (remaining > 0) {
114
+ pendingWriteCounts.set(key, remaining - 1);
115
+ return true;
116
+ }
117
+ }
118
+ return false;
119
+ };
120
+ for (let i = 0; i < filteredItems.length; i++) {
121
+ const filteredItem = filteredItems[i];
122
+ if (!filteredItem) {
123
+ continue;
124
+ }
125
+ let allocated = false;
126
+ const source = sourceItems[i];
127
+ if (source && typeof source === 'object') {
128
+ const pendingOccurrences = (sourceOccurrenceCounts.get(source) ?? 0) - 1;
129
+ sourceOccurrenceCounts.set(source, pendingOccurrences);
130
+ if (pendingOccurrences > 0) {
131
+ continue;
132
+ }
133
+ const sourceKey = getAgentInputItemKey(source);
134
+ const remaining = pendingWriteCounts.get(sourceKey) ?? 0;
135
+ if (remaining > 0) {
136
+ pendingWriteCounts.set(sourceKey, remaining - 1);
137
+ persistableItems.push(structuredClone(filteredItem));
138
+ allocated = true;
139
+ continue;
140
+ }
141
+ }
142
+ const filteredKey = getAgentInputItemKey(filteredItem);
143
+ const filteredRemaining = pendingWriteCounts.get(filteredKey) ?? 0;
144
+ if (filteredRemaining > 0) {
145
+ pendingWriteCounts.set(filteredKey, filteredRemaining - 1);
146
+ persistableItems.push(structuredClone(filteredItem));
147
+ allocated = true;
148
+ continue;
149
+ }
150
+ if (!source && consumeAnyPendingWriteSlot()) {
151
+ persistableItems.push(structuredClone(filteredItem));
152
+ allocated = true;
153
+ }
154
+ if (!allocated &&
155
+ !source &&
156
+ sessionInputFilteredSnapshot === undefined) {
157
+ // Preserve at least one copy so later persistence resolves even when no counters remain.
158
+ persistableItems.push(structuredClone(filteredItem));
159
+ }
160
+ }
161
+ if (persistableItems.length > 0 ||
162
+ sessionInputFilteredSnapshot === undefined) {
163
+ sessionInputFilteredSnapshot = persistableItems;
164
+ }
165
+ return;
166
+ }
167
+ const filtered = [];
168
+ if (!pendingWriteCounts) {
169
+ for (const item of sourceItems) {
170
+ if (!item) {
171
+ continue;
172
+ }
173
+ filtered.push(structuredClone(item));
174
+ }
175
+ }
176
+ else {
177
+ for (const item of sourceItems) {
178
+ if (!item) {
179
+ continue;
180
+ }
181
+ const key = getAgentInputItemKey(item);
182
+ const remaining = pendingWriteCounts.get(key) ?? 0;
183
+ if (remaining <= 0) {
184
+ continue;
185
+ }
186
+ pendingWriteCounts.set(key, remaining - 1);
187
+ filtered.push(structuredClone(item));
188
+ }
189
+ }
190
+ if (filtered.length > 0) {
191
+ sessionInputFilteredSnapshot = filtered;
192
+ }
193
+ else if (sessionInputFilteredSnapshot === undefined) {
194
+ sessionInputFilteredSnapshot = [];
195
+ }
196
+ };
197
+ // Determine which items should be committed to session memory for this turn.
198
+ // Filters take precedence because they reflect the exact payload delivered to the model.
199
+ const resolveSessionItemsForPersistence = () => {
200
+ if (sessionInputFilteredSnapshot !== undefined) {
201
+ return sessionInputFilteredSnapshot;
202
+ }
203
+ if (hasCallModelInputFilter) {
204
+ return undefined;
205
+ }
206
+ return sessionInputOriginalSnapshot;
207
+ };
208
+ let preparedInput = input;
209
+ if (!(preparedInput instanceof RunState)) {
210
+ if (session && Array.isArray(preparedInput) && !sessionInputCallback) {
211
+ throw new UserError('RunConfig.sessionInputCallback must be provided when using session history with list inputs.');
212
+ }
213
+ const prepared = await prepareInputItemsWithSession(preparedInput, session, sessionInputCallback, {
214
+ // When the server tracks conversation state we only send the new turn inputs;
215
+ // previous messages are recovered via conversationId/previousResponseId.
216
+ includeHistoryInPreparedInput: !serverManagesConversation,
217
+ preserveDroppedNewItems: serverManagesConversation,
218
+ });
219
+ if (serverManagesConversation && session) {
220
+ // When the server manages memory we only persist the new turn inputs locally so the
221
+ // conversation service stays the single source of truth for prior exchanges.
222
+ const sessionItems = prepared.sessionItems;
223
+ if (sessionItems && sessionItems.length > 0) {
224
+ preparedInput = sessionItems;
225
+ }
226
+ else {
227
+ preparedInput = prepared.preparedInput;
228
+ }
229
+ }
230
+ else {
231
+ preparedInput = prepared.preparedInput;
232
+ }
233
+ if (session) {
234
+ const items = prepared.sessionItems ?? [];
235
+ // Clone the items that will be persisted so later mutations (filters, hooks) cannot desync history.
236
+ sessionInputOriginalSnapshot = items.map((item) => structuredClone(item));
237
+ // Reset pending counts so each prepared item reserves exactly one write slot until filters resolve matches.
238
+ sessionInputPendingWriteCounts = new Map();
239
+ for (const item of items) {
240
+ const key = getAgentInputItemKey(item);
241
+ sessionInputPendingWriteCounts.set(key, (sessionInputPendingWriteCounts.get(key) ?? 0) + 1);
242
+ }
243
+ }
244
+ }
245
+ // Streaming runs persist the input asynchronously, so track a one-shot helper
246
+ // that can be awaited from multiple branches without double-writing.
247
+ let ensureStreamInputPersisted;
248
+ // Sessions remain usable alongside server-managed conversations (e.g., OpenAIConversationsSession)
249
+ // so callers can reuse callbacks, resume-from-state logic, and other helpers without duplicating
250
+ // remote history, so persistence is gated on serverManagesConversation.
251
+ if (session && !serverManagesConversation) {
252
+ let persisted = false;
253
+ ensureStreamInputPersisted = async () => {
254
+ if (persisted) {
255
+ return;
256
+ }
257
+ const itemsToPersist = resolveSessionItemsForPersistence();
258
+ if (!itemsToPersist || itemsToPersist.length === 0) {
259
+ return;
260
+ }
261
+ persisted = true;
262
+ await saveStreamInputToSession(session, itemsToPersist);
263
+ };
264
+ }
265
+ const executeRun = async () => {
266
+ if (effectiveOptions.stream) {
267
+ const streamResult = await this.#runIndividualStream(agent, preparedInput, effectiveOptions, ensureStreamInputPersisted, recordSessionItemsForPersistence);
268
+ return streamResult;
269
+ }
270
+ const runResult = await this.#runIndividualNonStream(agent, preparedInput, effectiveOptions, recordSessionItemsForPersistence);
271
+ // See note above: allow sessions to run for callbacks/state but skip writes when the server
272
+ // is the source of truth for transcript history.
273
+ if (session && !serverManagesConversation) {
274
+ await saveToSession(session, resolveSessionItemsForPersistence(), runResult);
275
+ }
276
+ return runResult;
277
+ };
278
+ if (preparedInput instanceof RunState && preparedInput._trace) {
279
+ return withTrace(preparedInput._trace, async () => {
280
+ if (preparedInput._currentAgentSpan) {
281
+ setCurrentSpan(preparedInput._currentAgentSpan);
282
+ }
283
+ return executeRun();
284
+ });
285
+ }
286
+ return getOrCreateTrace(async () => executeRun(), {
287
+ traceId: this.config.traceId,
288
+ name: this.config.workflowName,
289
+ groupId: this.config.groupId,
290
+ metadata: this.config.traceMetadata,
291
+ });
292
+ }
293
+ // --------------------------------------------------------------
294
+ // Internals
295
+ // --------------------------------------------------------------
296
+ inputGuardrailDefs;
297
+ outputGuardrailDefs;
298
+ /**
299
+ * @internal
300
+ * Resolves the effective model once so both run loops obey the same precedence rules.
301
+ */
302
+ async #resolveModelForAgent(agent) {
303
+ const explictlyModelSet = (agent.model !== undefined &&
304
+ agent.model !== Agent.DEFAULT_MODEL_PLACEHOLDER) ||
305
+ (this.config.model !== undefined &&
306
+ this.config.model !== Agent.DEFAULT_MODEL_PLACEHOLDER);
307
+ let resolvedModel = selectModel(agent.model, this.config.model);
308
+ if (typeof resolvedModel === 'string') {
309
+ resolvedModel = await this.config.modelProvider.getModel(resolvedModel);
310
+ }
311
+ return { model: resolvedModel, explictlyModelSet };
312
+ }
174
313
  /**
175
314
  * @internal
176
315
  */
177
- async #runIndividualNonStream(startingAgent, input, options) {
316
+ async #runIndividualNonStream(startingAgent, input, options,
317
+ // sessionInputUpdate lets the caller adjust queued session items after filters run so we
318
+ // persist exactly what we send to the model (e.g., after redactions or truncation).
319
+ sessionInputUpdate) {
178
320
  return withNewSpanContext(async () => {
179
321
  // if we have a saved state we use that one, otherwise we create a new one
180
322
  const isResumedState = input instanceof RunState;
@@ -198,13 +340,6 @@ export class Runner extends RunHooks {
198
340
  }
199
341
  try {
200
342
  while (true) {
201
- const explictlyModelSet = (state._currentAgent.model !== undefined &&
202
- state._currentAgent.model !== '') ||
203
- (this.config.model !== undefined && this.config.model !== '');
204
- let model = selectModel(state._currentAgent.model, this.config.model);
205
- if (typeof model === 'string') {
206
- model = await this.config.modelProvider.getModel(model);
207
- }
208
343
  // if we don't have a current step, we treat this as a new run
209
344
  state._currentStep = state._currentStep ?? {
210
345
  type: 'next_step_run_again',
@@ -214,10 +349,13 @@ export class Runner extends RunHooks {
214
349
  if (!state._lastTurnResponse || !state._lastProcessedResponse) {
215
350
  throw new UserError('No model response found in previous state', state);
216
351
  }
217
- const turnResult = await executeInterruptedToolsAndSideEffects(state._currentAgent, state._originalInput, state._generatedItems, state._lastTurnResponse, state._lastProcessedResponse, this, state);
352
+ const turnResult = await resolveInterruptedTurn(state._currentAgent, state._originalInput, state._generatedItems, state._lastTurnResponse, state._lastProcessedResponse, this, state);
218
353
  state._toolUseTracker.addToolUse(state._currentAgent, state._lastProcessedResponse.toolsUsed);
219
354
  state._originalInput = turnResult.originalInput;
220
355
  state._generatedItems = turnResult.generatedItems;
356
+ if (turnResult.nextStep.type === 'next_step_run_again') {
357
+ state._currentTurnPersistedItemCount = 0;
358
+ }
221
359
  state._currentStep = turnResult.nextStep;
222
360
  if (turnResult.nextStep.type === 'next_step_interruption') {
223
361
  // we are still in an interruption, so we need to avoid an infinite loop
@@ -226,26 +364,9 @@ export class Runner extends RunHooks {
226
364
  continue;
227
365
  }
228
366
  if (state._currentStep.type === 'next_step_run_again') {
229
- const handoffs = await state._currentAgent.getEnabledHandoffs(state._context);
230
- if (!state._currentAgentSpan) {
231
- const handoffNames = handoffs.map((h) => h.agentName);
232
- state._currentAgentSpan = createAgentSpan({
233
- data: {
234
- name: state._currentAgent.name,
235
- handoffs: handoffNames,
236
- output_type: state._currentAgent.outputSchemaName,
237
- },
238
- });
239
- state._currentAgentSpan.start();
240
- setCurrentSpan(state._currentAgentSpan);
241
- }
242
- const tools = await state._currentAgent.getAllTools(state._context);
243
- const serializedTools = tools.map((t) => serializeTool(t));
244
- const serializedHandoffs = handoffs.map((h) => serializeHandoff(h));
245
- if (state._currentAgentSpan) {
246
- state._currentAgentSpan.spanData.tools = tools.map((t) => t.name);
247
- }
367
+ const artifacts = await prepareAgentArtifacts(state);
248
368
  state._currentTurn++;
369
+ state._currentTurnPersistedItemCount = 0;
249
370
  if (state._currentTurn > state._maxTurns) {
250
371
  state._currentAgentSpan?.setError({
251
372
  message: 'Max turns exceeded',
@@ -264,42 +385,39 @@ export class Runner extends RunHooks {
264
385
  state._currentAgent.emit('agent_start', state._context, state._currentAgent);
265
386
  this.emit('agent_start', state._context, state._currentAgent);
266
387
  }
267
- let modelSettings = {
268
- ...this.config.modelSettings,
269
- ...state._currentAgent.modelSettings,
270
- };
271
- const agentModelSettings = state._currentAgent.modelSettings;
272
- modelSettings = adjustModelSettingsForNonGPT5RunnerModel(explictlyModelSet, agentModelSettings, model, modelSettings);
273
- modelSettings = maybeResetToolChoice(state._currentAgent, state._toolUseTracker, modelSettings);
274
- const previousResponseId = serverConversationTracker?.previousResponseId ??
275
- options.previousResponseId;
276
- const conversationId = serverConversationTracker?.conversationId ??
277
- options.conversationId;
278
- state._lastTurnResponse = await model.getResponse({
279
- systemInstructions: await state._currentAgent.getSystemPrompt(state._context),
280
- prompt: await state._currentAgent.getPrompt(state._context),
388
+ const preparedCall = await this.#prepareModelCall(state, options, artifacts, turnInput, serverConversationTracker, sessionInputUpdate);
389
+ state._lastTurnResponse = await preparedCall.model.getResponse({
390
+ systemInstructions: preparedCall.modelInput.instructions,
391
+ prompt: preparedCall.prompt,
281
392
  // Explicit agent/run config models should take precedence over prompt defaults.
282
- ...(explictlyModelSet ? { overridePromptModel: true } : {}),
283
- input: turnInput,
284
- previousResponseId,
285
- conversationId,
286
- modelSettings,
287
- tools: serializedTools,
393
+ ...(preparedCall.explictlyModelSet
394
+ ? { overridePromptModel: true }
395
+ : {}),
396
+ input: preparedCall.modelInput.input,
397
+ previousResponseId: preparedCall.previousResponseId,
398
+ conversationId: preparedCall.conversationId,
399
+ modelSettings: preparedCall.modelSettings,
400
+ tools: preparedCall.serializedTools,
288
401
  outputType: convertAgentOutputTypeToSerializable(state._currentAgent.outputType),
289
- handoffs: serializedHandoffs,
402
+ handoffs: preparedCall.serializedHandoffs,
290
403
  tracing: getTracing(this.config.tracingDisabled, this.config.traceIncludeSensitiveData),
291
404
  signal: options.signal,
292
405
  });
293
406
  state._modelResponses.push(state._lastTurnResponse);
294
407
  state._context.usage.add(state._lastTurnResponse.usage);
295
408
  state._noActiveAgentRun = false;
409
+ // After each turn record the items echoed by the server so future requests only
410
+ // include the incremental inputs that have not yet been acknowledged.
296
411
  serverConversationTracker?.trackServerItems(state._lastTurnResponse);
297
- const processedResponse = processModelResponse(state._lastTurnResponse, state._currentAgent, tools, handoffs);
412
+ const processedResponse = processModelResponse(state._lastTurnResponse, state._currentAgent, preparedCall.tools, preparedCall.handoffs);
298
413
  state._lastProcessedResponse = processedResponse;
299
- const turnResult = await executeToolsAndSideEffects(state._currentAgent, state._originalInput, state._generatedItems, state._lastTurnResponse, state._lastProcessedResponse, this, state);
414
+ const turnResult = await resolveTurnAfterModelResponse(state._currentAgent, state._originalInput, state._generatedItems, state._lastTurnResponse, state._lastProcessedResponse, this, state);
300
415
  state._toolUseTracker.addToolUse(state._currentAgent, state._lastProcessedResponse.toolsUsed);
301
416
  state._originalInput = turnResult.originalInput;
302
417
  state._generatedItems = turnResult.generatedItems;
418
+ if (turnResult.nextStep.type === 'next_step_run_again') {
419
+ state._currentTurnPersistedItemCount = 0;
420
+ }
303
421
  state._currentStep = turnResult.nextStep;
304
422
  }
305
423
  if (state._currentStep &&
@@ -351,92 +469,27 @@ export class Runner extends RunHooks {
351
469
  }
352
470
  });
353
471
  }
354
- async #runInputGuardrails(state) {
355
- const guardrails = this.inputGuardrailDefs.concat(state._currentAgent.inputGuardrails.map(defineInputGuardrail));
356
- if (guardrails.length > 0) {
357
- const guardrailArgs = {
358
- agent: state._currentAgent,
359
- input: state._originalInput,
360
- context: state._context,
361
- };
362
- try {
363
- const results = await Promise.all(guardrails.map(async (guardrail) => {
364
- return withGuardrailSpan(async (span) => {
365
- const result = await guardrail.run(guardrailArgs);
366
- span.spanData.triggered = result.output.tripwireTriggered;
367
- return result;
368
- }, { data: { name: guardrail.name } }, state._currentAgentSpan);
369
- }));
370
- for (const result of results) {
371
- if (result.output.tripwireTriggered) {
372
- if (state._currentAgentSpan) {
373
- state._currentAgentSpan.setError({
374
- message: 'Guardrail tripwire triggered',
375
- data: { guardrail: result.guardrail.name },
376
- });
377
- }
378
- throw new InputGuardrailTripwireTriggered(`Input guardrail triggered: ${JSON.stringify(result.output.outputInfo)}`, result, state);
379
- }
380
- }
381
- }
382
- catch (e) {
383
- if (e instanceof InputGuardrailTripwireTriggered) {
384
- throw e;
385
- }
386
- // roll back the current turn to enable reruns
387
- state._currentTurn--;
388
- throw new GuardrailExecutionError(`Input guardrail failed to complete: ${e}`, e, state);
472
+ /**
473
+ * @internal
474
+ */
475
+ async #runStreamLoop(result, options, isResumedState, ensureStreamInputPersisted, sessionInputUpdate) {
476
+ const serverManagesConversation = Boolean(options.conversationId) || Boolean(options.previousResponseId);
477
+ const serverConversationTracker = serverManagesConversation
478
+ ? new ServerConversationTracker({
479
+ conversationId: options.conversationId,
480
+ previousResponseId: options.previousResponseId,
481
+ })
482
+ : undefined;
483
+ let handedInputToModel = false;
484
+ let streamInputPersisted = false;
485
+ const persistStreamInputIfNeeded = async () => {
486
+ if (streamInputPersisted || !ensureStreamInputPersisted) {
487
+ return;
389
488
  }
390
- }
391
- }
392
- async #runOutputGuardrails(state, output) {
393
- const guardrails = this.outputGuardrailDefs.concat(state._currentAgent.outputGuardrails.map(defineOutputGuardrail));
394
- if (guardrails.length > 0) {
395
- const agentOutput = state._currentAgent.processFinalOutput(output);
396
- const guardrailArgs = {
397
- agent: state._currentAgent,
398
- agentOutput,
399
- context: state._context,
400
- details: { modelResponse: state._lastTurnResponse },
401
- };
402
- try {
403
- const results = await Promise.all(guardrails.map(async (guardrail) => {
404
- return withGuardrailSpan(async (span) => {
405
- const result = await guardrail.run(guardrailArgs);
406
- span.spanData.triggered = result.output.tripwireTriggered;
407
- return result;
408
- }, { data: { name: guardrail.name } }, state._currentAgentSpan);
409
- }));
410
- for (const result of results) {
411
- if (result.output.tripwireTriggered) {
412
- if (state._currentAgentSpan) {
413
- state._currentAgentSpan.setError({
414
- message: 'Guardrail tripwire triggered',
415
- data: { guardrail: result.guardrail.name },
416
- });
417
- }
418
- throw new OutputGuardrailTripwireTriggered(`Output guardrail triggered: ${JSON.stringify(result.output.outputInfo)}`, result, state);
419
- }
420
- }
421
- }
422
- catch (e) {
423
- if (e instanceof OutputGuardrailTripwireTriggered) {
424
- throw e;
425
- }
426
- throw new GuardrailExecutionError(`Output guardrail failed to complete: ${e}`, e, state);
427
- }
428
- }
429
- }
430
- /**
431
- * @internal
432
- */
433
- async #runStreamLoop(result, options, isResumedState) {
434
- const serverConversationTracker = options.conversationId || options.previousResponseId
435
- ? new ServerConversationTracker({
436
- conversationId: options.conversationId,
437
- previousResponseId: options.previousResponseId,
438
- })
439
- : undefined;
489
+ // Both success and error paths call this helper, so guard against multiple writes.
490
+ await ensureStreamInputPersisted();
491
+ streamInputPersisted = true;
492
+ };
440
493
  if (serverConversationTracker && isResumedState) {
441
494
  serverConversationTracker.primeFromState({
442
495
  originalInput: result.state._originalInput,
@@ -447,10 +500,6 @@ export class Runner extends RunHooks {
447
500
  try {
448
501
  while (true) {
449
502
  const currentAgent = result.state._currentAgent;
450
- const handoffs = await currentAgent.getEnabledHandoffs(result.state._context);
451
- const tools = await currentAgent.getAllTools(result.state._context);
452
- const serializedTools = tools.map((t) => serializeTool(t));
453
- const serializedHandoffs = handoffs.map((h) => serializeHandoff(h));
454
503
  result.state._currentStep = result.state._currentStep ?? {
455
504
  type: 'next_step_run_again',
456
505
  };
@@ -460,11 +509,14 @@ export class Runner extends RunHooks {
460
509
  !result.state._lastProcessedResponse) {
461
510
  throw new UserError('No model response found in previous state', result.state);
462
511
  }
463
- const turnResult = await executeInterruptedToolsAndSideEffects(result.state._currentAgent, result.state._originalInput, result.state._generatedItems, result.state._lastTurnResponse, result.state._lastProcessedResponse, this, result.state);
512
+ const turnResult = await resolveInterruptedTurn(result.state._currentAgent, result.state._originalInput, result.state._generatedItems, result.state._lastTurnResponse, result.state._lastProcessedResponse, this, result.state);
464
513
  addStepToRunResult(result, turnResult);
465
514
  result.state._toolUseTracker.addToolUse(result.state._currentAgent, result.state._lastProcessedResponse.toolsUsed);
466
515
  result.state._originalInput = turnResult.originalInput;
467
516
  result.state._generatedItems = turnResult.generatedItems;
517
+ if (turnResult.nextStep.type === 'next_step_run_again') {
518
+ result.state._currentTurnPersistedItemCount = 0;
519
+ }
468
520
  result.state._currentStep = turnResult.nextStep;
469
521
  if (turnResult.nextStep.type === 'next_step_interruption') {
470
522
  // we are still in an interruption, so we need to avoid an infinite loop
@@ -473,20 +525,9 @@ export class Runner extends RunHooks {
473
525
  continue;
474
526
  }
475
527
  if (result.state._currentStep.type === 'next_step_run_again') {
476
- if (!result.state._currentAgentSpan) {
477
- const handoffNames = handoffs.map((h) => h.agentName);
478
- result.state._currentAgentSpan = createAgentSpan({
479
- data: {
480
- name: currentAgent.name,
481
- handoffs: handoffNames,
482
- tools: tools.map((t) => t.name),
483
- output_type: currentAgent.outputSchemaName,
484
- },
485
- });
486
- result.state._currentAgentSpan.start();
487
- setCurrentSpan(result.state._currentAgentSpan);
488
- }
528
+ const artifacts = await prepareAgentArtifacts(result.state);
489
529
  result.state._currentTurn++;
530
+ result.state._currentTurnPersistedItemCount = 0;
490
531
  if (result.state._currentTurn > result.state._maxTurns) {
491
532
  result.state._currentAgentSpan?.setError({
492
533
  message: 'Max turns exceeded',
@@ -495,22 +536,9 @@ export class Runner extends RunHooks {
495
536
  throw new MaxTurnsExceededError(`Max turns (${result.state._maxTurns}) exceeded`, result.state);
496
537
  }
497
538
  logger.debug(`Running agent ${currentAgent.name} (turn ${result.state._currentTurn})`);
498
- const explictlyModelSet = (currentAgent.model !== undefined && currentAgent.model !== '') ||
499
- (this.config.model !== undefined && this.config.model !== '');
500
- let model = selectModel(currentAgent.model, this.config.model);
501
- if (typeof model === 'string') {
502
- model = await this.config.modelProvider.getModel(model);
503
- }
504
539
  if (result.state._currentTurn === 1) {
505
540
  await this.#runInputGuardrails(result.state);
506
541
  }
507
- let modelSettings = {
508
- ...this.config.modelSettings,
509
- ...currentAgent.modelSettings,
510
- };
511
- const agentModelSettings = currentAgent.modelSettings;
512
- modelSettings = adjustModelSettingsForNonGPT5RunnerModel(explictlyModelSet, agentModelSettings, model, modelSettings);
513
- modelSettings = maybeResetToolChoice(currentAgent, result.state._toolUseTracker, modelSettings);
514
542
  const turnInput = serverConversationTracker
515
543
  ? serverConversationTracker.prepareInput(result.input, result.newItems)
516
544
  : getTurnInput(result.input, result.newItems);
@@ -519,20 +547,22 @@ export class Runner extends RunHooks {
519
547
  this.emit('agent_start', result.state._context, currentAgent);
520
548
  }
521
549
  let finalResponse = undefined;
522
- const previousResponseId = serverConversationTracker?.previousResponseId ??
523
- options.previousResponseId;
524
- const conversationId = serverConversationTracker?.conversationId ?? options.conversationId;
525
- for await (const event of model.getStreamedResponse({
526
- systemInstructions: await currentAgent.getSystemPrompt(result.state._context),
527
- prompt: await currentAgent.getPrompt(result.state._context),
550
+ const preparedCall = await this.#prepareModelCall(result.state, options, artifacts, turnInput, serverConversationTracker, sessionInputUpdate);
551
+ handedInputToModel = true;
552
+ await persistStreamInputIfNeeded();
553
+ for await (const event of preparedCall.model.getStreamedResponse({
554
+ systemInstructions: preparedCall.modelInput.instructions,
555
+ prompt: preparedCall.prompt,
528
556
  // Streaming requests should also honor explicitly chosen models.
529
- ...(explictlyModelSet ? { overridePromptModel: true } : {}),
530
- input: turnInput,
531
- previousResponseId,
532
- conversationId,
533
- modelSettings,
534
- tools: serializedTools,
535
- handoffs: serializedHandoffs,
557
+ ...(preparedCall.explictlyModelSet
558
+ ? { overridePromptModel: true }
559
+ : {}),
560
+ input: preparedCall.modelInput.input,
561
+ previousResponseId: preparedCall.previousResponseId,
562
+ conversationId: preparedCall.conversationId,
563
+ modelSettings: preparedCall.modelSettings,
564
+ tools: preparedCall.serializedTools,
565
+ handoffs: preparedCall.serializedHandoffs,
536
566
  outputType: convertAgentOutputTypeToSerializable(currentAgent.outputType),
537
567
  tracing: getTracing(this.config.tracingDisabled, this.config.traceIncludeSensitiveData),
538
568
  signal: options.signal,
@@ -557,9 +587,10 @@ export class Runner extends RunHooks {
557
587
  throw new ModelBehaviorError('Model did not produce a final response!', result.state);
558
588
  }
559
589
  result.state._lastTurnResponse = finalResponse;
590
+ // Keep the tracker in sync with the streamed response so reconnections remain accurate.
560
591
  serverConversationTracker?.trackServerItems(finalResponse);
561
592
  result.state._modelResponses.push(result.state._lastTurnResponse);
562
- const processedResponse = processModelResponse(result.state._lastTurnResponse, currentAgent, tools, handoffs);
593
+ const processedResponse = processModelResponse(result.state._lastTurnResponse, currentAgent, preparedCall.tools, preparedCall.handoffs);
563
594
  result.state._lastProcessedResponse = processedResponse;
564
595
  // Record the items emitted directly from the model response so we do not
565
596
  // stream them again after tools and other side effects finish.
@@ -567,23 +598,35 @@ export class Runner extends RunHooks {
567
598
  if (preToolItems.size > 0) {
568
599
  streamStepItemsToRunResult(result, processedResponse.newItems);
569
600
  }
570
- const turnResult = await executeToolsAndSideEffects(currentAgent, result.state._originalInput, result.state._generatedItems, result.state._lastTurnResponse, result.state._lastProcessedResponse, this, result.state);
601
+ const turnResult = await resolveTurnAfterModelResponse(currentAgent, result.state._originalInput, result.state._generatedItems, result.state._lastTurnResponse, result.state._lastProcessedResponse, this, result.state);
571
602
  addStepToRunResult(result, turnResult, {
572
603
  skipItems: preToolItems,
573
604
  });
574
605
  result.state._toolUseTracker.addToolUse(currentAgent, processedResponse.toolsUsed);
575
606
  result.state._originalInput = turnResult.originalInput;
576
607
  result.state._generatedItems = turnResult.generatedItems;
608
+ if (turnResult.nextStep.type === 'next_step_run_again') {
609
+ result.state._currentTurnPersistedItemCount = 0;
610
+ }
577
611
  result.state._currentStep = turnResult.nextStep;
578
612
  }
579
613
  if (result.state._currentStep.type === 'next_step_final_output') {
580
614
  await this.#runOutputGuardrails(result.state, result.state._currentStep.output);
615
+ await persistStreamInputIfNeeded();
616
+ // Guardrails must succeed before persisting session memory to avoid storing blocked outputs.
617
+ if (!serverManagesConversation) {
618
+ await saveStreamResultToSession(options.session, result);
619
+ }
581
620
  this.emit('agent_end', result.state._context, currentAgent, result.state._currentStep.output);
582
621
  currentAgent.emit('agent_end', result.state._context, result.state._currentStep.output);
583
622
  return;
584
623
  }
585
624
  else if (result.state._currentStep.type === 'next_step_interruption') {
586
625
  // we are done for now. Don't run any output guardrails
626
+ await persistStreamInputIfNeeded();
627
+ if (!serverManagesConversation) {
628
+ await saveStreamResultToSession(options.session, result);
629
+ }
587
630
  return;
588
631
  }
589
632
  else if (result.state._currentStep.type === 'next_step_handoff') {
@@ -607,6 +650,9 @@ export class Runner extends RunHooks {
607
650
  }
608
651
  }
609
652
  catch (error) {
653
+ if (handedInputToModel && !streamInputPersisted) {
654
+ await persistStreamInputIfNeeded();
655
+ }
610
656
  if (result.state._currentAgentSpan) {
611
657
  result.state._currentAgentSpan.setError({
612
658
  message: 'Error in agent run',
@@ -627,7 +673,7 @@ export class Runner extends RunHooks {
627
673
  /**
628
674
  * @internal
629
675
  */
630
- async #runIndividualStream(agent, input, options) {
676
+ async #runIndividualStream(agent, input, options, ensureStreamInputPersisted, sessionInputUpdate) {
631
677
  options = options ?? {};
632
678
  return withNewSpanContext(async () => {
633
679
  // Initialize or reuse existing state
@@ -645,7 +691,7 @@ export class Runner extends RunHooks {
645
691
  // Setup defaults
646
692
  result.maxTurns = options.maxTurns ?? state._maxTurns;
647
693
  // Continue the stream loop without blocking
648
- const streamLoopPromise = this.#runStreamLoop(result, options, isResumedState).then(() => {
694
+ const streamLoopPromise = this.#runStreamLoop(result, options, isResumedState, ensureStreamInputPersisted, sessionInputUpdate).then(() => {
649
695
  result._done();
650
696
  }, (err) => {
651
697
  result._raiseError(err);
@@ -655,38 +701,137 @@ export class Runner extends RunHooks {
655
701
  return result;
656
702
  });
657
703
  }
658
- run(agent, input, options = {
659
- stream: false,
660
- context: undefined,
661
- }) {
662
- if (input instanceof RunState && input._trace) {
663
- return withTrace(input._trace, async () => {
664
- if (input._currentAgentSpan) {
665
- setCurrentSpan(input._currentAgentSpan);
666
- }
667
- if (options?.stream) {
668
- return this.#runIndividualStream(agent, input, options);
704
+ async #runInputGuardrails(state) {
705
+ const guardrails = this.inputGuardrailDefs.concat(state._currentAgent.inputGuardrails.map(defineInputGuardrail));
706
+ if (guardrails.length > 0) {
707
+ const guardrailArgs = {
708
+ agent: state._currentAgent,
709
+ input: state._originalInput,
710
+ context: state._context,
711
+ };
712
+ try {
713
+ const results = await Promise.all(guardrails.map(async (guardrail) => {
714
+ return withGuardrailSpan(async (span) => {
715
+ const result = await guardrail.run(guardrailArgs);
716
+ span.spanData.triggered = result.output.tripwireTriggered;
717
+ return result;
718
+ }, { data: { name: guardrail.name } }, state._currentAgentSpan);
719
+ }));
720
+ for (const result of results) {
721
+ if (result.output.tripwireTriggered) {
722
+ if (state._currentAgentSpan) {
723
+ state._currentAgentSpan.setError({
724
+ message: 'Guardrail tripwire triggered',
725
+ data: { guardrail: result.guardrail.name },
726
+ });
727
+ }
728
+ throw new InputGuardrailTripwireTriggered(`Input guardrail triggered: ${JSON.stringify(result.output.outputInfo)}`, result, state);
729
+ }
669
730
  }
670
- else {
671
- return this.#runIndividualNonStream(agent, input, options);
731
+ }
732
+ catch (e) {
733
+ if (e instanceof InputGuardrailTripwireTriggered) {
734
+ throw e;
672
735
  }
673
- });
736
+ // roll back the current turn to enable reruns
737
+ state._currentTurn--;
738
+ throw new GuardrailExecutionError(`Input guardrail failed to complete: ${e}`, e, state);
739
+ }
674
740
  }
675
- return getOrCreateTrace(async () => {
676
- if (options?.stream) {
677
- return this.#runIndividualStream(agent, input, options);
741
+ }
742
+ async #runOutputGuardrails(state, output) {
743
+ const guardrails = this.outputGuardrailDefs.concat(state._currentAgent.outputGuardrails.map(defineOutputGuardrail));
744
+ if (guardrails.length > 0) {
745
+ const agentOutput = state._currentAgent.processFinalOutput(output);
746
+ const guardrailArgs = {
747
+ agent: state._currentAgent,
748
+ agentOutput,
749
+ context: state._context,
750
+ details: { modelResponse: state._lastTurnResponse },
751
+ };
752
+ try {
753
+ const results = await Promise.all(guardrails.map(async (guardrail) => {
754
+ return withGuardrailSpan(async (span) => {
755
+ const result = await guardrail.run(guardrailArgs);
756
+ span.spanData.triggered = result.output.tripwireTriggered;
757
+ return result;
758
+ }, { data: { name: guardrail.name } }, state._currentAgentSpan);
759
+ }));
760
+ for (const result of results) {
761
+ if (result.output.tripwireTriggered) {
762
+ if (state._currentAgentSpan) {
763
+ state._currentAgentSpan.setError({
764
+ message: 'Guardrail tripwire triggered',
765
+ data: { guardrail: result.guardrail.name },
766
+ });
767
+ }
768
+ throw new OutputGuardrailTripwireTriggered(`Output guardrail triggered: ${JSON.stringify(result.output.outputInfo)}`, result, state);
769
+ }
770
+ }
678
771
  }
679
- else {
680
- return this.#runIndividualNonStream(agent, input, options);
772
+ catch (e) {
773
+ if (e instanceof OutputGuardrailTripwireTriggered) {
774
+ throw e;
775
+ }
776
+ throw new GuardrailExecutionError(`Output guardrail failed to complete: ${e}`, e, state);
681
777
  }
682
- }, {
683
- traceId: this.config.traceId,
684
- name: this.config.workflowName,
685
- groupId: this.config.groupId,
686
- metadata: this.config.traceMetadata,
687
- });
778
+ }
779
+ }
780
+ /**
781
+ * @internal
782
+ * Applies call-level filters and merges session updates so the model request mirrors exactly
783
+ * what we persisted for history.
784
+ */
785
+ async #prepareModelCall(state, options, artifacts, turnInput, serverConversationTracker, sessionInputUpdate) {
786
+ const { model, explictlyModelSet } = await this.#resolveModelForAgent(state._currentAgent);
787
+ let modelSettings = {
788
+ ...this.config.modelSettings,
789
+ ...state._currentAgent.modelSettings,
790
+ };
791
+ modelSettings = adjustModelSettingsForNonGPT5RunnerModel(explictlyModelSet, state._currentAgent.modelSettings, model, modelSettings);
792
+ modelSettings = maybeResetToolChoice(state._currentAgent, state._toolUseTracker, modelSettings);
793
+ const systemInstructions = await state._currentAgent.getSystemPrompt(state._context);
794
+ const prompt = await state._currentAgent.getPrompt(state._context);
795
+ const { modelInput, sourceItems, persistedItems, filterApplied } = await applyCallModelInputFilter(state._currentAgent, options.callModelInputFilter, state._context, turnInput, systemInstructions);
796
+ // Inform the tracker which exact original objects made it to the provider so future turns
797
+ // only send the delta that has not yet been acknowledged by the server.
798
+ serverConversationTracker?.markInputAsSent(sourceItems);
799
+ // Provide filtered clones whenever filters run so session history mirrors the model payload.
800
+ // Returning an empty array is intentional: it tells the session layer to persist "nothing"
801
+ // instead of falling back to the unfiltered originals when the filter redacts everything.
802
+ sessionInputUpdate?.(sourceItems, filterApplied ? persistedItems : undefined);
803
+ const previousResponseId = serverConversationTracker?.previousResponseId ??
804
+ options.previousResponseId;
805
+ const conversationId = serverConversationTracker?.conversationId ?? options.conversationId;
806
+ return {
807
+ ...artifacts,
808
+ model,
809
+ explictlyModelSet,
810
+ modelSettings,
811
+ modelInput,
812
+ prompt,
813
+ previousResponseId,
814
+ conversationId,
815
+ };
688
816
  }
689
817
  }
818
+ /**
819
+ * Constructs the model input array for the current turn by combining the original turn input with
820
+ * any new run items (excluding tool approval placeholders). This helps ensure that repeated calls
821
+ * to the Responses API only send newly generated content.
822
+ *
823
+ * See: https://platform.openai.com/docs/guides/conversation-state?api-mode=responses.
824
+ */
825
+ export function getTurnInput(originalInput, generatedItems) {
826
+ const rawItems = generatedItems
827
+ .filter((item) => item.type !== 'tool_approval_item') // don't include approval items to avoid double function calls
828
+ .map((item) => item.rawItem);
829
+ return [...toAgentInputList(originalInput), ...rawItems];
830
+ }
831
+ // --------------------------------------------------------------
832
+ // Internal helpers
833
+ // --------------------------------------------------------------
834
+ const DEFAULT_MAX_TURNS = 10;
690
835
  let _defaultRunner = undefined;
691
836
  function getDefaultRunner() {
692
837
  if (_defaultRunner) {
@@ -695,9 +840,13 @@ function getDefaultRunner() {
695
840
  _defaultRunner = new Runner();
696
841
  return _defaultRunner;
697
842
  }
843
+ /**
844
+ * Resolves the effective model for the next turn by giving precedence to the agent-specific
845
+ * configuration when present, otherwise falling back to the runner-level default.
846
+ */
698
847
  export function selectModel(agentModel, runConfigModel) {
699
848
  // When initializing an agent without model name, the model property is set to an empty string. So,
700
- // * agentModel === '' & runConfigModel exists, runConfigModel will be used
849
+ // * agentModel === Agent.DEFAULT_MODEL_PLACEHOLDER & runConfigModel exists, runConfigModel will be used
701
850
  // * agentModel is set, the agentModel will be used over runConfigModel
702
851
  if ((typeof agentModel === 'string' &&
703
852
  agentModel !== Agent.DEFAULT_MODEL_PLACEHOLDER) ||
@@ -707,13 +856,259 @@ export function selectModel(agentModel, runConfigModel) {
707
856
  }
708
857
  return runConfigModel ?? agentModel ?? Agent.DEFAULT_MODEL_PLACEHOLDER;
709
858
  }
710
- export async function run(agent, input, options) {
711
- const runner = getDefaultRunner();
712
- if (options?.stream) {
713
- return await runner.run(agent, input, options);
859
+ /**
860
+ * Normalizes tracing configuration into the format expected by model providers.
861
+ * Returns `false` to disable tracing, `true` to include full payload data, or
862
+ * `'enabled_without_data'` to omit sensitive content while still emitting spans.
863
+ */
864
+ export function getTracing(tracingDisabled, traceIncludeSensitiveData) {
865
+ if (tracingDisabled) {
866
+ return false;
714
867
  }
715
- else {
716
- return await runner.run(agent, input, options);
868
+ if (traceIncludeSensitiveData) {
869
+ return true;
870
+ }
871
+ return 'enabled_without_data';
872
+ }
873
+ /**
874
+ * @internal
875
+ */
876
+ async function applyCallModelInputFilter(agent, callModelInputFilter, context, inputItems, systemInstructions) {
877
+ const cloneInputItems = (items, map) => items.map((item) => {
878
+ const cloned = structuredClone(item);
879
+ if (map && cloned && typeof cloned === 'object') {
880
+ map.set(cloned, item);
881
+ }
882
+ return cloned;
883
+ });
884
+ // Record the relationship between the cloned array passed to filters and the original inputs.
885
+ const cloneMap = new WeakMap();
886
+ const originalPool = buildAgentInputPool(inputItems);
887
+ const fallbackOriginals = [];
888
+ // Track any original object inputs so filtered replacements can still mark them as delivered.
889
+ for (const item of inputItems) {
890
+ if (item && typeof item === 'object') {
891
+ fallbackOriginals.push(item);
892
+ }
893
+ }
894
+ const removeFromFallback = (candidate) => {
895
+ if (!candidate || typeof candidate !== 'object') {
896
+ return;
897
+ }
898
+ const index = fallbackOriginals.findIndex((original) => original === candidate);
899
+ if (index !== -1) {
900
+ fallbackOriginals.splice(index, 1);
901
+ }
902
+ };
903
+ const takeFallbackOriginal = () => {
904
+ const next = fallbackOriginals.shift();
905
+ if (next) {
906
+ removeAgentInputFromPool(originalPool, next);
907
+ }
908
+ return next;
909
+ };
910
+ // Always create a deep copy so downstream mutations inside filters cannot affect
911
+ // the cached turn state.
912
+ const clonedBaseInput = cloneInputItems(inputItems, cloneMap);
913
+ const base = {
914
+ input: clonedBaseInput,
915
+ instructions: systemInstructions,
916
+ };
917
+ if (!callModelInputFilter) {
918
+ return {
919
+ modelInput: base,
920
+ sourceItems: [...inputItems],
921
+ persistedItems: [],
922
+ filterApplied: false,
923
+ };
924
+ }
925
+ try {
926
+ const result = await callModelInputFilter({
927
+ modelData: base,
928
+ agent,
929
+ context: context.context,
930
+ });
931
+ if (!result || !Array.isArray(result.input)) {
932
+ throw new UserError('callModelInputFilter must return a ModelInputData object with an input array.');
933
+ }
934
+ // Preserve a pointer to the original object backing each filtered clone so downstream
935
+ // trackers can keep their bookkeeping consistent even after redaction.
936
+ const sourceItems = result.input.map((item) => {
937
+ if (!item || typeof item !== 'object') {
938
+ return undefined;
939
+ }
940
+ const original = cloneMap.get(item);
941
+ if (original) {
942
+ removeFromFallback(original);
943
+ removeAgentInputFromPool(originalPool, original);
944
+ return original;
945
+ }
946
+ const key = getAgentInputItemKey(item);
947
+ const matchedByContent = takeAgentInputFromPool(originalPool, key);
948
+ if (matchedByContent) {
949
+ removeFromFallback(matchedByContent);
950
+ return matchedByContent;
951
+ }
952
+ const fallback = takeFallbackOriginal();
953
+ if (fallback) {
954
+ return fallback;
955
+ }
956
+ return undefined;
957
+ });
958
+ const clonedFilteredInput = cloneInputItems(result.input);
959
+ return {
960
+ modelInput: {
961
+ input: clonedFilteredInput,
962
+ instructions: typeof result.instructions === 'undefined'
963
+ ? systemInstructions
964
+ : result.instructions,
965
+ },
966
+ sourceItems,
967
+ persistedItems: clonedFilteredInput.map((item) => structuredClone(item)),
968
+ filterApplied: true,
969
+ };
970
+ }
971
+ catch (error) {
972
+ addErrorToCurrentSpan({
973
+ message: 'Error in callModelInputFilter',
974
+ data: { error: String(error) },
975
+ });
976
+ throw error;
977
+ }
978
+ }
979
+ // Tracks which items have already been sent to or received from the Responses API when the caller
980
+ // supplies `conversationId`/`previousResponseId`. This ensures we only send the delta each turn.
981
+ class ServerConversationTracker {
982
+ // Conversation ID:
983
+ // - https://platform.openai.com/docs/guides/conversation-state?api-mode=responses#using-the-conversations-api
984
+ // - https://platform.openai.com/docs/api-reference/conversations/create
985
+ conversationId;
986
+ // Previous Response ID:
987
+ // https://platform.openai.com/docs/guides/conversation-state?api-mode=responses#passing-context-from-the-previous-response
988
+ previousResponseId;
989
+ // Using this flag because WeakSet does not provide a way to check its size
990
+ sentInitialInput = false;
991
+ // The items already sent to the model; using WeakSet for memory efficiency
992
+ sentItems = new WeakSet();
993
+ // The items received from the server; using WeakSet for memory efficiency
994
+ serverItems = new WeakSet();
995
+ // Track initial input items that have not yet been sent so they can be retried on later turns.
996
+ remainingInitialInput = null;
997
+ constructor({ conversationId, previousResponseId, }) {
998
+ this.conversationId = conversationId ?? undefined;
999
+ this.previousResponseId = previousResponseId ?? undefined;
1000
+ }
1001
+ /**
1002
+ * Pre-populates tracker caches from an existing RunState when resuming server-managed runs.
1003
+ */
1004
+ primeFromState({ originalInput, generatedItems, modelResponses, }) {
1005
+ if (this.sentInitialInput) {
1006
+ return;
1007
+ }
1008
+ for (const item of toAgentInputList(originalInput)) {
1009
+ if (item && typeof item === 'object') {
1010
+ this.sentItems.add(item);
1011
+ }
1012
+ }
1013
+ this.sentInitialInput = true;
1014
+ this.remainingInitialInput = null;
1015
+ const latestResponse = modelResponses[modelResponses.length - 1];
1016
+ for (const response of modelResponses) {
1017
+ for (const item of response.output) {
1018
+ if (item && typeof item === 'object') {
1019
+ this.serverItems.add(item);
1020
+ }
1021
+ }
1022
+ }
1023
+ if (!this.conversationId && latestResponse?.responseId) {
1024
+ this.previousResponseId = latestResponse.responseId;
1025
+ }
1026
+ for (const item of generatedItems) {
1027
+ const rawItem = item.rawItem;
1028
+ if (!rawItem || typeof rawItem !== 'object') {
1029
+ continue;
1030
+ }
1031
+ if (this.serverItems.has(rawItem)) {
1032
+ this.sentItems.add(rawItem);
1033
+ }
1034
+ }
1035
+ }
1036
+ /**
1037
+ * Records the raw items returned by the server so future delta calculations skip them.
1038
+ * Also captures the latest response identifier to chain follow-up calls when possible.
1039
+ */
1040
+ trackServerItems(modelResponse) {
1041
+ if (!modelResponse) {
1042
+ return;
1043
+ }
1044
+ for (const item of modelResponse.output) {
1045
+ if (item && typeof item === 'object') {
1046
+ this.serverItems.add(item);
1047
+ }
1048
+ }
1049
+ if (!this.conversationId && modelResponse.responseId) {
1050
+ this.previousResponseId = modelResponse.responseId;
1051
+ }
1052
+ }
1053
+ /**
1054
+ * Returns the minimum set of items that still need to be delivered to the server for the
1055
+ * current turn. This includes the original turn inputs (until acknowledged) plus any
1056
+ * newly generated items that have not yet been echoed back by the API.
1057
+ */
1058
+ prepareInput(originalInput, generatedItems) {
1059
+ const inputItems = [];
1060
+ if (!this.sentInitialInput) {
1061
+ const initialItems = toAgentInputList(originalInput);
1062
+ // Preserve the full initial payload so a filter can drop items without losing their originals.
1063
+ inputItems.push(...initialItems);
1064
+ this.remainingInitialInput = initialItems.filter((item) => Boolean(item) && typeof item === 'object');
1065
+ this.sentInitialInput = true;
1066
+ }
1067
+ else if (this.remainingInitialInput &&
1068
+ this.remainingInitialInput.length > 0) {
1069
+ // Re-queue prior initial items until the tracker confirms they were delivered to the API.
1070
+ inputItems.push(...this.remainingInitialInput);
1071
+ }
1072
+ for (const item of generatedItems) {
1073
+ if (item.type === 'tool_approval_item') {
1074
+ continue;
1075
+ }
1076
+ const rawItem = item.rawItem;
1077
+ if (!rawItem || typeof rawItem !== 'object') {
1078
+ continue;
1079
+ }
1080
+ if (this.sentItems.has(rawItem) || this.serverItems.has(rawItem)) {
1081
+ continue;
1082
+ }
1083
+ inputItems.push(rawItem);
1084
+ }
1085
+ return inputItems;
1086
+ }
1087
+ /**
1088
+ * Marks the provided originals as delivered so future turns do not resend them and any
1089
+ * pending initial inputs can be dropped once the server acknowledges receipt.
1090
+ */
1091
+ markInputAsSent(items) {
1092
+ if (!items.length) {
1093
+ return;
1094
+ }
1095
+ const delivered = new Set();
1096
+ for (const item of items) {
1097
+ if (!item || typeof item !== 'object' || delivered.has(item)) {
1098
+ continue;
1099
+ }
1100
+ // Some inputs may be repeated in the filtered list; only mark unique originals once.
1101
+ delivered.add(item);
1102
+ this.sentItems.add(item);
1103
+ }
1104
+ if (!this.remainingInitialInput ||
1105
+ this.remainingInitialInput.length === 0) {
1106
+ return;
1107
+ }
1108
+ this.remainingInitialInput = this.remainingInitialInput.filter((item) => !delivered.has(item));
1109
+ if (this.remainingInitialInput.length === 0) {
1110
+ this.remainingInitialInput = null;
1111
+ }
717
1112
  }
718
1113
  }
719
1114
  /**
@@ -750,4 +1145,121 @@ function adjustModelSettingsForNonGPT5RunnerModel(explictlyModelSet, agentModelS
750
1145
  }
751
1146
  return modelSettings;
752
1147
  }
1148
+ /**
1149
+ * @internal
1150
+ * Collects tools/handoffs early so we can annotate spans before model execution begins.
1151
+ */
1152
+ async function prepareAgentArtifacts(state) {
1153
+ const handoffs = await state._currentAgent.getEnabledHandoffs(state._context);
1154
+ const tools = await state._currentAgent.getAllTools(state._context);
1155
+ if (!state._currentAgentSpan) {
1156
+ const handoffNames = handoffs.map((h) => h.agentName);
1157
+ state._currentAgentSpan = createAgentSpan({
1158
+ data: {
1159
+ name: state._currentAgent.name,
1160
+ handoffs: handoffNames,
1161
+ tools: tools.map((t) => t.name),
1162
+ output_type: state._currentAgent.outputSchemaName,
1163
+ },
1164
+ });
1165
+ state._currentAgentSpan.start();
1166
+ setCurrentSpan(state._currentAgentSpan);
1167
+ }
1168
+ else {
1169
+ state._currentAgentSpan.spanData.tools = tools.map((t) => t.name);
1170
+ }
1171
+ return {
1172
+ handoffs,
1173
+ tools,
1174
+ serializedHandoffs: handoffs.map((handoff) => serializeHandoff(handoff)),
1175
+ serializedTools: tools.map((tool) => serializeTool(tool)),
1176
+ };
1177
+ }
1178
+ function getAgentInputItemKey(item) {
1179
+ // Deep serialization keeps binary inputs comparable after filters clone them.
1180
+ return JSON.stringify(item, agentInputSerializationReplacer);
1181
+ }
1182
+ function buildAgentInputPool(items) {
1183
+ // Track every original object so filters can safely return cloned copies.
1184
+ const pool = new Map();
1185
+ for (const item of items) {
1186
+ const key = getAgentInputItemKey(item);
1187
+ const existing = pool.get(key);
1188
+ if (existing) {
1189
+ existing.push(item);
1190
+ }
1191
+ else {
1192
+ pool.set(key, [item]);
1193
+ }
1194
+ }
1195
+ return pool;
1196
+ }
1197
+ function takeAgentInputFromPool(pool, key) {
1198
+ // Prefer reusing the earliest untouched original to keep ordering stable.
1199
+ const candidates = pool.get(key);
1200
+ if (!candidates || candidates.length === 0) {
1201
+ return undefined;
1202
+ }
1203
+ const [first] = candidates;
1204
+ candidates.shift();
1205
+ if (candidates.length === 0) {
1206
+ pool.delete(key);
1207
+ }
1208
+ return first;
1209
+ }
1210
+ function removeAgentInputFromPool(pool, item) {
1211
+ // Remove exactly the matched instance so duplicate payloads remain available.
1212
+ const key = getAgentInputItemKey(item);
1213
+ const candidates = pool.get(key);
1214
+ if (!candidates || candidates.length === 0) {
1215
+ return;
1216
+ }
1217
+ const index = candidates.findIndex((candidate) => candidate === item);
1218
+ if (index === -1) {
1219
+ return;
1220
+ }
1221
+ candidates.splice(index, 1);
1222
+ if (candidates.length === 0) {
1223
+ pool.delete(key);
1224
+ }
1225
+ }
1226
+ function agentInputSerializationReplacer(_key, value) {
1227
+ // Mirror runImplementation serialization so buffer snapshots round-trip.
1228
+ if (value instanceof ArrayBuffer) {
1229
+ return {
1230
+ __type: 'ArrayBuffer',
1231
+ data: encodeUint8ArrayToBase64(new Uint8Array(value)),
1232
+ };
1233
+ }
1234
+ if (isArrayBufferView(value)) {
1235
+ const view = value;
1236
+ return {
1237
+ __type: view.constructor.name,
1238
+ data: encodeUint8ArrayToBase64(new Uint8Array(view.buffer, view.byteOffset, view.byteLength)),
1239
+ };
1240
+ }
1241
+ if (isNodeBuffer(value)) {
1242
+ const view = value;
1243
+ return {
1244
+ __type: 'Buffer',
1245
+ data: encodeUint8ArrayToBase64(new Uint8Array(view.buffer, view.byteOffset, view.byteLength)),
1246
+ };
1247
+ }
1248
+ if (isSerializedBufferSnapshot(value)) {
1249
+ return {
1250
+ __type: 'Buffer',
1251
+ data: encodeUint8ArrayToBase64(Uint8Array.from(value.data)),
1252
+ };
1253
+ }
1254
+ return value;
1255
+ }
1256
+ // Normalizes user-provided input into the structure the model expects. Strings become user messages,
1257
+ // arrays are kept as-is so downstream loops can treat both scenarios uniformly.
1258
+ function toAgentInputList(originalInput) {
1259
+ // Allow callers to pass plain strings while preserving original item order.
1260
+ if (typeof originalInput === 'string') {
1261
+ return [{ type: 'message', role: 'user', content: originalInput }];
1262
+ }
1263
+ return [...originalInput];
1264
+ }
753
1265
  //# sourceMappingURL=run.mjs.map