@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.
- package/dist/editor.d.ts +38 -0
- package/dist/editor.js +3 -0
- package/dist/editor.js.map +1 -0
- package/dist/editor.mjs +2 -0
- package/dist/editor.mjs.map +1 -0
- package/dist/extensions/handoffFilters.js +4 -0
- package/dist/extensions/handoffFilters.js.map +1 -1
- package/dist/extensions/handoffFilters.mjs +4 -0
- package/dist/extensions/handoffFilters.mjs.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/dist/items.d.ts +518 -4
- package/dist/items.js +22 -1
- package/dist/items.js.map +1 -1
- package/dist/items.mjs +22 -1
- package/dist/items.mjs.map +1 -1
- package/dist/memory/memorySession.d.ts +22 -0
- package/dist/memory/memorySession.js +64 -0
- package/dist/memory/memorySession.js.map +1 -0
- package/dist/memory/memorySession.mjs +60 -0
- package/dist/memory/memorySession.mjs.map +1 -0
- package/dist/memory/session.d.ts +36 -0
- package/dist/memory/session.js +3 -0
- package/dist/memory/session.js.map +1 -0
- package/dist/memory/session.mjs +2 -0
- package/dist/memory/session.mjs.map +1 -0
- package/dist/metadata.js +2 -2
- package/dist/metadata.mjs +2 -2
- package/dist/model.d.ts +10 -2
- package/dist/run.d.ts +88 -8
- package/dist/run.js +859 -347
- package/dist/run.js.map +1 -1
- package/dist/run.mjs +859 -347
- package/dist/run.mjs.map +1 -1
- package/dist/runContext.js +2 -2
- package/dist/runContext.js.map +1 -1
- package/dist/runContext.mjs +2 -2
- package/dist/runContext.mjs.map +1 -1
- package/dist/runImplementation.d.ts +37 -3
- package/dist/runImplementation.js +1116 -319
- package/dist/runImplementation.js.map +1 -1
- package/dist/runImplementation.mjs +1106 -317
- package/dist/runImplementation.mjs.map +1 -1
- package/dist/runState.d.ts +4453 -785
- package/dist/runState.js +62 -3
- package/dist/runState.js.map +1 -1
- package/dist/runState.mjs +62 -3
- package/dist/runState.mjs.map +1 -1
- package/dist/shell.d.ts +36 -0
- package/dist/shell.js +3 -0
- package/dist/shell.js.map +1 -0
- package/dist/shell.mjs +2 -0
- package/dist/shell.mjs.map +1 -0
- package/dist/tool.d.ts +62 -1
- package/dist/tool.js +30 -0
- package/dist/tool.js.map +1 -1
- package/dist/tool.mjs +28 -0
- package/dist/tool.mjs.map +1 -1
- package/dist/types/aliases.d.ts +3 -3
- package/dist/types/protocol.d.ts +5470 -1519
- package/dist/types/protocol.js +74 -1
- package/dist/types/protocol.js.map +1 -1
- package/dist/types/protocol.mjs +73 -0
- package/dist/types/protocol.mjs.map +1 -1
- package/dist/utils/applyDiff.d.ts +9 -0
- package/dist/utils/applyDiff.js +275 -0
- package/dist/utils/applyDiff.js.map +1 -0
- package/dist/utils/applyDiff.mjs +272 -0
- package/dist/utils/applyDiff.mjs.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +3 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs +1 -0
- package/dist/utils/index.mjs.map +1 -1
- package/dist/utils/serialize.js +12 -0
- package/dist/utils/serialize.js.map +1 -1
- package/dist/utils/serialize.mjs +12 -0
- package/dist/utils/serialize.mjs.map +1 -1
- package/dist/utils/smartString.d.ts +9 -0
- package/dist/utils/smartString.js +15 -0
- package/dist/utils/smartString.js.map +1 -1
- package/dist/utils/smartString.mjs +14 -3
- package/dist/utils/smartString.mjs.map +1 -1
- 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,
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
154
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
393
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
for await (const event of model.getStreamedResponse({
|
|
526
|
-
systemInstructions:
|
|
527
|
-
prompt:
|
|
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
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
return
|
|
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
|
-
|
|
671
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
680
|
-
|
|
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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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 ===
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
716
|
-
return
|
|
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
|