@meshagent/meshagent-agents 0.43.1 → 0.43.3

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,6 +1,6 @@
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, agentThreadLoadedType, agentConnectionStatusType, agentThreadStatusType, agentThreadStartType, agentThreadStartRejectedType, agentThreadStartedType, agentToolCallArgumentsDeltaType, agentToolCallLogDeltaType, agentTurnEndedType, agentTurnStartAcceptedType, agentTurnStartRejectedType, agentTurnStartType, agentTurnStartedType, agentTurnSteerAcceptedType, agentTurnSteerRejectedType, agentTurnSteerType, agentTurnSteeredType, } from "./agent-messages.js";
4
4
  import { ChangeEmitter } from "./change-emitter.js";
5
5
  export class AgentMessageEvent {
6
6
  constructor({ message, attachment }) {
@@ -11,6 +11,30 @@ export class AgentMessageEvent {
11
11
  return this.message.toJson();
12
12
  }
13
13
  }
14
+ function agentRoomMessageFromEvent(event) {
15
+ const candidate = event.message;
16
+ if (candidate == null || typeof candidate !== "object" || Array.isArray(candidate)) {
17
+ return undefined;
18
+ }
19
+ const roomMessage = candidate;
20
+ if (roomMessage.type !== agentRoomMessageType) {
21
+ return undefined;
22
+ }
23
+ if (roomMessage.message == null || typeof roomMessage.message !== "object" || Array.isArray(roomMessage.message)) {
24
+ return undefined;
25
+ }
26
+ const rawMessage = roomMessage.message;
27
+ const payload = typeof rawMessage.type === "string" ? rawMessage : rawMessage.payload;
28
+ if (payload == null || typeof payload !== "object" || Array.isArray(payload)) {
29
+ return undefined;
30
+ }
31
+ return {
32
+ fromParticipantId: typeof roomMessage.fromParticipantId === "string" ? roomMessage.fromParticipantId : undefined,
33
+ message: payload,
34
+ local: roomMessage.local === true,
35
+ attachment: roomMessage.attachment instanceof Uint8Array ? roomMessage.attachment : undefined,
36
+ };
37
+ }
14
38
  export class PendingAgentInput {
15
39
  constructor({ messageId, messageType, threadPath, payload, createdAt, awaitingAcceptance = false, awaitingApplication = false, awaitingOnline = false, }) {
16
40
  this.messageId = messageId;
@@ -69,28 +93,52 @@ export class ChatThreadStartResult {
69
93
  }
70
94
  class EventQueue {
71
95
  constructor() {
72
- this.items = [];
73
- this.waiters = [];
96
+ this.subscribers = new Set();
74
97
  }
75
98
  add(item) {
76
- const waiter = this.waiters.shift();
77
- if (waiter != null) {
78
- waiter({ value: item, done: false });
79
- return;
99
+ for (const subscriber of this.subscribers) {
100
+ if (subscriber.closed) {
101
+ continue;
102
+ }
103
+ const waiter = subscriber.waiters.shift();
104
+ if (waiter != null) {
105
+ waiter({ value: item, done: false });
106
+ }
107
+ else {
108
+ subscriber.items.push(item);
109
+ }
80
110
  }
81
- this.items.push(item);
82
111
  }
83
112
  get stream() {
84
113
  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
- }),
114
+ [Symbol.asyncIterator]: () => {
115
+ const subscriber = {
116
+ items: [],
117
+ waiters: [],
118
+ closed: false,
119
+ };
120
+ this.subscribers.add(subscriber);
121
+ return {
122
+ next: async () => {
123
+ if (subscriber.closed) {
124
+ return { done: true, value: undefined };
125
+ }
126
+ const item = subscriber.items.shift();
127
+ if (item !== undefined) {
128
+ return { value: item, done: false };
129
+ }
130
+ return new Promise((resolve) => subscriber.waiters.push(resolve));
131
+ },
132
+ return: async () => {
133
+ subscriber.closed = true;
134
+ this.subscribers.delete(subscriber);
135
+ for (const waiter of subscriber.waiters.splice(0)) {
136
+ waiter({ done: true, value: undefined });
137
+ }
138
+ return { done: true, value: undefined };
139
+ },
140
+ };
141
+ },
94
142
  };
95
143
  }
96
144
  }
@@ -99,6 +147,7 @@ export class BaseChatClient extends ChangeEmitter {
99
147
  super(...arguments);
100
148
  this.sessionsByPath = new Map();
101
149
  this.pendingStartRequests = new Map();
150
+ this.pendingSessionEventsByPath = new Map();
102
151
  this.eventQueue = new EventQueue();
103
152
  }
104
153
  get events() {
@@ -121,7 +170,7 @@ export class BaseChatClient extends ChangeEmitter {
121
170
  cleanParticipantName(value) {
122
171
  return nonEmptyTrimmed(value);
123
172
  }
124
- openThread(threadPath, { load = true, sinceTurn, reloadIfOpen = false } = {}) {
173
+ openThread(threadPath, { load = true, sinceTurn, reloadIfOpen = false, drainPendingEvents = true, } = {}) {
125
174
  const normalized = threadPath.trim();
126
175
  if (normalized === "") {
127
176
  throw new TypeError("thread path cannot be empty");
@@ -129,23 +178,32 @@ export class BaseChatClient extends ChangeEmitter {
129
178
  const existing = this.sessionsByPath.get(normalized);
130
179
  if (existing != null) {
131
180
  if (!existing.isOpen || reloadIfOpen) {
132
- void existing.open({ load, sinceTurn });
181
+ this.openSession(existing, { load, sinceTurn });
133
182
  }
134
183
  return existing;
135
184
  }
136
185
  const created = new ChatThreadSession({ client: this, threadPath: normalized });
137
186
  this.sessionsByPath.set(normalized, created);
138
- void created.open({ load, sinceTurn });
187
+ if (drainPendingEvents) {
188
+ this.drainPendingSessionEvents(normalized, created);
189
+ }
190
+ this.openSession(created, { load, sinceTurn });
139
191
  this.notifyListeners();
140
192
  return created;
141
193
  }
142
- async startThread({ messageId, message, attachments, name, provider, model, voice, outputModalities, realtimeProtocol, senderName, clientToolkits, omitContent = false, }) {
194
+ openSession(session, options) {
195
+ void session.open(options).catch(() => {
196
+ this.notifyListeners();
197
+ });
198
+ }
199
+ async startThread({ messageId, message, attachments, name, backend, provider, model, voice, outputModalities, realtimeProtocol, senderName, clientToolkits, omitContent = false, }) {
143
200
  const resolvedMessageId = messageId == null || messageId.trim() === "" ? uuidv4() : messageId.trim();
144
201
  const resolvedSenderName = this.cleanParticipantName(senderName) ?? this.localParticipantName();
145
202
  const payload = new StartThread({
146
203
  messageId: resolvedMessageId,
147
204
  content: omitContent ? undefined : agentInputContent({ text: message, attachments }),
148
205
  name: nonEmptyTrimmed(name),
206
+ backend: nonEmptyTrimmed(backend),
149
207
  provider: nonEmptyTrimmed(provider),
150
208
  model: nonEmptyTrimmed(model),
151
209
  voice: nonEmptyTrimmed(voice),
@@ -165,10 +223,11 @@ export class BaseChatClient extends ChangeEmitter {
165
223
  if (threadPath === "") {
166
224
  throw new Error("Agent did not return a thread_id for the new thread.");
167
225
  }
168
- const session = this.openThread(threadPath, { load: false });
226
+ const session = this.openThread(threadPath, { load: false, drainPendingEvents: false });
169
227
  if (!omitContent) {
170
228
  session.addAgentMessage(new AgentMessageEvent({ message: payload }));
171
229
  }
230
+ this.drainPendingSessionEvents(threadPath, session);
172
231
  return new ChatThreadStartResult({
173
232
  session,
174
233
  threadPath,
@@ -181,6 +240,16 @@ export class BaseChatClient extends ChangeEmitter {
181
240
  this.pendingStartRequests.delete(resolvedMessageId);
182
241
  }
183
242
  }
243
+ drainPendingSessionEvents(threadPath, session) {
244
+ const pendingEvents = this.pendingSessionEventsByPath.get(threadPath);
245
+ if (pendingEvents == null) {
246
+ return;
247
+ }
248
+ this.pendingSessionEventsByPath.delete(threadPath);
249
+ for (const event of pendingEvents) {
250
+ session.addAgentMessage(event);
251
+ }
252
+ }
184
253
  handleAgentMessage(message, { attachment } = {}) {
185
254
  if (message.type === agentConnectionStatusType) {
186
255
  this._connectionStatus = message;
@@ -205,7 +274,16 @@ export class BaseChatClient extends ChangeEmitter {
205
274
  this.notifyListeners();
206
275
  return;
207
276
  }
208
- this.sessionsByPath.get(threadPath)?.addAgentMessage(new AgentMessageEvent({ message, attachment }));
277
+ const event = new AgentMessageEvent({ message, attachment });
278
+ const session = this.sessionsByPath.get(threadPath);
279
+ if (session != null) {
280
+ session.addAgentMessage(event);
281
+ }
282
+ else {
283
+ const pending = this.pendingSessionEventsByPath.get(threadPath) ?? [];
284
+ pending.push(event);
285
+ this.pendingSessionEventsByPath.set(threadPath, pending);
286
+ }
209
287
  this.notifyListeners();
210
288
  }
211
289
  emitConnectionStatus({ status, message, reason, retryDelay, }) {
@@ -238,7 +316,6 @@ export class MessagingChatClient extends BaseChatClient {
238
316
  constructor({ room, agentName }) {
239
317
  super();
240
318
  this.pendingParticipantWaits = new Map();
241
- this.roomAbortController = new AbortController();
242
319
  this.started = false;
243
320
  this.onMessagingChanged = () => {
244
321
  const participant = this.agentParticipant();
@@ -252,10 +329,21 @@ export class MessagingChatClient extends BaseChatClient {
252
329
  }
253
330
  this.notifyListeners();
254
331
  };
332
+ this.onMessagingMessage = (event) => {
333
+ this.onMessagingChanged();
334
+ const roomMessage = agentRoomMessageFromEvent(event);
335
+ if (roomMessage == null || roomMessage.local) {
336
+ return;
337
+ }
338
+ const participant = this.agentParticipant();
339
+ if (participant == null || roomMessage.fromParticipantId !== participant.id) {
340
+ return;
341
+ }
342
+ this.handleAgentMessage(AgentMessageFactory.fromJson(roomMessage.message), { attachment: roomMessage.attachment });
343
+ };
255
344
  this.room = room;
256
345
  this.agentName = agentName;
257
- this.room.messaging.addListener("message", this.onMessagingChanged);
258
- this.roomPump = this.consumeRoomEvents();
346
+ this.room.messaging.addListener("message", this.onMessagingMessage);
259
347
  }
260
348
  localParticipantName() {
261
349
  const name = this.room.localParticipant?.getAttribute("name");
@@ -271,15 +359,13 @@ export class MessagingChatClient extends BaseChatClient {
271
359
  }
272
360
  async stop() {
273
361
  this.started = false;
274
- this.roomAbortController.abort();
275
- this.room.messaging.removeListener("message", this.onMessagingChanged);
362
+ this.room.messaging.removeListener("message", this.onMessagingMessage);
276
363
  for (const wait of this.pendingParticipantWaits.values()) {
277
364
  if (!wait.completed) {
278
365
  wait.completeError(new Error("Chat client stopped while waiting for an agent participant."));
279
366
  }
280
367
  }
281
368
  this.pendingParticipantWaits.clear();
282
- await this.roomPump?.catch(() => undefined);
283
369
  }
284
370
  agentParticipant() {
285
371
  const normalizedAgentName = this.agentName?.trim();
@@ -320,37 +406,15 @@ export class MessagingChatClient extends BaseChatClient {
320
406
  ignoreOffline,
321
407
  });
322
408
  }
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
- }
349
409
  }
350
410
  export class ChatThreadSession extends ChangeEmitter {
351
411
  constructor({ client, threadPath }) {
352
412
  super();
353
413
  this._messages = [];
414
+ this.messageIndexes = new Map();
415
+ this.localAgentMessageIds = new Set();
416
+ this.pendingLocalInputMessageIds = new Set();
417
+ this.mergedDeltaMessageIds = new Set();
354
418
  this.pendingInputsById = new Map();
355
419
  this.openState = false;
356
420
  this.loadStateValue = ChatThreadSessionLoadState.idle();
@@ -375,6 +439,18 @@ export class ChatThreadSession extends ChangeEmitter {
375
439
  get lastCompletedTurnId() {
376
440
  return this.lastCompletedTurnIdValue;
377
441
  }
442
+ get activeTurnId() {
443
+ return this.activeTurnIdValue;
444
+ }
445
+ get threadStatus() {
446
+ return this.threadStatusValue;
447
+ }
448
+ get currentModel() {
449
+ return this.currentModelValue;
450
+ }
451
+ get modelsResponse() {
452
+ return this.modelsResponseValue;
453
+ }
378
454
  async open({ load = true, sinceTurn } = {}) {
379
455
  if (this.openState) {
380
456
  if (load) {
@@ -418,10 +494,11 @@ export class ChatThreadSession extends ChangeEmitter {
418
494
  requestModels({ ignoreOffline = false } = {}) {
419
495
  return this.client.sendAgentMessage(new ModelsRequest({ messageId: uuidv4() }), { ignoreOffline });
420
496
  }
421
- changeModel({ provider, model, voice }) {
497
+ changeModel({ backend, provider, model, voice }) {
422
498
  return this.client.sendAgentMessage(new ChangeModel({
423
499
  threadId: this.threadPath,
424
500
  messageId: uuidv4(),
501
+ backend: nonEmptyTrimmed(backend),
425
502
  provider,
426
503
  model,
427
504
  voice: nonEmptyTrimmed(voice),
@@ -438,8 +515,16 @@ export class ChatThreadSession extends ChangeEmitter {
438
515
  response,
439
516
  }));
440
517
  }
441
- async sendText({ messageId, text, attachments, steer = false, turnId, provider, model, voice, outputModalities, senderName, clientToolkits, }) {
518
+ async sendText({ messageId, text, attachments, steer = false, turnId, backend, provider, model, voice, outputModalities, senderName, clientToolkits, }) {
442
519
  const resolvedMessageId = messageId == null || messageId.trim() === "" ? uuidv4() : messageId.trim();
520
+ const currentModel = this.currentModelValue;
521
+ const resolvedBackend = nonEmptyTrimmed(backend) ?? currentModel?.backend;
522
+ const resolvedProvider = nonEmptyTrimmed(provider) ?? currentModel?.provider;
523
+ const resolvedModel = nonEmptyTrimmed(model) ?? currentModel?.model;
524
+ const resolvedVoice = nonEmptyTrimmed(voice) ?? currentModel?.voice;
525
+ const resolvedOutputModalities = outputModalities != null && outputModalities.length > 0
526
+ ? outputModalities
527
+ : currentModel?.outputModalities;
443
528
  const payload = steer
444
529
  ? new TurnSteer({
445
530
  threadId: this.threadPath,
@@ -453,21 +538,15 @@ export class ChatThreadSession extends ChangeEmitter {
453
538
  messageId: resolvedMessageId,
454
539
  senderName: nonEmptyTrimmed(senderName),
455
540
  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,
541
+ backend: resolvedBackend,
542
+ provider: resolvedProvider,
543
+ model: resolvedModel,
544
+ voice: resolvedVoice,
545
+ outputModalities: resolvedOutputModalities,
460
546
  clientToolkits: clientToolkits != null && clientToolkits.length > 0 ? clientToolkits : undefined,
461
547
  });
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
- }));
548
+ this.localAgentMessageIds.add(resolvedMessageId);
549
+ this.pendingLocalInputMessageIds.add(resolvedMessageId);
471
550
  this.addAgentMessage(new AgentMessageEvent({ message: payload }));
472
551
  try {
473
552
  await this.client.sendAgentMessage(payload);
@@ -511,16 +590,86 @@ export class ChatThreadSession extends ChangeEmitter {
511
590
  return this.client.sendAgentMessage(new RenameThread({ threadId: threadPath, name }));
512
591
  }
513
592
  addAgentMessage(event) {
514
- this._messages.push(event);
515
593
  const message = event.message;
516
594
  const type = message.type;
517
595
  this.lastAgentMessageType = type;
518
596
  this.lastAgentMessageAt = new Date();
597
+ this.updateSessionState(message);
598
+ this.updatePendingInputsForMessage(message);
599
+ this.appendRenderableMessage(event);
600
+ this.notifyListeners();
601
+ }
602
+ updateSessionState(message) {
603
+ if (message.type === agentTurnEndedType) {
604
+ const turnId = normalizedString(threadMessageValue(message, "turnId"));
605
+ if (turnId != null) {
606
+ this.lastCompletedTurnIdValue = turnId;
607
+ if (this.activeTurnIdValue === turnId) {
608
+ this.activeTurnIdValue = undefined;
609
+ }
610
+ }
611
+ const statusTurnId = normalizedString(this.threadStatusValue?.turnId);
612
+ if (statusTurnId == null || statusTurnId === turnId) {
613
+ this.threadStatusValue = undefined;
614
+ }
615
+ return;
616
+ }
617
+ if (message.type === agentTurnStartAcceptedType || message.type === agentTurnStartedType) {
618
+ const turnId = normalizedString(threadMessageValue(message, "turnId"));
619
+ if (turnId != null) {
620
+ this.activeTurnIdValue = turnId;
621
+ }
622
+ if (message.type === agentTurnStartedType) {
623
+ const statusTurnId = normalizedString(this.threadStatusValue?.turnId);
624
+ if (statusTurnId == null || statusTurnId === turnId) {
625
+ this.threadStatusValue = undefined;
626
+ }
627
+ }
628
+ return;
629
+ }
630
+ if (message.type === agentThreadStatusType && message instanceof AgentThreadStatus) {
631
+ this.threadStatusValue = message;
632
+ const mode = normalizedString(message.mode);
633
+ const status = normalizedString(message.status);
634
+ const turnId = normalizedString(message.turnId);
635
+ if (status != null && mode === "steerable" && turnId != null) {
636
+ this.activeTurnIdValue = turnId;
637
+ }
638
+ else if (status == null && turnId != null && this.activeTurnIdValue === turnId) {
639
+ this.activeTurnIdValue = undefined;
640
+ }
641
+ return;
642
+ }
643
+ if (message.type === agentModelChangedType && message instanceof AgentModelChanged) {
644
+ this.currentModelValue = message;
645
+ return;
646
+ }
647
+ if (message.type === agentModelsResponseType && message instanceof ModelsResponse) {
648
+ this.modelsResponseValue = message;
649
+ this.applyActiveModel(message);
650
+ return;
651
+ }
652
+ if (message.type === agentThreadLoadedType) {
653
+ this.setLoadState(new ChatThreadSessionLoadState({
654
+ phase: ChatThreadSessionLoadPhase.Loaded,
655
+ startedAt: this.loadStateValue.startedAt,
656
+ completedAt: this.lastAgentMessageAt,
657
+ requestMessageId: this.loadStateValue.requestMessageId,
658
+ sinceTurn: this.loadStateValue.sinceTurn,
659
+ lastMessageType: message.type,
660
+ lastMessageAt: this.lastAgentMessageAt,
661
+ }), { notify: false });
662
+ }
663
+ }
664
+ updatePendingInputsForMessage(message) {
665
+ const type = message.type;
519
666
  const messageId = message.messageId;
520
667
  const sourceMessageId = sourceMessageIdFor(message);
521
- if (type === agentTurnStartType || type === agentTurnSteerType) {
668
+ if (type === agentThreadStartType || type === agentTurnStartType || type === agentTurnSteerType) {
522
669
  const normalizedMessageId = messageId.trim();
523
670
  if (normalizedMessageId !== "") {
671
+ this.localAgentMessageIds.add(normalizedMessageId);
672
+ this.pendingLocalInputMessageIds.add(normalizedMessageId);
524
673
  this.markPending(new PendingAgentInput({
525
674
  messageId: normalizedMessageId,
526
675
  messageType: type,
@@ -532,37 +681,152 @@ export class ChatThreadSession extends ChangeEmitter {
532
681
  }));
533
682
  }
534
683
  }
684
+ else if (type === agentThreadStartRejectedType) {
685
+ if (sourceMessageId != null) {
686
+ this.pendingInputsById.delete(sourceMessageId);
687
+ }
688
+ }
535
689
  else if (type === agentTurnStartAcceptedType || type === agentTurnSteerAcceptedType) {
536
690
  this.updatePending(sourceMessageId, { awaitingAcceptance: false, awaitingApplication: true });
537
691
  }
538
692
  else if (type === agentTurnStartedType || type === agentTurnSteeredType) {
539
693
  if (sourceMessageId != null) {
540
694
  this.pendingInputsById.delete(sourceMessageId);
695
+ this.pendingLocalInputMessageIds.delete(sourceMessageId);
541
696
  }
542
697
  }
543
698
  else if (type === agentTurnStartRejectedType || type === agentTurnSteerRejectedType) {
544
699
  if (sourceMessageId != null) {
545
700
  this.pendingInputsById.delete(sourceMessageId);
701
+ this.pendingLocalInputMessageIds.delete(sourceMessageId);
546
702
  }
547
703
  }
548
704
  else if (type === agentTurnEndedType || type === agentThreadClearedType) {
549
705
  this.pendingInputsById.clear();
550
- if (message.type === agentTurnEndedType && "turnId" in message && typeof message.turnId === "string" && message.turnId.trim() !== "") {
551
- this.lastCompletedTurnIdValue = message.turnId.trim();
706
+ this.pendingLocalInputMessageIds.clear();
707
+ }
708
+ }
709
+ appendRenderableMessage(event) {
710
+ const message = event.message;
711
+ if (message.type === agentTurnEndedType) {
712
+ if (message.error != null) {
713
+ this.appendMessage(event);
552
714
  }
715
+ return;
553
716
  }
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 });
717
+ if (message.type === agentThreadStartType || message.type === agentTurnStartType || message.type === agentTurnSteerType) {
718
+ if (agentInputText(message.content).trim() !== "") {
719
+ this.appendMessage(event);
720
+ }
721
+ return;
722
+ }
723
+ if (message.type === agentTurnStartAcceptedType || message.type === agentTurnSteerAcceptedType) {
724
+ const sourceMessageId = normalizedString(sourceMessageIdFor(message));
725
+ if (sourceMessageId != null && this.localAgentMessageIds.has(sourceMessageId)) {
726
+ this.pendingLocalInputMessageIds.delete(sourceMessageId);
727
+ return;
728
+ }
729
+ if (agentInputText(message.content).trim() !== "") {
730
+ this.appendMessage(event, { beforePendingLocalInputs: true });
731
+ }
732
+ return;
733
+ }
734
+ if (isMergeableDelta(message)) {
735
+ this.appendOrMergeDelta(event);
736
+ return;
737
+ }
738
+ if (message.type === agentImageGenerationPartialType || message.type === agentImageGenerationCompletedType) {
739
+ const itemId = normalizedString(message.itemId);
740
+ if (itemId != null) {
741
+ this.upsertItemMessage(`image_generation:${itemId}`, event);
742
+ }
743
+ return;
744
+ }
745
+ if (message instanceof AgentClientToolCallRequested) {
746
+ this.appendMessage(event);
747
+ }
748
+ }
749
+ appendMessage(event, { beforePendingLocalInputs = false } = {}) {
750
+ const normalizedMessageId = normalizedString(event.message.messageId);
751
+ if (normalizedMessageId == null || this.messageIndexes.has(normalizedMessageId)) {
752
+ return;
753
+ }
754
+ if (beforePendingLocalInputs) {
755
+ const insertIndex = this._messages.findIndex((existing) => this.pendingLocalInputMessageIds.has(existing.message.messageId));
756
+ if (insertIndex >= 0) {
757
+ this._messages.splice(insertIndex, 0, event);
758
+ this.indexMessagesFrom(insertIndex);
759
+ return;
760
+ }
761
+ }
762
+ this.messageIndexes.set(normalizedMessageId, this._messages.length);
763
+ this._messages.push(event);
764
+ }
765
+ upsertItemMessage(key, event) {
766
+ const existingIndex = this.messageIndexes.get(key);
767
+ if (existingIndex == null) {
768
+ this.messageIndexes.set(key, this._messages.length);
769
+ this._messages.push(event);
770
+ return;
771
+ }
772
+ this._messages[existingIndex] = event;
773
+ }
774
+ appendOrMergeDelta(event) {
775
+ const message = event.message;
776
+ const normalizedMessageId = normalizedString(message.messageId);
777
+ if (normalizedMessageId != null) {
778
+ if (this.mergedDeltaMessageIds.has(normalizedMessageId)) {
779
+ return;
780
+ }
781
+ this.mergedDeltaMessageIds.add(normalizedMessageId);
782
+ }
783
+ const itemId = normalizedString(message.itemId);
784
+ if (itemId == null) {
785
+ return;
786
+ }
787
+ const key = `${message.type}:${itemId}`;
788
+ const existingIndex = this.messageIndexes.get(key);
789
+ if (existingIndex == null) {
790
+ this.messageIndexes.set(key, this._messages.length);
791
+ this._messages.push(event);
792
+ return;
793
+ }
794
+ const existing = this._messages[existingIndex].message;
795
+ const merged = mergeDeltaMessage(existing, message);
796
+ if (merged != null) {
797
+ this._messages[existingIndex] = new AgentMessageEvent({ message: merged, attachment: event.attachment });
798
+ }
799
+ }
800
+ indexMessagesFrom(start) {
801
+ for (let index = Math.max(0, start); index < this._messages.length; index += 1) {
802
+ const key = messageIndexKey(this._messages[index].message);
803
+ if (key != null) {
804
+ this.messageIndexes.set(key, index);
805
+ }
806
+ }
807
+ }
808
+ applyActiveModel(response) {
809
+ for (const provider of response.providers) {
810
+ for (const model of provider.models) {
811
+ if (!model.active) {
812
+ continue;
813
+ }
814
+ this.currentModelValue = new AgentModelChanged({
815
+ threadId: this.threadPath,
816
+ sourceMessageId: response.sourceMessageId,
817
+ backend: provider.backend,
818
+ provider: provider.name,
819
+ model: model.name,
820
+ voice: model.defaultOutputVoice,
821
+ inputFormat: model.inputFormat,
822
+ outputFormat: model.outputFormat,
823
+ turnDetection: model.turnDetection,
824
+ realtimeProtocols: model.realtimeProtocols,
825
+ outputModalities: model.modalities.length > 0 ? [model.modalities[0]] : ["text"],
826
+ });
827
+ return;
828
+ }
564
829
  }
565
- this.notifyListeners();
566
830
  }
567
831
  markClosed({ notify = true } = {}) {
568
832
  this.openState = false;
@@ -626,6 +890,74 @@ function sourceMessageIdFor(message) {
626
890
  const value = "sourceMessageId" in message ? message.sourceMessageId : undefined;
627
891
  return typeof value === "string" ? value : undefined;
628
892
  }
893
+ function threadMessageValue(message, key) {
894
+ const value = message[key];
895
+ return typeof value === "string" ? value : undefined;
896
+ }
897
+ function agentInputText(content) {
898
+ if (!Array.isArray(content)) {
899
+ return "";
900
+ }
901
+ return content
902
+ .filter((entry) => (typeof entry === "object" &&
903
+ entry !== null &&
904
+ "type" in entry &&
905
+ entry.type === "text" &&
906
+ "text" in entry &&
907
+ typeof entry.text === "string"))
908
+ .map((entry) => entry.text)
909
+ .join("");
910
+ }
911
+ function isMergeableDelta(message) {
912
+ return message.type === agentAudioGenerationDeltaType ||
913
+ message.type === agentAudioTranscriptionDeltaType ||
914
+ message.type === agentFileContentDeltaType ||
915
+ message.type === agentReasoningContentDeltaType ||
916
+ message.type === agentTextContentDeltaType ||
917
+ message.type === agentToolCallArgumentsDeltaType ||
918
+ message.type === agentToolCallLogDeltaType;
919
+ }
920
+ function messageIndexKey(message) {
921
+ if (isMergeableDelta(message)) {
922
+ const itemId = normalizedString(message.itemId);
923
+ return itemId == null ? undefined : `${message.type}:${itemId}`;
924
+ }
925
+ return normalizedString(message.messageId);
926
+ }
927
+ function mergeDeltaMessage(existing, incoming) {
928
+ if (existing.type !== incoming.type) {
929
+ return undefined;
930
+ }
931
+ if (existing.type === agentAudioGenerationDeltaType && existing instanceof AgentAudioGenerationDelta && incoming instanceof AgentAudioGenerationDelta) {
932
+ const data = new Uint8Array(existing.data.length + incoming.data.length);
933
+ data.set(existing.data, 0);
934
+ data.set(incoming.data, existing.data.length);
935
+ return new AgentAudioGenerationDelta({ ...existing.toJson(), data });
936
+ }
937
+ if (existing.type === agentAudioTranscriptionDeltaType && existing instanceof AgentAudioTranscriptionDelta && incoming instanceof AgentAudioTranscriptionDelta) {
938
+ return new AgentAudioTranscriptionDelta({ ...existing.toJson(), text: `${existing.text}${incoming.text}` });
939
+ }
940
+ if (existing.type === agentFileContentDeltaType && incoming instanceof AgentFileContentDelta) {
941
+ return incoming;
942
+ }
943
+ if (existing.type === agentReasoningContentDeltaType && existing instanceof AgentReasoningContentDelta && incoming instanceof AgentReasoningContentDelta) {
944
+ return new AgentReasoningContentDelta({ ...existing.toJson(), text: `${existing.text}${incoming.text}` });
945
+ }
946
+ if (existing.type === agentTextContentDeltaType && existing instanceof AgentTextContentDelta && incoming instanceof AgentTextContentDelta) {
947
+ return new AgentTextContentDelta({ ...existing.toJson(), text: `${existing.text}${incoming.text}` });
948
+ }
949
+ if (existing.type === agentToolCallArgumentsDeltaType && existing instanceof AgentToolCallArgumentsDelta && incoming instanceof AgentToolCallArgumentsDelta) {
950
+ return new AgentToolCallArgumentsDelta({ ...existing.toJson(), delta: `${existing.delta}${incoming.delta}` });
951
+ }
952
+ if (existing.type === agentToolCallLogDeltaType && existing instanceof AgentToolCallLogDelta && incoming instanceof AgentToolCallLogDelta) {
953
+ return new AgentToolCallLogDelta({ ...existing.toJson(), lines: [...existing.lines, ...incoming.lines] });
954
+ }
955
+ return undefined;
956
+ }
957
+ function normalizedString(value) {
958
+ const normalized = value?.trim();
959
+ return normalized == null || normalized === "" ? undefined : normalized;
960
+ }
629
961
  function nonEmptyTrimmed(value) {
630
962
  const normalized = value?.trim();
631
963
  return normalized == null || normalized === "" ? undefined : normalized;