@meshagent/meshagent-agents 0.43.2 → 0.44.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,16 +1,41 @@
1
- import { Completer, RoomMessageEvent, } from "@meshagent/meshagent";
1
+ import { Completer, } from "@meshagent/meshagent";
2
2
  import { v4 as uuidv4 } from "uuid";
3
- import { AgentMessage as AgentMessageFactory, AgentRealtimeAudioChunk, AgentRealtimeAudioCommit, ChangeModel, CloseThread, DeleteThread, ModelsRequest, OpenThread, RenameThread, StartThread, AgentConnectionStatus, TurnInterrupt, TurnStart, TurnSteer, AgentClientToolCallResponse, agentInputContent, agentRoomMessageType, agentThreadClearedType, agentThreadLoadedType, agentConnectionStatusType, agentThreadStartRejectedType, agentThreadStartedType, agentTurnEndedType, agentTurnStartAcceptedType, agentTurnStartRejectedType, agentTurnStartType, agentTurnStartedType, agentTurnSteerAcceptedType, agentTurnSteerRejectedType, agentTurnSteerType, agentTurnSteeredType, } from "./agent-messages.js";
3
+ import { AgentMessage as AgentMessageFactory, AgentAudioGenerationDelta, AgentAudioTranscriptionDelta, AgentRealtimeAudioChunk, AgentRealtimeAudioCommit, AgentFileContentDelta, AgentModelChanged, AgentReasoningContentDelta, AgentTextContentDelta, AgentThreadStatus, AgentToolCallArgumentsDelta, AgentToolCallLogDelta, ChangeModel, CloseThread, DeleteThread, ModelsResponse, ModelsRequest, OpenThread, RenameThread, StartThread, AgentConnectionStatus, TurnInterrupt, TurnStart, TurnSteer, AgentClientToolCallRequested, AgentClientToolCallResponse, agentInputContent, agentAudioGenerationDeltaType, agentAudioTranscriptionDeltaType, agentFileContentDeltaType, agentImageGenerationCompletedType, agentImageGenerationPartialType, agentModelChangedType, agentModelsResponseType, agentReasoningContentDeltaType, agentRoomMessageType, agentTextContentDeltaType, agentThreadClearedType, agentThreadCreatedType, agentThreadDeletedType, agentThreadLoadedType, agentThreadListedType, agentThreadUpdatedType, agentConnectionStatusType, agentThreadStatusType, agentThreadStartType, agentThreadStartRejectedType, agentThreadStartedType, agentToolCallArgumentsDeltaType, agentToolCallLogDeltaType, agentToolCallEndedType, agentToolCallInProgressType, agentToolCallPendingType, agentToolCallStartedType, agentSecretRequestedType, agentTurnEndedType, agentTurnInterruptAcceptedType, agentTurnInterruptedType, agentTurnStartAcceptedType, agentTurnStartRejectedType, agentTurnStartType, agentTurnStartedType, agentTurnSteerAcceptedType, agentTurnSteerRejectedType, agentTurnSteerType, agentTurnSteeredType, agentUsageUpdatedType, } from "./agent-messages.js";
4
4
  import { ChangeEmitter } from "./change-emitter.js";
