@zhivex-ai/core 1.2.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.
@@ -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
- await agent.onTelemetryEvent?.(event);
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.store?.save(cloneState(state));
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
- await agent.memory?.save?.({
67
- runId: state.runId,
68
- agentId: state.agentId,
69
- state: cloneState(state),
70
- metadata: state.metadata
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 trigger = await guardrail(requestFactory(index));
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.onTelemetryEvent?.({
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 signal = mergeAbortSignals(abortSignal, controller.signal);
124
- return new Promise((resolve, reject) => {
125
- const timer = setTimeout(() => {
126
- controller.abort();
127
- reject(new Error(`Tool execution timed out after ${timeoutMs}ms.`));
128
- }, timeoutMs);
129
- Promise.resolve()
130
- .then(() => operation(signal))
131
- .then((value) => {
132
- clearTimeout(timer);
133
- resolve(value);
134
- })
135
- .catch((error) => {
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
- reject(error);
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
- stream: () => broadcast.stream()
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 = resolve;
219
- rejectSession = reject;
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
- const memoryMessages = agent.memory
225
- ? await agent.memory.load({
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 sessionError;
292
- let transcript = [...messages];
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
- session = await agent.model.connect(realtimeConfig, input.connectOptions);
298
- resolveSession(session);
299
- if (input.messages) {
300
- for (const message of input.messages) {
301
- if (message.role !== "user") {
302
- continue;
303
- }
304
- const text = textFromMessage(message);
305
- if (text) {
306
- await session.sendText(text);
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
- await session.sendText(input.prompt);
528
+ if (lifetime.signal.aborted)
529
+ throw abortError(lifetime.signal);
530
+ await raceWithAbort(session.sendText(input.prompt), lifetime.signal);
312
531
  }
313
- for await (const event of session.eventStream()) {
314
- await broadcast.publish({ done: false, value: event });
315
- if (event.type === "realtime-text-delta") {
316
- assistantBuffer.push(event.textDelta);
317
- await broadcast.publish({
318
- done: false,
319
- value: {
320
- type: "text-delta",
321
- textDelta: event.textDelta
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
- continue;
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
- if (event.type === "realtime-transcript") {
327
- if (event.role === "user" && event.isFinal && event.text) {
328
- transcript.push(createTextMessage("user", event.text));
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
- if (event.role === "assistant" && event.isFinal) {
331
- const text = event.text || assistantBuffer.join("");
332
- if (text) {
333
- finalText = text;
334
- transcript.push(createTextMessage("assistant", text));
335
- assistantBuffer.length = 0;
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
- continue;
632
+ return {
633
+ toolCallId: call.id,
634
+ toolName: call.name,
635
+ error: { message: normalizedError.message },
636
+ isError: true
637
+ };
339
638
  }
340
- if (event.type === "realtime-tool-call") {
341
- await broadcast.publish({
342
- done: false,
343
- value: {
344
- type: "tool-call",
345
- toolCall: event.toolCall
346
- }
347
- });
348
- const definition = resolvedTools[event.toolCall.name];
349
- if (!definition) {
350
- const result = {
351
- toolCallId: event.toolCall.id,
352
- toolName: event.toolCall.name,
353
- error: { message: `Tool "${event.toolCall.name}" is not registered.` },
354
- isError: true
355
- };
356
- toolResults.push(result);
357
- transcript.push({
358
- role: "tool",
359
- parts: [toolResultPart(result)]
360
- });
361
- await session.sendToolResult(result);
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 (!isCallableToolDefinition(definition)) {
365
- const result = {
366
- toolCallId: event.toolCall.id,
367
- toolName: event.toolCall.name,
368
- error: { message: `Tool "${event.toolCall.name}" is provider-hosted and cannot be executed locally.` },
369
- isError: true
370
- };
371
- toolResults.push(result);
372
- transcript.push({
373
- role: "tool",
374
- parts: [toolResultPart(result)]
375
- });
376
- await session.sendToolResult(result);
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
- const parsed = definition.schema.safeParse(event.toolCall.input);
380
- if (!parsed.success) {
381
- const result = {
382
- toolCallId: event.toolCall.id,
383
- toolName: event.toolCall.name,
384
- error: { message: `Invalid input for tool "${event.toolCall.name}": ${parsed.error.message}` },
385
- isError: true
386
- };
387
- toolResults.push(result);
388
- transcript.push({
389
- role: "tool",
390
- parts: [toolResultPart(result)]
391
- });
392
- await session.sendToolResult(result);
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
- const approval = await resolveApproval({
396
- agent,
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
- try {
422
- const output = serializeJsonValue(await withToolTimeout(async (abortSignal) => definition.execute(parsed.data, {
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
- catch (error) {
443
- const result = {
444
- toolCallId: event.toolCall.id,
445
- toolName: event.toolCall.name,
446
- error: { message: error instanceof Error ? error.message : "Tool execution failed." },
447
- isError: true
448
- };
449
- toolResults.push(result);
450
- transcript.push({
451
- role: "tool",
452
- parts: [toolResultPart(result)]
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
- if (event.type === "realtime-response-complete" || event.type === "realtime-end") {
466
- break;
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
- state.messages = transcript;
479
- state.toolResults = toolResults;
480
- state.outputText = finalText;
481
- state.status = "completed";
482
- state.updatedAt = Date.now();
483
- state.error = undefined;
484
- const result = createResult(state);
485
- const outputGuardrail = await runGuardrails(agent, state, "output", agent.outputGuardrails, () => ({
486
- runId: state.runId,
487
- agentId: state.agentId,
488
- state: cloneState(state),
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: state.metadata
491
- }));
492
- const finalState = outputGuardrail ? applyGuardrailFailure(state, "output", outputGuardrail) : state;
833
+ metadata: completedState.metadata
834
+ }), lifetime.signal);
835
+ const finalState = outputGuardrail
836
+ ? applyGuardrailFailure(completedState, "output", outputGuardrail)
837
+ : completedState;
493
838
  if (outputGuardrail) {
494
- await broadcast.publish({
495
- done: false,
496
- value: {
497
- type: "error",
498
- error: new GuardrailTriggeredError("output", finalState.error?.message ?? "Agent output guardrail triggered.", {
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 broadcast.publish({
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
- if (!session) {
519
- rejectSession(error);
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
- const failedState = createFailedState(state, error instanceof Error ? error.message : String(error));
522
- await persistState(agent, failedState);
523
- await emitRunFinishTelemetry(agent, failedState);
524
- await broadcast.publish({
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
- await broadcast.publish({
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
- if (session) {
544
- await session.close();
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,