@zhivex-ai/core 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/api-stability.js +7 -7
- package/dist/api-stability.js.map +1 -1
- package/dist/bounded-broadcast.d.ts +1 -1
- package/dist/bounded-broadcast.d.ts.map +1 -1
- package/dist/bounded-broadcast.js +4 -2
- package/dist/bounded-broadcast.js.map +1 -1
- package/dist/live-agent.d.ts.map +1 -1
- package/dist/live-agent.js +691 -324
- package/dist/live-agent.js.map +1 -1
- package/dist/realtime.d.ts +19 -2
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js +379 -100
- package/dist/realtime.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/live-agent.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { BoundedReplayBroadcast } from "./bounded-broadcast.js";
|
|
2
|
-
import { GuardrailTriggeredError } from "./errors.js";
|
|
2
|
+
import { ConflictError, GuardrailTriggeredError, ValidationError } from "./errors.js";
|
|
3
3
|
import { AGENT_RUN_STATE_SCHEMA_VERSION, normalizeAgentRunState } from "./agent-state.js";
|
|
4
4
|
import { normalizeMessages } from "./generate-text.js";
|
|
5
5
|
import { createTextMessage, getTextFromParts, isCallableToolDefinition, serializeJsonValue, toolResultPart } from "./messages.js";
|
|
6
|
-
import { mergeAbortSignals } from "./runtime.js";
|
|
7
6
|
import { createSecureId } from "./secure-id.js";
|
|
8
7
|
import { toToolSet } from "./tool-registry.js";
|
|
9
8
|
const joinInstructions = (...parts) => {
|
|
@@ -15,12 +14,14 @@ const cloneMetadata = (...values) => {
|
|
|
15
14
|
return Object.keys(merged).length ? merged : undefined;
|
|
16
15
|
};
|
|
17
16
|
const cloneState = (state) => JSON.parse(JSON.stringify(normalizeAgentRunState(state)));
|
|
18
|
-
const createBaseState = (provider, modelId, initialMessages, metadata, agentId, runId) => {
|
|
17
|
+
const createBaseState = (provider, modelId, initialMessages, metadata, agentId, runId, scope, idempotencyKey) => {
|
|
19
18
|
const startedAt = Date.now();
|
|
20
19
|
return {
|
|
21
20
|
schemaVersion: AGENT_RUN_STATE_SCHEMA_VERSION,
|
|
22
21
|
revision: 0,
|
|
22
|
+
scope,
|
|
23
23
|
runId,
|
|
24
|
+
idempotencyKey,
|
|
24
25
|
agentId,
|
|
25
26
|
provider,
|
|
26
27
|
modelId,
|
|
@@ -37,12 +38,6 @@ const createBaseState = (provider, modelId, initialMessages, metadata, agentId,
|
|
|
37
38
|
updatedAt: startedAt
|
|
38
39
|
};
|
|
39
40
|
};
|
|
40
|
-
const createFailedState = (state, message) => ({
|
|
41
|
-
...state,
|
|
42
|
-
status: "failed",
|
|
43
|
-
error: { message },
|
|
44
|
-
updatedAt: Date.now()
|
|
45
|
-
});
|
|
46
41
|
const applyGuardrailFailure = (state, stage, trigger) => ({
|
|
47
42
|
...state,
|
|
48
43
|
status: "failed",
|
|
@@ -51,44 +46,70 @@ const applyGuardrailFailure = (state, stage, trigger) => ({
|
|
|
51
46
|
},
|
|
52
47
|
updatedAt: Date.now()
|
|
53
48
|
});
|
|
54
|
-
const emitTelemetryEvent = async (agent, event) => {
|
|
55
|
-
|
|
49
|
+
const emitTelemetryEvent = async (agent, event, abortSignal) => {
|
|
50
|
+
const operation = Promise.resolve().then(() => agent.onTelemetryEvent?.(event));
|
|
51
|
+
if (abortSignal)
|
|
52
|
+
await raceWithAbort(operation, abortSignal);
|
|
53
|
+
else
|
|
54
|
+
await operation;
|
|
55
|
+
};
|
|
56
|
+
const checkpointState = async (agent, state, abortSignal) => {
|
|
57
|
+
if (agent.store) {
|
|
58
|
+
const expectedRevision = state.revision ?? 0;
|
|
59
|
+
const nextRevision = expectedRevision + 1;
|
|
60
|
+
if (abortSignal?.aborted)
|
|
61
|
+
throw abortError(abortSignal);
|
|
62
|
+
const save = Promise.resolve(agent.store.save(cloneState({ ...state, revision: nextRevision }), { expectedRevision }));
|
|
63
|
+
if (abortSignal)
|
|
64
|
+
await raceWithAbort(save, abortSignal);
|
|
65
|
+
else
|
|
66
|
+
await save;
|
|
67
|
+
state.revision = nextRevision;
|
|
68
|
+
}
|
|
56
69
|
};
|
|
57
|
-
const persistState = async (agent, state) => {
|
|
70
|
+
const persistState = async (agent, state, abortSignal) => {
|
|
58
71
|
state.updatedAt = Date.now();
|
|
59
|
-
await agent
|
|
72
|
+
await checkpointState(agent, state, abortSignal);
|
|
60
73
|
await emitTelemetryEvent(agent, {
|
|
61
74
|
type: "state-saved",
|
|
62
75
|
runId: state.runId,
|
|
63
76
|
agentId: state.agentId,
|
|
64
77
|
status: state.status
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
}, abortSignal);
|
|
79
|
+
if (agent.memory?.save) {
|
|
80
|
+
const save = Promise.resolve().then(() => agent.memory.save({
|
|
81
|
+
runId: state.runId,
|
|
82
|
+
agentId: state.agentId,
|
|
83
|
+
scope: state.scope,
|
|
84
|
+
state: cloneState(state),
|
|
85
|
+
metadata: state.metadata
|
|
86
|
+
}));
|
|
87
|
+
if (abortSignal)
|
|
88
|
+
await raceWithAbort(save, abortSignal);
|
|
89
|
+
else
|
|
90
|
+
await save;
|
|
91
|
+
}
|
|
72
92
|
};
|
|
73
|
-
const runGuardrails = async (agent, state, stage, guardrails, requestFactory) => {
|
|
93
|
+
const runGuardrails = async (agent, state, stage, guardrails, requestFactory, abortSignal) => {
|
|
74
94
|
for (const [index, guardrail] of (guardrails ?? []).entries()) {
|
|
75
|
-
const
|
|
95
|
+
const execution = Promise.resolve().then(() => guardrail(requestFactory(index)));
|
|
96
|
+
const trigger = abortSignal ? await raceWithAbort(execution, abortSignal) : await execution;
|
|
76
97
|
if (!trigger?.triggered) {
|
|
77
98
|
continue;
|
|
78
99
|
}
|
|
79
|
-
await agent
|
|
100
|
+
await emitTelemetryEvent(agent, {
|
|
80
101
|
type: "guardrail-triggered",
|
|
81
102
|
runId: state.runId,
|
|
82
103
|
agentId: state.agentId,
|
|
83
104
|
stage,
|
|
84
105
|
reason: trigger.reason ?? `Agent ${stage} guardrail #${index + 1} triggered.`,
|
|
85
106
|
metadata: trigger.metadata
|
|
86
|
-
});
|
|
107
|
+
}, abortSignal);
|
|
87
108
|
return trigger;
|
|
88
109
|
}
|
|
89
110
|
return undefined;
|
|
90
111
|
};
|
|
91
|
-
const emitRunStartTelemetry = async (agent, state, memoryMessages) => {
|
|
112
|
+
const emitRunStartTelemetry = async (agent, state, memoryMessages, abortSignal) => {
|
|
92
113
|
await emitTelemetryEvent(agent, {
|
|
93
114
|
type: "run-start",
|
|
94
115
|
runId: state.runId,
|
|
@@ -96,48 +117,53 @@ const emitRunStartTelemetry = async (agent, state, memoryMessages) => {
|
|
|
96
117
|
provider: state.provider,
|
|
97
118
|
modelId: state.modelId,
|
|
98
119
|
maxSteps: state.maxSteps
|
|
99
|
-
});
|
|
120
|
+
}, abortSignal);
|
|
100
121
|
if (memoryMessages.length) {
|
|
101
122
|
await emitTelemetryEvent(agent, {
|
|
102
123
|
type: "memory-loaded",
|
|
103
124
|
runId: state.runId,
|
|
104
125
|
agentId: state.agentId,
|
|
105
126
|
messageCount: memoryMessages.length
|
|
106
|
-
});
|
|
127
|
+
}, abortSignal);
|
|
107
128
|
}
|
|
108
129
|
};
|
|
109
|
-
const emitRunFinishTelemetry = async (agent, state) => {
|
|
130
|
+
const emitRunFinishTelemetry = async (agent, state, abortSignal) => {
|
|
110
131
|
await emitTelemetryEvent(agent, {
|
|
111
132
|
type: "run-finish",
|
|
112
133
|
runId: state.runId,
|
|
113
134
|
agentId: state.agentId,
|
|
114
135
|
status: state.status,
|
|
115
136
|
state: cloneState(state)
|
|
116
|
-
});
|
|
137
|
+
}, abortSignal);
|
|
117
138
|
};
|
|
118
139
|
const withToolTimeout = async (operation, timeoutMs, abortSignal) => {
|
|
119
|
-
if (!timeoutMs) {
|
|
120
|
-
return operation(abortSignal);
|
|
121
|
-
}
|
|
122
140
|
const controller = new AbortController();
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
141
|
+
const abortFromCaller = () => controller.abort(abortSignal?.reason);
|
|
142
|
+
if (abortSignal?.aborted) {
|
|
143
|
+
abortFromCaller();
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
abortSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
147
|
+
}
|
|
148
|
+
const signal = controller.signal;
|
|
149
|
+
const timer = timeoutMs === undefined
|
|
150
|
+
? undefined
|
|
151
|
+
: setTimeout(() => controller.abort(new ToolExecutionTimeoutError(timeoutMs)), timeoutMs);
|
|
152
|
+
try {
|
|
153
|
+
return await raceWithAbort(Promise.resolve().then(() => operation(signal)), signal);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
if (timer)
|
|
136
157
|
clearTimeout(timer);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
});
|
|
158
|
+
abortSignal?.removeEventListener("abort", abortFromCaller);
|
|
159
|
+
}
|
|
140
160
|
};
|
|
161
|
+
class ToolExecutionTimeoutError extends Error {
|
|
162
|
+
constructor(timeoutMs) {
|
|
163
|
+
super(`Tool execution timed out after ${timeoutMs}ms.`);
|
|
164
|
+
this.name = "ToolExecutionTimeoutError";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
141
167
|
const injectContextMessages = (messages, extraMessages) => {
|
|
142
168
|
if (!extraMessages.length) {
|
|
143
169
|
return messages;
|
|
@@ -148,6 +174,116 @@ const injectContextMessages = (messages, extraMessages) => {
|
|
|
148
174
|
return [...extraMessages, ...messages];
|
|
149
175
|
};
|
|
150
176
|
const textFromMessage = (message) => getTextFromParts(message.parts).trim();
|
|
177
|
+
const contextInstructions = (messages) => {
|
|
178
|
+
const lines = messages.flatMap((message) => {
|
|
179
|
+
const text = textFromMessage(message);
|
|
180
|
+
return text ? [`${message.role}: ${text}`] : [];
|
|
181
|
+
});
|
|
182
|
+
return lines.length ? `Conversation context:\n${lines.join("\n")}` : undefined;
|
|
183
|
+
};
|
|
184
|
+
const ensureValidScope = (scope) => {
|
|
185
|
+
if (!scope)
|
|
186
|
+
return;
|
|
187
|
+
if (typeof scope.tenantId !== "string" || scope.tenantId.length === 0) {
|
|
188
|
+
throw new ValidationError('Live agent scope "tenantId" must be a non-empty string.');
|
|
189
|
+
}
|
|
190
|
+
for (const field of ["userId", "namespace"]) {
|
|
191
|
+
if (scope[field] !== undefined && (typeof scope[field] !== "string" || scope[field].length === 0)) {
|
|
192
|
+
throw new ValidationError(`Live agent scope "${field}" must be a non-empty string when provided.`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const ensureDurableConfiguration = (agent, input, tools) => {
|
|
197
|
+
ensureValidScope(input.scope);
|
|
198
|
+
if (input.idempotencyKey && !agent.store) {
|
|
199
|
+
throw new ValidationError('The live agent "idempotencyKey" option requires an agent run "store".');
|
|
200
|
+
}
|
|
201
|
+
if (input.idempotencyKey && !agent.store?.claimIdempotencyKey) {
|
|
202
|
+
throw new ValidationError('The live agent run "store" must implement "claimIdempotencyKey()" to use "idempotencyKey" safely.');
|
|
203
|
+
}
|
|
204
|
+
const hasCallableTools = Object.values(toToolSet(tools) ?? {}).some(isCallableToolDefinition);
|
|
205
|
+
if (agent.store &&
|
|
206
|
+
hasCallableTools &&
|
|
207
|
+
(!agent.store.claimToolExecution || !agent.store.loadToolExecution || !agent.store.completeToolExecution)) {
|
|
208
|
+
throw new ValidationError('A live agent run "store" with local tools must implement claimToolExecution(), loadToolExecution(), and completeToolExecution().');
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
class LiveAgentTimeoutError extends Error {
|
|
212
|
+
constructor(timeoutMs) {
|
|
213
|
+
super(`Live agent run timed out after ${timeoutMs}ms.`);
|
|
214
|
+
this.name = "LiveAgentTimeoutError";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const createLifetimeAbort = (input) => {
|
|
218
|
+
if (input.timeoutMs !== undefined &&
|
|
219
|
+
(!Number.isSafeInteger(input.timeoutMs) || input.timeoutMs <= 0 || input.timeoutMs > 24 * 60 * 60 * 1_000)) {
|
|
220
|
+
throw new ValidationError('The "timeoutMs" option must be a positive safe integer no greater than 86400000.');
|
|
221
|
+
}
|
|
222
|
+
const controller = new AbortController();
|
|
223
|
+
const sources = [input.abortSignal, input.connectOptions?.signal].filter((signal) => Boolean(signal));
|
|
224
|
+
const listeners = new Map();
|
|
225
|
+
for (const source of sources) {
|
|
226
|
+
const abort = () => controller.abort(source.reason);
|
|
227
|
+
listeners.set(source, abort);
|
|
228
|
+
if (source.aborted) {
|
|
229
|
+
abort();
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
source.addEventListener("abort", abort, { once: true });
|
|
233
|
+
}
|
|
234
|
+
const timer = input.timeoutMs === undefined
|
|
235
|
+
? undefined
|
|
236
|
+
: setTimeout(() => controller.abort(new LiveAgentTimeoutError(input.timeoutMs)), input.timeoutMs);
|
|
237
|
+
return {
|
|
238
|
+
signal: controller.signal,
|
|
239
|
+
cleanup: () => {
|
|
240
|
+
if (timer)
|
|
241
|
+
clearTimeout(timer);
|
|
242
|
+
for (const [source, listener] of listeners) {
|
|
243
|
+
source.removeEventListener("abort", listener);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
const abortError = (signal) => signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted.", "AbortError");
|
|
249
|
+
const raceWithAbort = async (operation, signal) => {
|
|
250
|
+
if (signal.aborted)
|
|
251
|
+
throw abortError(signal);
|
|
252
|
+
return new Promise((resolve, reject) => {
|
|
253
|
+
const onAbort = () => reject(abortError(signal));
|
|
254
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
255
|
+
operation.then((value) => {
|
|
256
|
+
signal.removeEventListener("abort", onAbort);
|
|
257
|
+
resolve(value);
|
|
258
|
+
}, (error) => {
|
|
259
|
+
signal.removeEventListener("abort", onAbort);
|
|
260
|
+
reject(error);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
const TERMINAL_FAILURE_CLEANUP_TIMEOUT_MS = 1_000;
|
|
265
|
+
const runTerminalFailureCleanup = async (operations) => {
|
|
266
|
+
const controller = new AbortController();
|
|
267
|
+
const timer = setTimeout(() => controller.abort(new DOMException("Terminal live-agent cleanup timed out.", "TimeoutError")), TERMINAL_FAILURE_CLEANUP_TIMEOUT_MS);
|
|
268
|
+
try {
|
|
269
|
+
await Promise.allSettled(operations.map((operation) => raceWithAbort(Promise.resolve().then(() => operation(controller.signal)), controller.signal)));
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
clearTimeout(timer);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const nextWithAbort = (iterator, signal) => raceWithAbort(Promise.resolve(iterator.next()), signal);
|
|
276
|
+
const canonicalJson = (value) => {
|
|
277
|
+
if (value === undefined)
|
|
278
|
+
return "undefined";
|
|
279
|
+
if (Array.isArray(value))
|
|
280
|
+
return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
281
|
+
if (value && typeof value === "object") {
|
|
282
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
283
|
+
}
|
|
284
|
+
return JSON.stringify(value);
|
|
285
|
+
};
|
|
286
|
+
const sameJson = (left, right) => canonicalJson(left) === canonicalJson(right);
|
|
151
287
|
const createResult = (state) => ({
|
|
152
288
|
status: state.status,
|
|
153
289
|
outputText: state.outputText,
|
|
@@ -172,7 +308,11 @@ const createBroadcast = () => {
|
|
|
172
308
|
};
|
|
173
309
|
return {
|
|
174
310
|
publish,
|
|
175
|
-
|
|
311
|
+
close: () => broadcast.close(),
|
|
312
|
+
get isClosed() {
|
|
313
|
+
return broadcast.isClosed;
|
|
314
|
+
},
|
|
315
|
+
stream: (accepts) => broadcast.stream(accepts)
|
|
176
316
|
};
|
|
177
317
|
};
|
|
178
318
|
const resolveApproval = async (options) => {
|
|
@@ -214,344 +354,571 @@ export const streamLiveAgent = (agent, input = {}) => {
|
|
|
214
354
|
const broadcast = createBroadcast();
|
|
215
355
|
let resolveSession;
|
|
216
356
|
let rejectSession;
|
|
357
|
+
let sessionSettled = false;
|
|
217
358
|
const sessionPromise = new Promise((resolve, reject) => {
|
|
218
|
-
resolveSession =
|
|
219
|
-
|
|
359
|
+
resolveSession = (session) => {
|
|
360
|
+
if (sessionSettled)
|
|
361
|
+
return;
|
|
362
|
+
sessionSettled = true;
|
|
363
|
+
resolve(session);
|
|
364
|
+
};
|
|
365
|
+
rejectSession = (error) => {
|
|
366
|
+
if (sessionSettled)
|
|
367
|
+
return;
|
|
368
|
+
sessionSettled = true;
|
|
369
|
+
reject(error);
|
|
370
|
+
};
|
|
220
371
|
});
|
|
372
|
+
void sessionPromise.catch(() => undefined);
|
|
373
|
+
const lifetime = createLifetimeAbort(input);
|
|
221
374
|
const runner = (async () => {
|
|
222
375
|
const runId = input.runId ?? createSecureId("run");
|
|
223
376
|
const metadata = cloneMetadata(agent.metadata, input.metadata);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
runId,
|
|
227
|
-
agentId: agent.id,
|
|
228
|
-
metadata
|
|
229
|
-
})
|
|
230
|
-
: [];
|
|
231
|
-
let messages = normalizeMessages({
|
|
232
|
-
prompt: input.prompt,
|
|
233
|
-
messages: input.messages,
|
|
234
|
-
system: joinInstructions(agent.instructions, input.system)
|
|
235
|
-
});
|
|
236
|
-
messages = injectContextMessages(messages, memoryMessages);
|
|
237
|
-
const state = createBaseState(agent.model.provider, agent.model.modelId, messages, metadata, agent.id, runId);
|
|
238
|
-
await emitRunStartTelemetry(agent, state, memoryMessages);
|
|
239
|
-
await broadcast.publish({
|
|
240
|
-
done: false,
|
|
241
|
-
value: {
|
|
242
|
-
type: "agent-run-start",
|
|
243
|
-
currentStep: 1,
|
|
244
|
-
maxSteps: 1
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
const inputGuardrail = await runGuardrails(agent, state, "input", agent.inputGuardrails, () => ({
|
|
248
|
-
runId: state.runId,
|
|
249
|
-
agentId: state.agentId,
|
|
250
|
-
state: cloneState(state),
|
|
251
|
-
messages,
|
|
252
|
-
metadata: state.metadata
|
|
253
|
-
}));
|
|
254
|
-
if (inputGuardrail) {
|
|
255
|
-
const failedState = applyGuardrailFailure(state, "input", inputGuardrail);
|
|
256
|
-
await persistState(agent, failedState);
|
|
257
|
-
await emitRunFinishTelemetry(agent, failedState);
|
|
258
|
-
const error = new GuardrailTriggeredError("input", failedState.error?.message ?? "Agent input guardrail triggered.", {
|
|
259
|
-
metadata: inputGuardrail.metadata
|
|
260
|
-
});
|
|
261
|
-
await broadcast.publish({ done: false, value: { type: "error", error } });
|
|
262
|
-
await broadcast.publish({
|
|
263
|
-
done: false,
|
|
264
|
-
value: {
|
|
265
|
-
type: "agent-run-finish",
|
|
266
|
-
status: failedState.status,
|
|
267
|
-
state: failedState
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
await broadcast.publish({ done: true, value: undefined });
|
|
271
|
-
rejectSession(error);
|
|
272
|
-
return createResult(failedState);
|
|
273
|
-
}
|
|
274
|
-
const resolvedTools = {
|
|
275
|
-
...(toToolSet(agent.tools) ?? {}),
|
|
276
|
-
...(toToolSet(input.tools) ?? {})
|
|
277
|
-
};
|
|
278
|
-
const realtimeConfig = {
|
|
279
|
-
autoResponse: true,
|
|
280
|
-
...input.realtime,
|
|
281
|
-
instructions: joinInstructions(agent.instructions, input.system, input.realtime?.instructions),
|
|
282
|
-
tools: Object.keys(resolvedTools).length ? resolvedTools : undefined,
|
|
283
|
-
toolChoice: input.toolChoice ?? input.realtime?.toolChoice ?? agent.toolChoice,
|
|
284
|
-
providerOptions: {
|
|
285
|
-
...(agent.providerOptions ?? {}),
|
|
286
|
-
...(input.providerOptions ?? {}),
|
|
287
|
-
...(input.realtime?.providerOptions ?? {})
|
|
288
|
-
}
|
|
289
|
-
};
|
|
377
|
+
let resolvedTools = {};
|
|
378
|
+
let state;
|
|
290
379
|
let session;
|
|
291
|
-
let
|
|
292
|
-
let
|
|
380
|
+
let closePromise;
|
|
381
|
+
let runError;
|
|
382
|
+
let failureCleanupRan = false;
|
|
383
|
+
let transcript = [];
|
|
293
384
|
const toolResults = [];
|
|
294
385
|
const assistantBuffer = [];
|
|
386
|
+
const outputTranscriptBuffer = [];
|
|
295
387
|
let finalText = "";
|
|
388
|
+
let sessionError;
|
|
389
|
+
const processedToolCalls = new Map();
|
|
390
|
+
const closeSession = async () => {
|
|
391
|
+
if (!session)
|
|
392
|
+
return;
|
|
393
|
+
closePromise ??= Promise.resolve().then(() => session.close());
|
|
394
|
+
await closePromise;
|
|
395
|
+
};
|
|
396
|
+
const abortSession = () => {
|
|
397
|
+
void closeSession().catch(() => undefined);
|
|
398
|
+
};
|
|
399
|
+
lifetime.signal.addEventListener("abort", abortSession, { once: true });
|
|
400
|
+
const publish = async (event) => {
|
|
401
|
+
if (!broadcast.isClosed) {
|
|
402
|
+
const terminal = event.type === "agent-run-finish" || event.type === "error" || event.type === "realtime-end";
|
|
403
|
+
const operation = broadcast.publish({ done: false, value: event });
|
|
404
|
+
if (terminal)
|
|
405
|
+
await operation;
|
|
406
|
+
else
|
|
407
|
+
await raceWithAbort(operation, lifetime.signal);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
const rejectNoSession = (error) => {
|
|
411
|
+
rejectSession(error instanceof Error ? error : new Error(String(error)));
|
|
412
|
+
};
|
|
296
413
|
try {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
414
|
+
resolvedTools = {
|
|
415
|
+
...(toToolSet(agent.tools) ?? {}),
|
|
416
|
+
...(toToolSet(input.tools) ?? {})
|
|
417
|
+
};
|
|
418
|
+
ensureDurableConfiguration(agent, input, resolvedTools);
|
|
419
|
+
const baseMessages = normalizeMessages({
|
|
420
|
+
prompt: input.prompt,
|
|
421
|
+
messages: input.messages,
|
|
422
|
+
system: joinInstructions(agent.instructions, input.system)
|
|
423
|
+
});
|
|
424
|
+
state = createBaseState(agent.model.provider, agent.model.modelId, baseMessages, metadata, agent.id, runId, input.scope, input.idempotencyKey);
|
|
425
|
+
if (agent.store) {
|
|
426
|
+
let existing = input.idempotencyKey
|
|
427
|
+
? undefined
|
|
428
|
+
: await raceWithAbort(Promise.resolve(agent.store.load(runId, input.scope)), lifetime.signal);
|
|
429
|
+
if (input.idempotencyKey) {
|
|
430
|
+
const claim = await raceWithAbort(Promise.resolve(agent.store.claimIdempotencyKey(state)), lifetime.signal);
|
|
431
|
+
if (!claim.claimed)
|
|
432
|
+
existing = normalizeAgentRunState(claim.state);
|
|
433
|
+
else
|
|
434
|
+
state = normalizeAgentRunState(claim.state);
|
|
435
|
+
}
|
|
436
|
+
if (existing) {
|
|
437
|
+
const normalized = normalizeAgentRunState(existing);
|
|
438
|
+
const terminalStatuses = ["completed", "failed", "cancelled", "timed_out"];
|
|
439
|
+
if (!terminalStatuses.includes(normalized.status)) {
|
|
440
|
+
throw new ConflictError(`Live agent run "${normalized.runId}" is already ${normalized.status}.`);
|
|
307
441
|
}
|
|
442
|
+
state = normalized;
|
|
443
|
+
await emitRunStartTelemetry(agent, state, [], lifetime.signal);
|
|
444
|
+
await publish({ type: "agent-run-start", currentStep: 1, maxSteps: 1 });
|
|
445
|
+
await emitRunFinishTelemetry(agent, state, lifetime.signal);
|
|
446
|
+
await publish({ type: "agent-run-finish", status: state.status, state: cloneState(state) });
|
|
447
|
+
rejectNoSession(new ValidationError(`Live agent run "${state.runId}" was replayed from durable state and has no active realtime session.`));
|
|
448
|
+
return createResult(state);
|
|
449
|
+
}
|
|
450
|
+
if (!input.idempotencyKey) {
|
|
451
|
+
await checkpointState(agent, state, lifetime.signal);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const memoryMessages = agent.memory
|
|
455
|
+
? await raceWithAbort(Promise.resolve(agent.memory.load({
|
|
456
|
+
runId,
|
|
457
|
+
agentId: agent.id,
|
|
458
|
+
scope: input.scope,
|
|
459
|
+
metadata
|
|
460
|
+
})), lifetime.signal)
|
|
461
|
+
: [];
|
|
462
|
+
const messages = injectContextMessages(baseMessages, memoryMessages);
|
|
463
|
+
transcript = [...messages];
|
|
464
|
+
state.messages = messages;
|
|
465
|
+
await emitRunStartTelemetry(agent, state, memoryMessages, lifetime.signal);
|
|
466
|
+
await persistState(agent, state, lifetime.signal);
|
|
467
|
+
await publish({ type: "agent-run-start", currentStep: 1, maxSteps: 1 });
|
|
468
|
+
const inputGuardrail = await runGuardrails(agent, state, "input", agent.inputGuardrails, () => ({
|
|
469
|
+
runId: state.runId,
|
|
470
|
+
agentId: state.agentId,
|
|
471
|
+
state: cloneState(state),
|
|
472
|
+
messages,
|
|
473
|
+
metadata: state.metadata
|
|
474
|
+
}), lifetime.signal);
|
|
475
|
+
if (inputGuardrail) {
|
|
476
|
+
const failedState = applyGuardrailFailure(state, "input", inputGuardrail);
|
|
477
|
+
state = failedState;
|
|
478
|
+
await persistState(agent, failedState, lifetime.signal);
|
|
479
|
+
await emitRunFinishTelemetry(agent, failedState, lifetime.signal);
|
|
480
|
+
const error = new GuardrailTriggeredError("input", failedState.error?.message ?? "Agent input guardrail triggered.", { metadata: inputGuardrail.metadata });
|
|
481
|
+
await publish({ type: "error", error });
|
|
482
|
+
await publish({ type: "agent-run-finish", status: failedState.status, state: cloneState(failedState) });
|
|
483
|
+
rejectNoSession(error);
|
|
484
|
+
return createResult(failedState);
|
|
485
|
+
}
|
|
486
|
+
const explicitMessages = input.messages ?? [];
|
|
487
|
+
let activeUserIndex = -1;
|
|
488
|
+
explicitMessages.forEach((message, index) => {
|
|
489
|
+
if (message.role === "user")
|
|
490
|
+
activeUserIndex = index;
|
|
491
|
+
});
|
|
492
|
+
const priorConversation = explicitMessages.filter((_, index) => index !== activeUserIndex);
|
|
493
|
+
const realtimeConfig = {
|
|
494
|
+
autoResponse: true,
|
|
495
|
+
...input.realtime,
|
|
496
|
+
instructions: joinInstructions(agent.instructions, input.system, contextInstructions([...memoryMessages, ...priorConversation]), input.realtime?.instructions),
|
|
497
|
+
tools: Object.keys(resolvedTools).length ? resolvedTools : undefined,
|
|
498
|
+
toolChoice: input.toolChoice ?? input.realtime?.toolChoice ?? agent.toolChoice,
|
|
499
|
+
providerOptions: {
|
|
500
|
+
...(agent.providerOptions ?? {}),
|
|
501
|
+
...(input.providerOptions ?? {}),
|
|
502
|
+
...(input.realtime?.providerOptions ?? {})
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
if (lifetime.signal.aborted)
|
|
506
|
+
throw abortError(lifetime.signal);
|
|
507
|
+
const connectPromise = agent.model.connect(realtimeConfig, {
|
|
508
|
+
...input.connectOptions,
|
|
509
|
+
timeoutMs: input.connectOptions?.timeoutMs ?? input.timeoutMs,
|
|
510
|
+
signal: lifetime.signal
|
|
511
|
+
});
|
|
512
|
+
void connectPromise.then((lateSession) => {
|
|
513
|
+
if (lifetime.signal.aborted && lateSession !== session) {
|
|
514
|
+
void lateSession.close().catch(() => undefined);
|
|
515
|
+
}
|
|
516
|
+
}, () => undefined);
|
|
517
|
+
session = await raceWithAbort(connectPromise, lifetime.signal);
|
|
518
|
+
resolveSession(session);
|
|
519
|
+
if (activeUserIndex >= 0) {
|
|
520
|
+
const text = textFromMessage(explicitMessages[activeUserIndex]);
|
|
521
|
+
if (text) {
|
|
522
|
+
if (lifetime.signal.aborted)
|
|
523
|
+
throw abortError(lifetime.signal);
|
|
524
|
+
await raceWithAbort(session.sendText(text), lifetime.signal);
|
|
308
525
|
}
|
|
309
526
|
}
|
|
310
527
|
else if (input.prompt) {
|
|
311
|
-
|
|
528
|
+
if (lifetime.signal.aborted)
|
|
529
|
+
throw abortError(lifetime.signal);
|
|
530
|
+
await raceWithAbort(session.sendText(input.prompt), lifetime.signal);
|
|
312
531
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
532
|
+
const recordToolResult = async (result, fingerprint) => {
|
|
533
|
+
processedToolCalls.set(result.toolCallId, { fingerprint, result });
|
|
534
|
+
toolResults.push(result);
|
|
535
|
+
transcript.push({ role: "tool", parts: [toolResultPart(result)] });
|
|
536
|
+
state.messages = transcript;
|
|
537
|
+
state.toolResults = toolResults;
|
|
538
|
+
await persistState(agent, state, lifetime.signal);
|
|
539
|
+
if (lifetime.signal.aborted)
|
|
540
|
+
throw abortError(lifetime.signal);
|
|
541
|
+
await raceWithAbort(session.sendToolResult(result), lifetime.signal);
|
|
542
|
+
};
|
|
543
|
+
const executeTool = async (definition, call, parsedInput, serializedInput) => {
|
|
544
|
+
const idempotencyKey = `${input.idempotencyKey ?? state.runId}:${call.id}`;
|
|
545
|
+
let journalClaim;
|
|
546
|
+
if (agent.store) {
|
|
547
|
+
const candidate = {
|
|
548
|
+
runId: state.runId,
|
|
549
|
+
scope: state.scope,
|
|
550
|
+
toolCallId: call.id,
|
|
551
|
+
toolName: call.name,
|
|
552
|
+
status: "pending",
|
|
553
|
+
idempotencyKey,
|
|
554
|
+
revision: 0,
|
|
555
|
+
input: serializedInput,
|
|
556
|
+
updatedAt: Date.now()
|
|
557
|
+
};
|
|
558
|
+
if (lifetime.signal.aborted)
|
|
559
|
+
throw abortError(lifetime.signal);
|
|
560
|
+
const claim = await raceWithAbort(Promise.resolve(agent.store.claimToolExecution(candidate)), lifetime.signal);
|
|
561
|
+
if (!claim.claimed) {
|
|
562
|
+
if (claim.entry.toolName !== call.name || !sameJson(claim.entry.input, serializedInput)) {
|
|
563
|
+
throw new ConflictError(`Realtime tool call id "${call.id}" was reused with a different payload.`);
|
|
564
|
+
}
|
|
565
|
+
if (claim.entry.status === "completed") {
|
|
566
|
+
return {
|
|
567
|
+
toolCallId: call.id,
|
|
568
|
+
toolName: call.name,
|
|
569
|
+
output: claim.entry.output ?? null,
|
|
570
|
+
isError: false
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
if (claim.entry.status === "failed") {
|
|
574
|
+
return {
|
|
575
|
+
toolCallId: call.id,
|
|
576
|
+
toolName: call.name,
|
|
577
|
+
error: { message: claim.entry.error?.message ?? `Tool "${call.name}" previously failed.` },
|
|
578
|
+
isError: true
|
|
579
|
+
};
|
|
322
580
|
}
|
|
323
|
-
|
|
324
|
-
|
|
581
|
+
throw new ConflictError(`Tool "${call.name}" has an indeterminate durable execution. Reconcile idempotency key "${claim.entry.idempotencyKey}" before retrying.`);
|
|
582
|
+
}
|
|
583
|
+
journalClaim = claim.entry;
|
|
325
584
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
585
|
+
try {
|
|
586
|
+
const output = serializeJsonValue(await withToolTimeout(async (abortSignal) => definition.execute(parsedInput, {
|
|
587
|
+
abortSignal,
|
|
588
|
+
toolCall: call,
|
|
589
|
+
step: 1,
|
|
590
|
+
model: agent.model,
|
|
591
|
+
realtimeConfig,
|
|
592
|
+
runId: state.runId,
|
|
593
|
+
agentId: state.agentId,
|
|
594
|
+
scope: state.scope,
|
|
595
|
+
metadata: state.metadata,
|
|
596
|
+
idempotencyKey
|
|
597
|
+
}), (input.toolExecution ?? agent.toolExecution)?.timeoutMs, lifetime.signal));
|
|
598
|
+
if (journalClaim) {
|
|
599
|
+
if (lifetime.signal.aborted)
|
|
600
|
+
throw abortError(lifetime.signal);
|
|
601
|
+
await raceWithAbort(Promise.resolve(agent.store.completeToolExecution({
|
|
602
|
+
...journalClaim,
|
|
603
|
+
status: "completed",
|
|
604
|
+
output,
|
|
605
|
+
completedAt: Date.now(),
|
|
606
|
+
updatedAt: Date.now()
|
|
607
|
+
}, { expectedRevision: journalClaim.revision })), lifetime.signal);
|
|
329
608
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
609
|
+
return { toolCallId: call.id, toolName: call.name, output, isError: false };
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
613
|
+
if (lifetime.signal.aborted || normalizedError instanceof ToolExecutionTimeoutError) {
|
|
614
|
+
throw normalizedError;
|
|
615
|
+
}
|
|
616
|
+
if (journalClaim) {
|
|
617
|
+
try {
|
|
618
|
+
if (lifetime.signal.aborted)
|
|
619
|
+
throw abortError(lifetime.signal);
|
|
620
|
+
await raceWithAbort(Promise.resolve(agent.store.completeToolExecution({
|
|
621
|
+
...journalClaim,
|
|
622
|
+
status: "failed",
|
|
623
|
+
error: { message: normalizedError.message },
|
|
624
|
+
completedAt: Date.now(),
|
|
625
|
+
updatedAt: Date.now()
|
|
626
|
+
}, { expectedRevision: journalClaim.revision })), lifetime.signal);
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
// Preserve the original tool error. A running journal entry blocks unsafe replay.
|
|
336
630
|
}
|
|
337
631
|
}
|
|
338
|
-
|
|
632
|
+
return {
|
|
633
|
+
toolCallId: call.id,
|
|
634
|
+
toolName: call.name,
|
|
635
|
+
error: { message: normalizedError.message },
|
|
636
|
+
isError: true
|
|
637
|
+
};
|
|
339
638
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
await
|
|
639
|
+
};
|
|
640
|
+
let waitingForPostToolResponse = false;
|
|
641
|
+
let postToolResponseObserved = false;
|
|
642
|
+
let finalOutputTranscriptObserved = false;
|
|
643
|
+
let terminalResponseCompletionObserved = false;
|
|
644
|
+
const requiresFinalOutputTranscript = Boolean(realtimeConfig.outputAudioTranscription);
|
|
645
|
+
const iterator = session.eventStream()[Symbol.asyncIterator]();
|
|
646
|
+
let iteratorCompleted = false;
|
|
647
|
+
try {
|
|
648
|
+
while (true) {
|
|
649
|
+
const next = await nextWithAbort(iterator, lifetime.signal);
|
|
650
|
+
if (next.done) {
|
|
651
|
+
iteratorCompleted = true;
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
const event = next.value;
|
|
655
|
+
await publish(event);
|
|
656
|
+
if (event.type === "realtime-text-delta") {
|
|
657
|
+
assistantBuffer.push(event.textDelta);
|
|
658
|
+
if (waitingForPostToolResponse)
|
|
659
|
+
postToolResponseObserved = true;
|
|
660
|
+
await publish({ type: "text-delta", textDelta: event.textDelta });
|
|
362
661
|
continue;
|
|
363
662
|
}
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
663
|
+
if (event.type === "realtime-transcript") {
|
|
664
|
+
if (event.role === "user" && event.isFinal && event.text) {
|
|
665
|
+
transcript.push(createTextMessage("user", event.text));
|
|
666
|
+
}
|
|
667
|
+
if (event.role === "assistant") {
|
|
668
|
+
if (event.text && waitingForPostToolResponse)
|
|
669
|
+
postToolResponseObserved = true;
|
|
670
|
+
if (event.isFinal) {
|
|
671
|
+
finalOutputTranscriptObserved = true;
|
|
672
|
+
const bufferedTranscript = outputTranscriptBuffer.join("");
|
|
673
|
+
finalText = event.text.startsWith(bufferedTranscript)
|
|
674
|
+
? event.text
|
|
675
|
+
: `${bufferedTranscript}${event.text}`;
|
|
676
|
+
if (finalText)
|
|
677
|
+
transcript.push(createTextMessage("assistant", finalText));
|
|
678
|
+
assistantBuffer.length = 0;
|
|
679
|
+
outputTranscriptBuffer.length = 0;
|
|
680
|
+
}
|
|
681
|
+
else if (requiresFinalOutputTranscript && event.text) {
|
|
682
|
+
outputTranscriptBuffer.push(event.text);
|
|
683
|
+
}
|
|
684
|
+
if (requiresFinalOutputTranscript &&
|
|
685
|
+
terminalResponseCompletionObserved &&
|
|
686
|
+
finalOutputTranscriptObserved &&
|
|
687
|
+
(!waitingForPostToolResponse || postToolResponseObserved)) {
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
377
691
|
continue;
|
|
378
692
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
693
|
+
if (event.type === "realtime-tool-call") {
|
|
694
|
+
const serializedCallInput = serializeJsonValue(event.toolCall.input);
|
|
695
|
+
const fingerprint = `${event.toolCall.name}:${canonicalJson(serializedCallInput)}`;
|
|
696
|
+
const previous = processedToolCalls.get(event.toolCall.id);
|
|
697
|
+
if (previous) {
|
|
698
|
+
if (previous.fingerprint !== fingerprint) {
|
|
699
|
+
throw new ConflictError(`Realtime tool call id "${event.toolCall.id}" was reused with a different payload.`);
|
|
700
|
+
}
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
await publish({ type: "tool-call", toolCall: event.toolCall });
|
|
704
|
+
waitingForPostToolResponse = true;
|
|
705
|
+
postToolResponseObserved = false;
|
|
706
|
+
finalOutputTranscriptObserved = false;
|
|
707
|
+
terminalResponseCompletionObserved = false;
|
|
708
|
+
const definition = resolvedTools[event.toolCall.name];
|
|
709
|
+
if (!definition) {
|
|
710
|
+
const result = {
|
|
711
|
+
toolCallId: event.toolCall.id,
|
|
712
|
+
toolName: event.toolCall.name,
|
|
713
|
+
error: { message: `Tool "${event.toolCall.name}" is not registered.` },
|
|
714
|
+
isError: true
|
|
715
|
+
};
|
|
716
|
+
await recordToolResult(result, fingerprint);
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (!isCallableToolDefinition(definition)) {
|
|
720
|
+
const result = {
|
|
721
|
+
toolCallId: event.toolCall.id,
|
|
722
|
+
toolName: event.toolCall.name,
|
|
723
|
+
error: { message: `Tool "${event.toolCall.name}" is provider-hosted and cannot be executed locally.` },
|
|
724
|
+
isError: true
|
|
725
|
+
};
|
|
726
|
+
await recordToolResult(result, fingerprint);
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
const parsed = definition.schema.safeParse(event.toolCall.input);
|
|
730
|
+
if (!parsed.success) {
|
|
731
|
+
const result = {
|
|
732
|
+
toolCallId: event.toolCall.id,
|
|
733
|
+
toolName: event.toolCall.name,
|
|
734
|
+
error: { message: `Invalid input for tool "${event.toolCall.name}": ${parsed.error.message}` },
|
|
735
|
+
isError: true
|
|
736
|
+
};
|
|
737
|
+
await recordToolResult(result, fingerprint);
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const approval = await raceWithAbort(resolveApproval({
|
|
741
|
+
agent,
|
|
742
|
+
input,
|
|
743
|
+
state,
|
|
744
|
+
call: event.toolCall,
|
|
745
|
+
parsedInput: serializeJsonValue(parsed.data),
|
|
746
|
+
tool: definition,
|
|
747
|
+
realtimeConfig
|
|
748
|
+
}), lifetime.signal);
|
|
749
|
+
if (approval.approvalRequired) {
|
|
750
|
+
throw new ValidationError(`Tool "${event.toolCall.name}" requested resumable approval, but streamLiveAgent only supports immediate approval decisions.`);
|
|
751
|
+
}
|
|
752
|
+
if (!approval.approved) {
|
|
753
|
+
const result = {
|
|
754
|
+
toolCallId: event.toolCall.id,
|
|
755
|
+
toolName: event.toolCall.name,
|
|
756
|
+
error: {
|
|
757
|
+
message: approval.reason ?? `Tool "${event.toolCall.name}" was denied by the approval policy.`
|
|
758
|
+
},
|
|
759
|
+
isError: true
|
|
760
|
+
};
|
|
761
|
+
await recordToolResult(result, fingerprint);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const result = await executeTool(definition, event.toolCall, parsed.data, serializeJsonValue(parsed.data));
|
|
765
|
+
await recordToolResult(result, fingerprint);
|
|
393
766
|
continue;
|
|
394
767
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
input,
|
|
398
|
-
state,
|
|
399
|
-
call: event.toolCall,
|
|
400
|
-
parsedInput: serializeJsonValue(parsed.data),
|
|
401
|
-
tool: definition,
|
|
402
|
-
realtimeConfig
|
|
403
|
-
});
|
|
404
|
-
if (!approval.approved) {
|
|
405
|
-
const result = {
|
|
406
|
-
toolCallId: event.toolCall.id,
|
|
407
|
-
toolName: event.toolCall.name,
|
|
408
|
-
error: {
|
|
409
|
-
message: approval.reason ?? `Tool "${event.toolCall.name}" was denied by the approval policy.`
|
|
410
|
-
},
|
|
411
|
-
isError: true
|
|
412
|
-
};
|
|
413
|
-
toolResults.push(result);
|
|
414
|
-
transcript.push({
|
|
415
|
-
role: "tool",
|
|
416
|
-
parts: [toolResultPart(result)]
|
|
417
|
-
});
|
|
418
|
-
await session.sendToolResult(result);
|
|
768
|
+
if (event.type === "realtime-error") {
|
|
769
|
+
sessionError = event.error ?? new Error(event.message ?? "Realtime session failed.");
|
|
419
770
|
continue;
|
|
420
771
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
abortSignal,
|
|
424
|
-
toolCall: event.toolCall,
|
|
425
|
-
step: 1,
|
|
426
|
-
model: agent.model,
|
|
427
|
-
realtimeConfig
|
|
428
|
-
}), (input.toolExecution ?? agent.toolExecution)?.timeoutMs, input.abortSignal));
|
|
429
|
-
const result = {
|
|
430
|
-
toolCallId: event.toolCall.id,
|
|
431
|
-
toolName: event.toolCall.name,
|
|
432
|
-
output,
|
|
433
|
-
isError: false
|
|
434
|
-
};
|
|
435
|
-
toolResults.push(result);
|
|
436
|
-
transcript.push({
|
|
437
|
-
role: "tool",
|
|
438
|
-
parts: [toolResultPart(result)]
|
|
439
|
-
});
|
|
440
|
-
await session.sendToolResult(result);
|
|
772
|
+
if (event.type === "realtime-end" && event.reason === "error") {
|
|
773
|
+
sessionError ??= new Error(typeof event.providerMetadata?.message === "string" ? event.providerMetadata.message : "Realtime session failed.");
|
|
441
774
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
await session.sendToolResult(result);
|
|
775
|
+
if (event.type === "realtime-response-complete") {
|
|
776
|
+
if (event.reason === "generation-complete")
|
|
777
|
+
continue;
|
|
778
|
+
if (waitingForPostToolResponse && !postToolResponseObserved)
|
|
779
|
+
continue;
|
|
780
|
+
terminalResponseCompletionObserved = true;
|
|
781
|
+
if (requiresFinalOutputTranscript && !finalOutputTranscriptObserved)
|
|
782
|
+
continue;
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
if (event.type === "realtime-end") {
|
|
786
|
+
break;
|
|
455
787
|
}
|
|
456
|
-
continue;
|
|
457
|
-
}
|
|
458
|
-
if (event.type === "realtime-error") {
|
|
459
|
-
sessionError = event.error ?? new Error(event.message ?? "Realtime session failed.");
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
-
if (event.type === "realtime-end" && event.reason === "error") {
|
|
463
|
-
sessionError ??= new Error(typeof event.providerMetadata?.message === "string" ? event.providerMetadata.message : "Realtime session failed.");
|
|
464
788
|
}
|
|
465
|
-
|
|
466
|
-
|
|
789
|
+
}
|
|
790
|
+
finally {
|
|
791
|
+
if (!iteratorCompleted && iterator.return) {
|
|
792
|
+
const returned = Promise.resolve(iterator.return());
|
|
793
|
+
if (lifetime.signal.aborted)
|
|
794
|
+
void returned.catch(() => undefined);
|
|
795
|
+
else
|
|
796
|
+
await raceWithAbort(returned, lifetime.signal);
|
|
467
797
|
}
|
|
468
798
|
}
|
|
469
799
|
if (sessionError) {
|
|
470
800
|
throw sessionError;
|
|
471
801
|
}
|
|
802
|
+
if (waitingForPostToolResponse && !postToolResponseObserved) {
|
|
803
|
+
throw new ValidationError("Realtime session ended before the response following a tool result was observed.");
|
|
804
|
+
}
|
|
805
|
+
if (requiresFinalOutputTranscript &&
|
|
806
|
+
(!terminalResponseCompletionObserved || !finalOutputTranscriptObserved)) {
|
|
807
|
+
throw new ValidationError("Realtime session ended before both response completion and final output transcription were observed.");
|
|
808
|
+
}
|
|
809
|
+
await closeSession();
|
|
472
810
|
if (assistantBuffer.length && !finalText) {
|
|
473
811
|
finalText = assistantBuffer.join("");
|
|
474
812
|
if (finalText) {
|
|
475
813
|
transcript.push(createTextMessage("assistant", finalText));
|
|
476
814
|
}
|
|
477
815
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
816
|
+
else if (outputTranscriptBuffer.length && !finalText) {
|
|
817
|
+
finalText = outputTranscriptBuffer.join("");
|
|
818
|
+
transcript.push(createTextMessage("assistant", finalText));
|
|
819
|
+
}
|
|
820
|
+
const completedState = state;
|
|
821
|
+
completedState.messages = transcript;
|
|
822
|
+
completedState.toolResults = toolResults;
|
|
823
|
+
completedState.outputText = finalText;
|
|
824
|
+
completedState.status = "completed";
|
|
825
|
+
completedState.updatedAt = Date.now();
|
|
826
|
+
completedState.error = undefined;
|
|
827
|
+
const result = createResult(completedState);
|
|
828
|
+
const outputGuardrail = await runGuardrails(agent, completedState, "output", agent.outputGuardrails, () => ({
|
|
829
|
+
runId: completedState.runId,
|
|
830
|
+
agentId: completedState.agentId,
|
|
831
|
+
state: cloneState(completedState),
|
|
489
832
|
output: result,
|
|
490
|
-
metadata:
|
|
491
|
-
}));
|
|
492
|
-
const finalState = outputGuardrail
|
|
833
|
+
metadata: completedState.metadata
|
|
834
|
+
}), lifetime.signal);
|
|
835
|
+
const finalState = outputGuardrail
|
|
836
|
+
? applyGuardrailFailure(completedState, "output", outputGuardrail)
|
|
837
|
+
: completedState;
|
|
493
838
|
if (outputGuardrail) {
|
|
494
|
-
await
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
metadata: outputGuardrail.metadata
|
|
500
|
-
})
|
|
501
|
-
}
|
|
839
|
+
await publish({
|
|
840
|
+
type: "error",
|
|
841
|
+
error: new GuardrailTriggeredError("output", finalState.error?.message ?? "Agent output guardrail triggered.", {
|
|
842
|
+
metadata: outputGuardrail.metadata
|
|
843
|
+
})
|
|
502
844
|
});
|
|
503
845
|
}
|
|
504
|
-
await persistState(agent, finalState);
|
|
505
|
-
await emitRunFinishTelemetry(agent, finalState);
|
|
506
|
-
await
|
|
507
|
-
done: false,
|
|
508
|
-
value: {
|
|
509
|
-
type: "agent-run-finish",
|
|
510
|
-
status: finalState.status,
|
|
511
|
-
state: finalState
|
|
512
|
-
}
|
|
513
|
-
});
|
|
514
|
-
await broadcast.publish({ done: true, value: undefined });
|
|
846
|
+
await persistState(agent, finalState, lifetime.signal);
|
|
847
|
+
await emitRunFinishTelemetry(agent, finalState, lifetime.signal);
|
|
848
|
+
await publish({ type: "agent-run-finish", status: finalState.status, state: cloneState(finalState) });
|
|
515
849
|
return createResult(finalState);
|
|
516
850
|
}
|
|
517
851
|
catch (error) {
|
|
518
|
-
|
|
519
|
-
|
|
852
|
+
runError = error;
|
|
853
|
+
failureCleanupRan = true;
|
|
854
|
+
rejectNoSession(error);
|
|
855
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
856
|
+
if (state) {
|
|
857
|
+
state.messages = transcript.length ? transcript : state.messages;
|
|
858
|
+
state.toolResults = toolResults;
|
|
859
|
+
const status = lifetime.signal.aborted
|
|
860
|
+
? lifetime.signal.reason instanceof LiveAgentTimeoutError
|
|
861
|
+
? "timed_out"
|
|
862
|
+
: "cancelled"
|
|
863
|
+
: "failed";
|
|
864
|
+
state = {
|
|
865
|
+
...state,
|
|
866
|
+
status,
|
|
867
|
+
error: { message: normalizedError.message },
|
|
868
|
+
...(status === "cancelled"
|
|
869
|
+
? { cancelledAt: Date.now(), cancellationReason: normalizedError.message }
|
|
870
|
+
: {}),
|
|
871
|
+
updatedAt: Date.now()
|
|
872
|
+
};
|
|
873
|
+
await runTerminalFailureCleanup([
|
|
874
|
+
(cleanupSignal) => persistState(agent, state, cleanupSignal),
|
|
875
|
+
(cleanupSignal) => emitRunFinishTelemetry(agent, state, cleanupSignal),
|
|
876
|
+
() => closeSession()
|
|
877
|
+
]);
|
|
878
|
+
try {
|
|
879
|
+
await publish({ type: "error", error: normalizedError });
|
|
880
|
+
await publish({ type: "agent-run-finish", status: state.status, state: cloneState(state) });
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
// The original failure remains authoritative if the event broadcast also failed.
|
|
884
|
+
}
|
|
520
885
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
done: false,
|
|
526
|
-
value: {
|
|
527
|
-
type: "error",
|
|
528
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
886
|
+
else {
|
|
887
|
+
await runTerminalFailureCleanup([() => closeSession()]);
|
|
888
|
+
try {
|
|
889
|
+
await publish({ type: "error", error: normalizedError });
|
|
529
890
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
done: false,
|
|
533
|
-
value: {
|
|
534
|
-
type: "agent-run-finish",
|
|
535
|
-
status: failedState.status,
|
|
536
|
-
state: failedState
|
|
891
|
+
catch {
|
|
892
|
+
// The original failure remains authoritative if the event broadcast also failed.
|
|
537
893
|
}
|
|
538
|
-
}
|
|
539
|
-
await broadcast.publish({ done: true, value: undefined });
|
|
894
|
+
}
|
|
540
895
|
throw error;
|
|
541
896
|
}
|
|
542
897
|
finally {
|
|
543
|
-
|
|
544
|
-
|
|
898
|
+
lifetime.signal.removeEventListener("abort", abortSession);
|
|
899
|
+
lifetime.cleanup();
|
|
900
|
+
if (!failureCleanupRan) {
|
|
901
|
+
try {
|
|
902
|
+
await closeSession();
|
|
903
|
+
}
|
|
904
|
+
catch (closeError) {
|
|
905
|
+
if (!runError)
|
|
906
|
+
runError = closeError;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (!sessionSettled) {
|
|
910
|
+
rejectNoSession(runError ?? new ValidationError(`Live agent run "${runId}" ended without a realtime session.`));
|
|
545
911
|
}
|
|
912
|
+
broadcast.close();
|
|
546
913
|
}
|
|
547
914
|
})();
|
|
915
|
+
void runner.catch(() => undefined);
|
|
548
916
|
return {
|
|
549
917
|
eventStream: broadcast.stream(),
|
|
550
918
|
textStream: (async function* () {
|
|
551
|
-
for await (const event of broadcast.stream()) {
|
|
552
|
-
if (event.type === "text-delta")
|
|
919
|
+
for await (const event of broadcast.stream((candidate) => candidate.type === "text-delta")) {
|
|
920
|
+
if (event.type === "text-delta")
|
|
553
921
|
yield event.textDelta;
|
|
554
|
-
}
|
|
555
922
|
}
|
|
556
923
|
})(),
|
|
557
924
|
session: sessionPromise,
|