5
5
  export class AgentMessageEvent {
6
- constructor({ message, attachment }) {
6
+ constructor({ message, createdAt, attachment }) {
7
7
  this.message = message;
8
+ this.createdAt = createdAt ?? dateOrNull(message.createdAt) ?? new Date();
8
9
  this.attachment = attachment;
9
10
  }
10
11
  get payload() {
11
- return this.message.toJson();
12
+ return { ...this.message.toJson(), created_at: this.createdAt.toISOString() };
12
13
  }
13
14
  }
15
+ function agentRoomMessageFromEvent(event) {
16
+ const candidate = event.message;
17
+ if (candidate == null || typeof candidate !== "object" || Array.isArray(candidate)) {
18
+ return undefined;
19
+ }
20
+ const roomMessage = candidate;
21
+ if (roomMessage.type !== agentRoomMessageType) {
22
+ return undefined;
23
+ }
24
+ if (roomMessage.message == null || typeof roomMessage.message !== "object" || Array.isArray(roomMessage.message)) {
25
+ return undefined;
26
+ }
27
+ const rawMessage = roomMessage.message;
28
+ const payload = typeof rawMessage.type === "string" ? rawMessage : rawMessage.payload;
29
+ if (payload == null || typeof payload !== "object" || Array.isArray(payload)) {
30
+ return undefined;
31
+ }
32
+ return {
33
+ fromParticipantId: typeof roomMessage.fromParticipantId === "string" ? roomMessage.fromParticipantId : undefined,
34
+ message: payload,
35
+ local: roomMessage.local === true,
36
+ attachment: roomMessage.attachment instanceof Uint8Array ? roomMessage.attachment : undefined,
37
+ };
38
+ }
14
39
  export class PendingAgentInput {
15
40
  constructor({ messageId, messageType, threadPath, payload, createdAt, awaitingAcceptance = false, awaitingApplication = false, awaitingOnline = false, }) {
16
41
  this.messageId = messageId;
@@ -69,28 +94,52 @@ export class ChatThreadStartResult {
69
94
  }
70
95
  class EventQueue {
71
96
  constructor() {
72
- this.items = [];
73
- this.waiters = [];
97
+ this.subscribers = new Set();
74
98
  }
75
99
  add(item) {
76
- const waiter = this.waiters.shift();
77
- if (waiter != null) {
78
- waiter({ value: item, done: false });
79
- return;
100
+ for (const subscriber of this.subscribers) {
101
+ if (subscriber.closed) {
102
+ continue;
103
+ }
104
+ const waiter = subscriber.waiters.shift();
105
+ if (waiter != null) {
106
+ waiter({ value: item, done: false });
107
+ }
108
+ else {
109
+ subscriber.items.push(item);
110
+ }
80
111
  }
81
- this.items.push(item);
82
112
  }
83
113
  get stream() {
84
114
  return {
85
- [Symbol.asyncIterator]: () => ({
86
- next: async () => {
87
- const item = this.items.shift();
88
- if (item !== undefined) {
89
- return { value: item, done: false };
90
- }
91
- return new Promise((resolve) => this.waiters.push(resolve));
92
- },
93
- }),
115
+ [Symbol.asyncIterator]: () => {
116
+ const subscriber = {
117
+ items: [],
118
+ waiters: [],
119
+ closed: false,
120
+ };
121
+ this.subscribers.add(subscriber);
122
+ return {
123
+ next: async () => {
124
+ if (subscriber.closed) {
125
+ return { done: true, value: undefined };
126
+ }
127
+ const item = subscriber.items.shift();
128
+ if (item !== undefined) {
129
+ return { value: item, done: false };
130
+ }
131
+ return new Promise((resolve) => subscriber.waiters.push(resolve));
132
+ },
133
+ return: async () => {
134
+ subscriber.closed = true;
135
+ this.subscribers.delete(subscriber);
136
+ for (const waiter of subscriber.waiters.splice(0)) {
137
+ waiter({ done: true, value: undefined });
138
+ }
139
+ return { done: true, value: undefined };
140
+ },
141
+ };
142
+ },
94
143
  };
95
144
  }
96
145
  }
@@ -99,6 +148,7 @@ export class BaseChatClient extends ChangeEmitter {
99
148
  super(...arguments);
100
149
  this.sessionsByPath = new Map();
101
150
  this.pendingStartRequests = new Map();
151
+ this.pendingSessionEventsByPath = new Map();
102
152
  this.eventQueue = new EventQueue();
103
153
  }
104
154
  get events() {
@@ -121,7 +171,7 @@ export class BaseChatClient extends ChangeEmitter {
121
171
  cleanParticipantName(value) {
122
172
  return nonEmptyTrimmed(value);
123
173
  }
124
- openThread(threadPath, { load = true, sinceTurn, reloadIfOpen = false } = {}) {
174
+ openThread(threadPath, { load = true, sinceTurn, reloadIfOpen = false, drainPendingEvents = true, } = {}) {
125
175
  const normalized = threadPath.trim();
126
176
  if (normalized === "") {
127
177
  throw new TypeError("thread path cannot be empty");
@@ -129,23 +179,32 @@ export class BaseChatClient extends ChangeEmitter {
129
179
  const existing = this.sessionsByPath.get(normalized);
130
180
  if (existing != null) {
131
181
  if (!existing.isOpen || reloadIfOpen) {
132
- void existing.open({ load, sinceTurn });
182
+ this.openSession(existing, { load, sinceTurn });
133
183
  }
134
184
  return existing;
135
185
  }
136
186
  const created = new ChatThreadSession({ client: this, threadPath: normalized });
137
187
  this.sessionsByPath.set(normalized, created);
138
- void created.open({ load, sinceTurn });
188
+ if (drainPendingEvents) {
189
+ this.drainPendingSessionEvents(normalized, created);
190
+ }
191
+ this.openSession(created, { load, sinceTurn });
139
192
  this.notifyListeners();
140
193
  return created;
141
194
  }
142
- async startThread({ messageId, message, attachments, name, provider, model, voice, outputModalities, realtimeProtocol, senderName, clientToolkits, omitContent = false, }) {
195
+ openSession(session, options) {
196
+ void session.open(options).catch(() => {
197
+ this.notifyListeners();
198
+ });
199
+ }
200
+ async startThread({ messageId, message, attachments, name, backend, provider, model, voice, outputModalities, realtimeProtocol, senderName, clientToolkits, toolChoice, omitContent = false, }) {
143
201
  const resolvedMessageId = messageId == null || messageId.trim() === "" ? uuidv4() : messageId.trim();
144
202
  const resolvedSenderName = this.cleanParticipantName(senderName) ?? this.localParticipantName();
145
203
  const payload = new StartThread({
146
204
  messageId: resolvedMessageId,
147
205
  content: omitContent ? undefined : agentInputContent({ text: message, attachments }),
148
206
  name: nonEmptyTrimmed(name),
207
+ backend: nonEmptyTrimmed(backend),
149
208
  provider: nonEmptyTrimmed(provider),
150
209
  model: nonEmptyTrimmed(model),
151
210
  voice: nonEmptyTrimmed(voice),
@@ -153,6 +212,7 @@ export class BaseChatClient extends ChangeEmitter {
153
212
  realtimeProtocol: nonEmptyTrimmed(realtimeProtocol),
154
213
  senderName: resolvedSenderName,
155
214
  clientToolkits: clientToolkits != null && clientToolkits.length > 0 ? clientToolkits : undefined,
215
+ toolChoice: toolChoice?.toJson(),
156
216
  });
157
217
  const completer = new Completer();
158
218
  this.pendingStartRequests.set(resolvedMessageId, completer);
@@ -165,10 +225,14 @@ export class BaseChatClient extends ChangeEmitter {
165
225
  if (threadPath === "") {
166
226
  throw new Error("Agent did not return a thread_id for the new thread.");
167
227
  }
168
- const session = this.openThread(threadPath, { load: false });
228
+ const session = this.openThread(threadPath, { load: false, drainPendingEvents: false });
169
229
  if (!omitContent) {
170
- session.addAgentMessage(new AgentMessageEvent({ message: payload }));
230
+ session.addAgentMessage(new AgentMessageEvent({
231
+ message: this.resolvedStartThreadMessage({ payload, threadPath }),
232
+ createdAt: dateOrNull(payload.createdAt) ?? undefined,
233
+ }));
171
234
  }
235
+ this.drainPendingSessionEvents(threadPath, session);
172
236
  return new ChatThreadStartResult({
173
237
  session,
174
238
  threadPath,
@@ -181,16 +245,43 @@ export class BaseChatClient extends ChangeEmitter {
181
245
  this.pendingStartRequests.delete(resolvedMessageId);
182
246
  }
183
247
  }
184
- handleAgentMessage(message, { attachment } = {}) {
248
+ resolvedStartThreadMessage({ payload, threadPath, }) {
249
+ return new TurnStart({
250
+ threadId: threadPath,
251
+ messageId: payload.messageId,
252
+ senderName: payload.senderName,
253
+ content: payload.content,
254
+ backend: payload.backend,
255
+ provider: payload.provider,
256
+ model: payload.model,
257
+ voice: payload.voice,
258
+ outputModalities: payload.outputModalities,
259
+ instructions: payload.instructions,
260
+ mcp: payload.mcp,
261
+ clientToolkits: payload.clientToolkits,
262
+ toolkits: payload.toolkits,
263
+ });
264
+ }
265
+ drainPendingSessionEvents(threadPath, session) {
266
+ const pendingEvents = this.pendingSessionEventsByPath.get(threadPath);
267
+ if (pendingEvents == null) {
268
+ return;
269
+ }
270
+ this.pendingSessionEventsByPath.delete(threadPath);
271
+ for (const event of pendingEvents) {
272
+ session.addAgentMessage(event);
273
+ }
274
+ }
275
+ handleAgentMessage(message, { createdAt, attachment } = {}) {
185
276
  if (message.type === agentConnectionStatusType) {
186
277
  this._connectionStatus = message;
187
278
  for (const session of this.sessionsByPath.values()) {
188
279
  if (session.isOpen) {
189
- session.addAgentMessage(new AgentMessageEvent({ message, attachment }));
280
+ session.addAgentMessage(new AgentMessageEvent({ message, createdAt, attachment }));
190
281
  }
191
282
  }
192
283
  }
193
- this.eventQueue.add(new AgentMessageEvent({ message, attachment }));
284
+ this.eventQueue.add(new AgentMessageEvent({ message, createdAt, attachment }));
194
285
  const sourceMessageId = sourceMessageIdFor(message);
195
286
  if ((message.type === agentThreadStartedType || message.type === agentThreadStartRejectedType) &&
196
287
  sourceMessageId != null &&
@@ -205,7 +296,16 @@ export class BaseChatClient extends ChangeEmitter {
205
296
  this.notifyListeners();
206
297
  return;
207
298
  }
208
- this.sessionsByPath.get(threadPath)?.addAgentMessage(new AgentMessageEvent({ message, attachment }));
299
+ const event = new AgentMessageEvent({ message, createdAt, attachment });
300
+ const session = this.sessionsByPath.get(threadPath);
301
+ if (session != null) {
302
+ session.addAgentMessage(event);
303
+ }
304
+ else {
305
+ const pending = this.pendingSessionEventsByPath.get(threadPath) ?? [];
306
+ pending.push(event);
307
+ this.pendingSessionEventsByPath.set(threadPath, pending);
308
+ }
209
309
  this.notifyListeners();
210
310
  }
211
311
  emitConnectionStatus({ status, message, reason, retryDelay, }) {
@@ -238,7 +338,6 @@ export class MessagingChatClient extends BaseChatClient {
238
338
  constructor({ room, agentName }) {
239
339
  super();
240
340
  this.pendingParticipantWaits = new Map();
241
- this.roomAbortController = new AbortController();
242
341
  this.started = false;
243
342
  this.onMessagingChanged = () => {
244
343
  const participant = this.agentParticipant();
@@ -252,10 +351,26 @@ export class MessagingChatClient extends BaseChatClient {
252
351
  }
253
352
  this.notifyListeners();
254
353
  };
354
+ this.onMessagingMessage = (event) => {
355
+ this.onMessagingChanged();
356
+ const roomMessage = agentRoomMessageFromEvent(event);
357
+ if (roomMessage == null || roomMessage.local) {
358
+ return;
359
+ }
360
+ const participant = this.agentParticipant();
361
+ const fromSelectedAgent = participant != null && roomMessage.fromParticipantId === participant.id;
362
+ const fromParticipantlessThreadList = roomMessage.fromParticipantId == null && isThreadListMessage(roomMessage.message);
363
+ if (!fromSelectedAgent && !fromParticipantlessThreadList) {
364
+ return;
365
+ }
366
+ this.handleAgentMessage(AgentMessageFactory.fromJson(roomMessage.message), {
367
+ createdAt: dateOrNull(roomMessage.message.created_at),
368
+ attachment: roomMessage.attachment,
369
+ });
370
+ };
255
371
  this.room = room;
256
372
  this.agentName = agentName;
257
- this.room.messaging.addListener("message", this.onMessagingChanged);
258
- this.roomPump = this.consumeRoomEvents();
373
+ this.room.messaging.addListener("message", this.onMessagingMessage);
259
374
  }
260
375
  localParticipantName() {
261
376
  const name = this.room.localParticipant?.getAttribute("name");
@@ -271,15 +386,13 @@ export class MessagingChatClient extends BaseChatClient {
271
386
  }
272
387
  async stop() {
273
388
  this.started = false;
274
- this.roomAbortController.abort();
275
- this.room.messaging.removeListener("message", this.onMessagingChanged);
389
+ this.room.messaging.removeListener("message", this.onMessagingMessage);
276
390
  for (const wait of this.pendingParticipantWaits.values()) {
277
391
  if (!wait.completed) {
278
392
  wait.completeError(new Error("Chat client stopped while waiting for an agent participant."));
279
393
  }
280
394
  }
281
395
  this.pendingParticipantWaits.clear();
282
- await this.roomPump?.catch(() => undefined);
283
396
  }
284
397
  agentParticipant() {
285
398
  const normalizedAgentName = this.agentName?.trim();
@@ -320,37 +433,21 @@ export class MessagingChatClient extends BaseChatClient {
320
433
  ignoreOffline,
321
434
  });
322
435
  }
323
- async consumeRoomEvents() {
324
- try {
325
- for await (const event of this.room.listen({ abortSignal: this.roomAbortController.signal })) {
326
- this.onRoomEvent(event);
327
- }
328
- }
329
- catch {
330
- if (!this.roomAbortController.signal.aborted) {
331
- throw new Error("chat client room event listener stopped unexpectedly");
332
- }
333
- }
334
- }
335
- onRoomEvent(event) {
336
- if (!(event instanceof RoomMessageEvent) || event.message.type !== agentRoomMessageType) {
337
- return;
338
- }
339
- const participant = this.agentParticipant();
340
- if (participant == null || event.message.fromParticipantId !== participant.id) {
341
- return;
342
- }
343
- const message = event.message.message;
344
- const rawPayload = typeof message.type === "string" ? message : message.payload;
345
- if (rawPayload != null && typeof rawPayload === "object" && !Array.isArray(rawPayload)) {
346
- this.handleAgentMessage(AgentMessageFactory.fromJson(rawPayload), { attachment: event.message.attachment });
347
- }
348
- }
436
+ }
437
+ function isThreadListMessage(message) {
438
+ return message.type === agentThreadListedType
439
+ || message.type === agentThreadCreatedType
440
+ || message.type === agentThreadUpdatedType
441
+ || message.type === agentThreadDeletedType;
349
442
  }
350
443
  export class ChatThreadSession extends ChangeEmitter {
351
444
  constructor({ client, threadPath }) {
352
445
  super();
353
446
  this._messages = [];
447
+ this.messageIndexes = new Map();
448
+ this.localAgentMessageIds = new Set();
449
+ this.pendingLocalInputMessageIds = new Set();
450
+ this.mergedDeltaMessageIds = new Set();
354
451
  this.pendingInputsById = new Map();
355
452
  this.openState = false;
356
453
  this.loadStateValue = ChatThreadSessionLoadState.idle();
@@ -375,9 +472,24 @@ export class ChatThreadSession extends ChangeEmitter {
375
472
  get lastCompletedTurnId() {
376
473
  return this.lastCompletedTurnIdValue;
377
474
  }
475
+ get activeTurnId() {
476
+ return this.activeTurnIdValue;
477
+ }
478
+ get threadStatus() {
479
+ return this.threadStatusValue;
480
+ }
481
+ get currentModel() {
482
+ return this.currentModelValue;
483
+ }
484
+ get modelsResponse() {
485
+ return this.modelsResponseValue;
486
+ }
378
487
  async open({ load = true, sinceTurn } = {}) {
379
488
  if (this.openState) {
380
489
  if (load) {
490
+ if (sinceTurn == null) {
491
+ this.resetReplayState();
492
+ }
381
493
  const openThread = new OpenThread({ threadId: this.threadPath, load: true, sinceTurn });
382
494
  this.setLoadState(this.loadingStateForOpen(openThread, sinceTurn));
383
495
  try {
@@ -408,6 +520,17 @@ export class ChatThreadSession extends ChangeEmitter {
408
520
  throw error;
409
521
  }
410
522
  }
523
+ resetReplayState() {
524
+ this._messages.length = 0;
525
+ this.messageIndexes.clear();
526
+ this.localAgentMessageIds.clear();
527
+ this.pendingLocalInputMessageIds.clear();
528
+ this.mergedDeltaMessageIds.clear();
529
+ this.pendingInputsById.clear();
530
+ this.lastCompletedTurnIdValue = undefined;
531
+ this.lastAgentMessageType = undefined;
532
+ this.lastAgentMessageAt = undefined;
533
+ }
411
534
  async close() {
412
535
  if (!this.openState) {
413
536
  return;
@@ -418,10 +541,11 @@ export class ChatThreadSession extends ChangeEmitter {
418
541
  requestModels({ ignoreOffline = false } = {}) {
419
542
  return this.client.sendAgentMessage(new ModelsRequest({ messageId: uuidv4() }), { ignoreOffline });
420
543
  }
421
- changeModel({ provider, model, voice }) {
544
+ changeModel({ backend, provider, model, voice }) {
422
545
  return this.client.sendAgentMessage(new ChangeModel({
423
546
  threadId: this.threadPath,
424
547
  messageId: uuidv4(),
548
+ backend: nonEmptyTrimmed(backend),
425
549
  provider,
426
550
  model,
427
551
  voice: nonEmptyTrimmed(voice),
@@ -438,8 +562,16 @@ export class ChatThreadSession extends ChangeEmitter {
438
562
  response,
439
563
  }));
440
564
  }
441
- async sendText({ messageId, text, attachments, steer = false, turnId, provider, model, voice, outputModalities, senderName, clientToolkits, }) {
565
+ async sendText({ messageId, text, attachments, steer = false, turnId, backend, provider, model, voice, outputModalities, senderName, clientToolkits, toolChoice, }) {
442
566
  const resolvedMessageId = messageId == null || messageId.trim() === "" ? uuidv4() : messageId.trim();
567
+ const currentModel = this.currentModelValue;
568
+ const resolvedBackend = nonEmptyTrimmed(backend) ?? currentModel?.backend;
569
+ const resolvedProvider = nonEmptyTrimmed(provider) ?? currentModel?.provider;
570
+ const resolvedModel = nonEmptyTrimmed(model) ?? currentModel?.model;
571
+ const resolvedVoice = nonEmptyTrimmed(voice) ?? currentModel?.voice;
572
+ const resolvedOutputModalities = outputModalities != null && outputModalities.length > 0
573
+ ? outputModalities
574
+ : currentModel?.outputModalities;
443
575
  const payload = steer
444
576
  ? new TurnSteer({
445
577
  threadId: this.threadPath,
@@ -453,21 +585,16 @@ export class ChatThreadSession extends ChangeEmitter {
453
585
  messageId: resolvedMessageId,
454
586
  senderName: nonEmptyTrimmed(senderName),
455
587
  content: agentInputContent({ text, attachments }),
456
- provider: nonEmptyTrimmed(provider),
457
- model: nonEmptyTrimmed(model),
458
- voice: nonEmptyTrimmed(voice),
459
- outputModalities: outputModalities != null && outputModalities.length > 0 ? outputModalities : undefined,
588
+ backend: resolvedBackend,
589
+ provider: resolvedProvider,
590
+ model: resolvedModel,
591
+ voice: resolvedVoice,
592
+ outputModalities: resolvedOutputModalities,
460
593
  clientToolkits: clientToolkits != null && clientToolkits.length > 0 ? clientToolkits : undefined,
594
+ toolChoice: toolChoice?.toJson(),
461
595
  });
462
- this.markPending(new PendingAgentInput({
463
- messageId: resolvedMessageId,
464
- messageType: payload.type,
465
- threadPath: this.threadPath,
466
- payload,
467
- createdAt: new Date(),
468
- awaitingAcceptance: true,
469
- awaitingApplication: true,
470
- }));
596
+ this.localAgentMessageIds.add(resolvedMessageId);
597
+ this.pendingLocalInputMessageIds.add(resolvedMessageId);
471
598
  this.addAgentMessage(new AgentMessageEvent({ message: payload }));
472
599
  try {
473
600
  await this.client.sendAgentMessage(payload);
@@ -511,58 +638,284 @@ export class ChatThreadSession extends ChangeEmitter {
511
638
  return this.client.sendAgentMessage(new RenameThread({ threadId: threadPath, name }));
512
639
  }
513
640
  addAgentMessage(event) {
514
- this._messages.push(event);
515
641
  const message = event.message;
516
642
  const type = message.type;
517
643
  this.lastAgentMessageType = type;
518
- this.lastAgentMessageAt = new Date();
644
+ this.lastAgentMessageAt = event.createdAt;
645
+ this.updateSessionState(message);
646
+ this.updatePendingInputsForMessage(message, event.createdAt);
647
+ this.appendRenderableMessage(event);
648
+ this.notifyListeners();
649
+ }
650
+ updateSessionState(message) {
651
+ if (message.type === agentTurnEndedType) {
652
+ const turnId = normalizedString(threadMessageValue(message, "turnId"));
653
+ if (turnId != null) {
654
+ this.lastCompletedTurnIdValue = turnId;
655
+ if (this.activeTurnIdValue === turnId) {
656
+ this.activeTurnIdValue = undefined;
657
+ }
658
+ }
659
+ const statusTurnId = normalizedString(this.threadStatusValue?.turnId);
660
+ if (statusTurnId == null || statusTurnId === turnId) {
661
+ this.threadStatusValue = undefined;
662
+ }
663
+ return;
664
+ }
665
+ if (message.type === agentTurnStartAcceptedType || message.type === agentTurnStartedType) {
666
+ const turnId = normalizedString(threadMessageValue(message, "turnId"));
667
+ if (turnId != null) {
668
+ this.activeTurnIdValue = turnId;
669
+ }
670
+ if (message.type === agentTurnStartedType) {
671
+ const statusTurnId = normalizedString(this.threadStatusValue?.turnId);
672
+ if (statusTurnId == null || statusTurnId === turnId) {
673
+ this.threadStatusValue = undefined;
674
+ }
675
+ }
676
+ return;
677
+ }
678
+ if (message.type === agentThreadStatusType && message instanceof AgentThreadStatus) {
679
+ this.threadStatusValue = message;
680
+ const mode = normalizedString(message.mode);
681
+ const status = normalizedString(message.status);
682
+ const turnId = normalizedString(message.turnId);
683
+ if (status != null && mode === "steerable" && turnId != null) {
684
+ this.activeTurnIdValue = turnId;
685
+ }
686
+ else if (status == null && turnId != null && this.activeTurnIdValue === turnId) {
687
+ this.activeTurnIdValue = undefined;
688
+ }
689
+ return;
690
+ }
691
+ if (message.type === agentModelChangedType && message instanceof AgentModelChanged) {
692
+ this.currentModelValue = message;
693
+ return;
694
+ }
695
+ if (message.type === agentModelsResponseType && message instanceof ModelsResponse) {
696
+ this.modelsResponseValue = message;
697
+ this.applyActiveModel(message);
698
+ return;
699
+ }
700
+ if (message.type === agentThreadLoadedType) {
701
+ this.setLoadState(new ChatThreadSessionLoadState({
702
+ phase: ChatThreadSessionLoadPhase.Loaded,
703
+ startedAt: this.loadStateValue.startedAt,
704
+ completedAt: this.lastAgentMessageAt,
705
+ requestMessageId: this.loadStateValue.requestMessageId,
706
+ sinceTurn: this.loadStateValue.sinceTurn,
707
+ lastMessageType: message.type,
708
+ lastMessageAt: this.lastAgentMessageAt,
709
+ }), { notify: false });
710
+ }
711
+ }
712
+ updatePendingInputsForMessage(message, createdAt) {
713
+ const type = message.type;
519
714
  const messageId = message.messageId;
520
715
  const sourceMessageId = sourceMessageIdFor(message);
521
- if (type === agentTurnStartType || type === agentTurnSteerType) {
716
+ if (type === agentThreadStartType || type === agentTurnStartType || type === agentTurnSteerType) {
522
717
  const normalizedMessageId = messageId.trim();
523
718
  if (normalizedMessageId !== "") {
719
+ this.localAgentMessageIds.add(normalizedMessageId);
720
+ this.pendingLocalInputMessageIds.add(normalizedMessageId);
524
721
  this.markPending(new PendingAgentInput({
525
722
  messageId: normalizedMessageId,
526
723
  messageType: type,
527
724
  threadPath: this.threadPath,
528
725
  payload: message,
529
- createdAt: new Date(),
726
+ createdAt,
530
727
  awaitingAcceptance: true,
531
728
  awaitingApplication: true,
532
729
  }));
533
730
  }
534
731
  }
732
+ else if (type === agentThreadStartRejectedType) {
733
+ if (sourceMessageId != null) {
734
+ this.pendingInputsById.delete(sourceMessageId);
735
+ }
736
+ }
535
737
  else if (type === agentTurnStartAcceptedType || type === agentTurnSteerAcceptedType) {
536
738
  this.updatePending(sourceMessageId, { awaitingAcceptance: false, awaitingApplication: true });
537
739
  }
538
740
  else if (type === agentTurnStartedType || type === agentTurnSteeredType) {
539
741
  if (sourceMessageId != null) {
540
742
  this.pendingInputsById.delete(sourceMessageId);
743
+ this.pendingLocalInputMessageIds.delete(sourceMessageId);
541
744
  }
542
745
  }
543
746
  else if (type === agentTurnStartRejectedType || type === agentTurnSteerRejectedType) {
544
747
  if (sourceMessageId != null) {
545
748
  this.pendingInputsById.delete(sourceMessageId);
749
+ this.pendingLocalInputMessageIds.delete(sourceMessageId);
546
750
  }
547
751
  }
548
752
  else if (type === agentTurnEndedType || type === agentThreadClearedType) {
549
753
  this.pendingInputsById.clear();
550
- if (message.type === agentTurnEndedType && "turnId" in message && typeof message.turnId === "string" && message.turnId.trim() !== "") {
551
- this.lastCompletedTurnIdValue = message.turnId.trim();
754
+ this.pendingLocalInputMessageIds.clear();
755
+ }
756
+ }
757
+ appendRenderableMessage(event) {
758
+ const message = event.message;
759
+ if (message.type === agentTurnEndedType) {
760
+ if (message.error != null) {
761
+ this.appendMessage(event);
552
762
  }
763
+ return;
553
764
  }
554
- else if (type === agentThreadLoadedType) {
555
- this.setLoadState(new ChatThreadSessionLoadState({
556
- phase: ChatThreadSessionLoadPhase.Loaded,
557
- startedAt: this.loadStateValue.startedAt,
558
- completedAt: this.lastAgentMessageAt,
559
- requestMessageId: this.loadStateValue.requestMessageId,
560
- sinceTurn: this.loadStateValue.sinceTurn,
561
- lastMessageType: type,
562
- lastMessageAt: this.lastAgentMessageAt,
563
- }), { notify: false });
765
+ if (message.type === agentToolCallEndedType) {
766
+ const itemId = normalizedString(message.itemId);
767
+ if (itemId != null) {
768
+ this.upsertItemMessage(`tool_call:${itemId}`, event);
769
+ }
770
+ else if (message.error != null) {
771
+ this.appendMessage(event);
772
+ }
773
+ return;
774
+ }
775
+ if (message.type === agentToolCallPendingType || message.type === agentToolCallInProgressType || message.type === agentToolCallStartedType) {
776
+ const itemId = normalizedString(message.itemId);
777
+ if (itemId != null) {
778
+ this.upsertItemMessage(`tool_call:${itemId}`, event);
779
+ }
780
+ return;
781
+ }
782
+ if (message.type === agentSecretRequestedType) {
783
+ this.appendMessage(event);
784
+ return;
785
+ }
786
+ if (message.type === agentModelChangedType) {
787
+ this.appendMessage(event);
788
+ return;
789
+ }
790
+ if (message.type === agentThreadStartType || message.type === agentTurnStartType || message.type === agentTurnSteerType) {
791
+ if (agentInputText(message.content).trim() !== "") {
792
+ this.appendMessage(event);
793
+ }
794
+ return;
795
+ }
796
+ if (message.type === agentTurnStartAcceptedType || message.type === agentTurnSteerAcceptedType) {
797
+ const sourceMessageId = normalizedString(sourceMessageIdFor(message));
798
+ if (sourceMessageId != null && this.localAgentMessageIds.has(sourceMessageId)) {
799
+ this.pendingLocalInputMessageIds.delete(sourceMessageId);
800
+ return;
801
+ }
802
+ if (agentInputText(message.content).trim() !== "") {
803
+ this.appendMessage(event, { beforePendingLocalInputs: true });
804
+ }
805
+ else {
806
+ this.appendMessage(event);
807
+ }
808
+ return;
809
+ }
810
+ if (message.type === agentTurnStartedType ||
811
+ message.type === agentTurnSteeredType ||
812
+ message.type === agentTurnStartRejectedType ||
813
+ message.type === agentTurnSteerRejectedType ||
814
+ message.type === agentTurnInterruptAcceptedType ||
815
+ message.type === agentTurnInterruptedType) {
816
+ this.appendMessage(event);
817
+ return;
818
+ }
819
+ if (isMergeableDelta(message)) {
820
+ this.appendOrMergeDelta(event);
821
+ return;
822
+ }
823
+ if (message.type === agentImageGenerationPartialType || message.type === agentImageGenerationCompletedType) {
824
+ const itemId = normalizedString(message.itemId);
825
+ if (itemId != null) {
826
+ this.upsertItemMessage(`image_generation:${itemId}`, event);
827
+ }
828
+ return;
829
+ }
830
+ if (message.type === agentUsageUpdatedType) {
831
+ this.appendMessage(event);
832
+ return;
833
+ }
834
+ if (message instanceof AgentClientToolCallRequested) {
835
+ this.appendMessage(event);
836
+ }
837
+ }
838
+ appendMessage(event, { beforePendingLocalInputs = false } = {}) {
839
+ const normalizedMessageId = normalizedString(event.message.messageId);
840
+ if (normalizedMessageId == null || this.messageIndexes.has(normalizedMessageId)) {
841
+ return;
842
+ }
843
+ if (beforePendingLocalInputs) {
844
+ const insertIndex = this._messages.findIndex((existing) => this.pendingLocalInputMessageIds.has(existing.message.messageId));
845
+ if (insertIndex >= 0) {
846
+ this._messages.splice(insertIndex, 0, event);
847
+ this.indexMessagesFrom(insertIndex);
848
+ return;
849
+ }
850
+ }
851
+ this.messageIndexes.set(normalizedMessageId, this._messages.length);
852
+ this._messages.push(event);
853
+ }
854
+ upsertItemMessage(key, event) {
855
+ const existingIndex = this.messageIndexes.get(key);
856
+ if (existingIndex == null) {
857
+ this.messageIndexes.set(key, this._messages.length);
858
+ this._messages.push(event);
859
+ return;
860
+ }
861
+ this._messages[existingIndex] = event;
862
+ }
863
+ appendOrMergeDelta(event) {
864
+ const message = event.message;
865
+ const normalizedMessageId = normalizedString(message.messageId);
866
+ if (normalizedMessageId != null) {
867
+ if (this.mergedDeltaMessageIds.has(normalizedMessageId)) {
868
+ return;
869
+ }
870
+ this.mergedDeltaMessageIds.add(normalizedMessageId);
871
+ }
872
+ const itemId = normalizedString(message.itemId);
873
+ if (itemId == null) {
874
+ return;
875
+ }
876
+ const key = `${message.type}:${itemId}`;
877
+ const existingIndex = this.messageIndexes.get(key);
878
+ if (existingIndex == null) {
879
+ this.messageIndexes.set(key, this._messages.length);
880
+ this._messages.push(event);
881
+ return;
882
+ }
883
+ const existing = this._messages[existingIndex].message;
884
+ const merged = mergeDeltaMessage(existing, message);
885
+ if (merged != null) {
886
+ this._messages[existingIndex] = new AgentMessageEvent({ message: merged, createdAt: event.createdAt, attachment: event.attachment });
887
+ }
888
+ }
889
+ indexMessagesFrom(start) {
890
+ for (let index = Math.max(0, start); index < this._messages.length; index += 1) {
891
+ const key = messageIndexKey(this._messages[index].message);
892
+ if (key != null) {
893
+ this.messageIndexes.set(key, index);
894
+ }
895
+ }
896
+ }
897
+ applyActiveModel(response) {
898
+ for (const provider of response.providers) {
899
+ for (const model of provider.models) {
900
+ if (!model.active) {
901
+ continue;
902
+ }
903
+ this.currentModelValue = new AgentModelChanged({
904
+ threadId: this.threadPath,
905
+ sourceMessageId: response.sourceMessageId,
906
+ backend: provider.backend,
907
+ provider: provider.name,
908
+ model: model.name,
909
+ voice: model.defaultOutputVoice,
910
+ inputFormat: model.inputFormat,
911
+ outputFormat: model.outputFormat,
912
+ turnDetection: model.turnDetection,
913
+ realtimeProtocols: model.realtimeProtocols,
914
+ outputModalities: model.modalities.length > 0 ? [model.modalities[0]] : ["text"],
915
+ });
916
+ return;
917
+ }
564
918
  }
565
- this.notifyListeners();
566
919
  }
567
920
  markClosed({ notify = true } = {}) {
568
921
  this.openState = false;
@@ -626,6 +979,84 @@ function sourceMessageIdFor(message) {
626
979
  const value = "sourceMessageId" in message ? message.sourceMessageId : undefined;
627
980
  return typeof value === "string" ? value : undefined;
628
981
  }
982
+ function threadMessageValue(message, key) {
983
+ const value = message[key];
984
+ return typeof value === "string" ? value : undefined;
985
+ }
986
+ function agentInputText(content) {
987
+ if (!Array.isArray(content)) {
988
+ return "";
989
+ }
990
+ return content
991
+ .filter((entry) => (typeof entry === "object" &&
992
+ entry !== null &&
993
+ "type" in entry &&
994
+ entry.type === "text" &&
995
+ "text" in entry &&
996
+ typeof entry.text === "string"))
997
+ .map((entry) => entry.text)
998
+ .join("");
999
+ }
1000
+ function isMergeableDelta(message) {
1001
+ return message.type === agentAudioGenerationDeltaType ||
1002
+ message.type === agentAudioTranscriptionDeltaType ||
1003
+ message.type === agentFileContentDeltaType ||
1004
+ message.type === agentReasoningContentDeltaType ||
1005
+ message.type === agentTextContentDeltaType ||
1006
+ message.type === agentToolCallArgumentsDeltaType ||
1007
+ message.type === agentToolCallLogDeltaType;
1008
+ }
1009
+ function messageIndexKey(message) {
1010
+ if (isMergeableDelta(message)) {
1011
+ const itemId = normalizedString(message.itemId);
1012
+ return itemId == null ? undefined : `${message.type}:${itemId}`;
1013
+ }
1014
+ return normalizedString(message.messageId);
1015
+ }
1016
+ function mergeDeltaMessage(existing, incoming) {
1017
+ if (existing.type !== incoming.type) {
1018
+ return undefined;
1019
+ }
1020
+ if (existing.type === agentAudioGenerationDeltaType && existing instanceof AgentAudioGenerationDelta && incoming instanceof AgentAudioGenerationDelta) {
1021
+ const data = new Uint8Array(existing.data.length + incoming.data.length);
1022
+ data.set(existing.data, 0);
1023
+ data.set(incoming.data, existing.data.length);
1024
+ return new AgentAudioGenerationDelta({ ...existing.toJson(), data });
1025
+ }
1026
+ if (existing.type === agentAudioTranscriptionDeltaType && existing instanceof AgentAudioTranscriptionDelta && incoming instanceof AgentAudioTranscriptionDelta) {
1027
+ return new AgentAudioTranscriptionDelta({ ...existing.toJson(), text: `${existing.text}${incoming.text}` });
1028
+ }
1029
+ if (existing.type === agentFileContentDeltaType && incoming instanceof AgentFileContentDelta) {
1030
+ return incoming;
1031
+ }
1032
+ if (existing.type === agentReasoningContentDeltaType && existing instanceof AgentReasoningContentDelta && incoming instanceof AgentReasoningContentDelta) {
1033
+ return new AgentReasoningContentDelta({ ...existing.toJson(), text: `${existing.text}${incoming.text}` });
1034
+ }
1035
+ if (existing.type === agentTextContentDeltaType && existing instanceof AgentTextContentDelta && incoming instanceof AgentTextContentDelta) {
1036
+ return new AgentTextContentDelta({ ...existing.toJson(), text: `${existing.text}${incoming.text}` });
1037
+ }
1038
+ if (existing.type === agentToolCallArgumentsDeltaType && existing instanceof AgentToolCallArgumentsDelta && incoming instanceof AgentToolCallArgumentsDelta) {
1039
+ return new AgentToolCallArgumentsDelta({ ...existing.toJson(), delta: `${existing.delta}${incoming.delta}` });
1040
+ }
1041
+ if (existing.type === agentToolCallLogDeltaType && existing instanceof AgentToolCallLogDelta && incoming instanceof AgentToolCallLogDelta) {
1042
+ return new AgentToolCallLogDelta({ ...existing.toJson(), lines: [...existing.lines, ...incoming.lines] });
1043
+ }
1044
+ return undefined;
1045
+ }
1046
+ function normalizedString(value) {
1047
+ const normalized = value?.trim();
1048
+ return normalized == null || normalized === "" ? undefined : normalized;
1049
+ }
1050
+ function dateOrNull(value) {
1051
+ if (value instanceof Date && !Number.isNaN(value.getTime())) {
1052
+ return value;
1053
+ }
1054
+ if (typeof value !== "string" || value.trim() === "") {
1055
+ return undefined;
1056
+ }
1057
+ const parsed = new Date(value);
1058
+ return Number.isNaN(parsed.getTime()) ? undefined : parsed;
1059
+ }
629
1060
  function nonEmptyTrimmed(value) {
630
1061
  const normalized = value?.trim();
631
1062
  return normalized == null || normalized === "" ? undefined : normalized;