@amaster.ai/dsh-a2a 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/inbox.js ADDED
@@ -0,0 +1,826 @@
1
+ /** Native DSH input ownership with standard A2A task delivery. */
2
+ import { randomUUID } from 'node:crypto';
3
+ import { Message, TaskState } from '@a2a-js/sdk';
4
+ import { AgentEvent, ServerCallContext, STATE_HEADERS_KEY, } from '@a2a-js/sdk/server';
5
+ import { createUserMessage } from '@deepseek-ai/dsh-llm/message';
6
+ import { agentTextMessage, SessionTranslator, terminalStatusUpdate } from './translator.js';
7
+ export const INBOX_EXTENSION = 'https://github.com/TGYD-helige/dsh-plugins/blob/main/docs/protocol/inbox-v1.md';
8
+ export class NativeInbox {
9
+ ctx;
10
+ bridge;
11
+ store;
12
+ contexts = new Map();
13
+ resolving = new Map();
14
+ stopping = false;
15
+ shutdownPromise;
16
+ preadmitted = new WeakMap();
17
+ server;
18
+ constructor(ctx, bridge, store) {
19
+ this.ctx = ctx;
20
+ this.bridge = bridge;
21
+ this.store = store;
22
+ bridge.inbox = this;
23
+ ctx.on('agent/pre-step', async (detail, next) => {
24
+ await this.prepare(detail);
25
+ return next();
26
+ });
27
+ ctx.on('agent/inbox/claimed', ({ agent, message, turn }) => {
28
+ const run = this.contexts.get(agent.session.id);
29
+ if (!run || message.source.kind !== 'a2a')
30
+ return;
31
+ const task = run.tasks.get(message.source.taskId);
32
+ if (task && !task.settled) {
33
+ run.claimed.add(task);
34
+ if (!run.active)
35
+ this.select(run, task);
36
+ if (run.active === task) {
37
+ const selectedAt = Date.now();
38
+ task.selectedAt.set(message.id, selectedAt);
39
+ this.observeInput('selected', run, message, { selectedAt, turn });
40
+ }
41
+ }
42
+ });
43
+ }
44
+ content(message, contextId) {
45
+ return this.bridge.buildContent(message, contextId);
46
+ }
47
+ async resolve(contextId) {
48
+ return (await this.context(contextId)).entry.handle.agent;
49
+ }
50
+ async enqueue(taskId, message, options = {}) {
51
+ if (this.stopping)
52
+ throw new Error('Native inbox is unloading.');
53
+ if (!message.messageId)
54
+ throw new Error('message.messageId is required.');
55
+ for (const run of this.contexts.values()) {
56
+ const task = run.tasks.get(taskId);
57
+ if (!task || task.settled)
58
+ continue;
59
+ const admitted = { ...message, taskId, contextId: run.entry.sessionId };
60
+ task.pendingRequests++;
61
+ try {
62
+ await this.admit(run, task, admitted, task.context, options.target ?? 'next-turn', options.wakeup !== false);
63
+ }
64
+ finally {
65
+ task.pendingRequests--;
66
+ this.settle(run, task);
67
+ }
68
+ return;
69
+ }
70
+ throw new Error('Host input must address a live A2A task.');
71
+ }
72
+ context(contextId, context = new ServerCallContext()) {
73
+ if (this.stopping)
74
+ return Promise.reject(new Error('Native inbox is unloading.'));
75
+ const existing = this.contexts.get(contextId);
76
+ if (existing)
77
+ return Promise.resolve(existing);
78
+ let creating = this.resolving.get(contextId);
79
+ if (!creating) {
80
+ creating = (async () => {
81
+ const { entry } = await this.bridge.ensureTask(randomUUID(), contextId);
82
+ const run = {
83
+ entry,
84
+ tasks: new Map(),
85
+ claimed: new Set(),
86
+ admissions: new Set(),
87
+ admissionTail: Promise.resolve(),
88
+ };
89
+ const snapshots = new Map();
90
+ for (const message of this.pending(run)) {
91
+ if (message.source.kind !== 'a2a' || snapshots.has(message.source.taskId))
92
+ continue;
93
+ const snapshot = await this.store.load(message.source.taskId, context);
94
+ if (!snapshot || snapshot.contextId !== contextId)
95
+ throw new Error('Queued A2A task is not accessible.');
96
+ if (this.terminal(snapshot))
97
+ entry.handle.agent.inbox.remove(message.id);
98
+ else
99
+ snapshots.set(snapshot.id, snapshot);
100
+ }
101
+ await this.flush(entry.handle.agent.session);
102
+ for (const snapshot of snapshots.values())
103
+ this.restoreTask(run, snapshot, context);
104
+ this.contexts.set(contextId, run);
105
+ this.observe('resolved', run);
106
+ for (const message of this.pending(run))
107
+ this.observeInput('restored', run, message);
108
+ return run;
109
+ })().finally(() => this.resolving.delete(contextId));
110
+ this.resolving.set(contextId, creating);
111
+ }
112
+ return creating;
113
+ }
114
+ observe(phase, run, fields = {}) {
115
+ try {
116
+ const pending = this.pending(run);
117
+ void this.ctx
118
+ .parallel('a2a/inbox', {
119
+ phase,
120
+ contextId: run.entry.sessionId,
121
+ at: Date.now(),
122
+ pending: pending.length,
123
+ agentStatus: run.entry.handle.agent.status,
124
+ taskIds: [
125
+ ...new Set([...run.tasks.values()]
126
+ .filter((task) => !task.settled)
127
+ .map((task) => task.task.id)
128
+ .concat(pending.flatMap((message) => message.source.kind === 'a2a' ? [message.source.taskId] : []))),
129
+ ],
130
+ ...fields,
131
+ })
132
+ .catch((error) => console.error('[dsh-a2a] inbox observer failed:', error));
133
+ }
134
+ catch (error) {
135
+ console.error('[dsh-a2a] inbox observer failed:', error);
136
+ }
137
+ }
138
+ observeInput(phase, run, message, fields = {}) {
139
+ try {
140
+ if (message.source.kind !== 'a2a')
141
+ return;
142
+ const original = Message.fromJSON(JSON.parse(message.source.message));
143
+ const position = this.pending(run).findIndex((input) => input.id === message.id);
144
+ this.observe(phase, run, {
145
+ target: message.source.target,
146
+ input: original.parts
147
+ .flatMap((part) => (part.content?.$case === 'text' ? [part.content.value] : []))
148
+ .join('\n'),
149
+ position: position < 0 ? undefined : position + 1,
150
+ taskId: message.source.taskId,
151
+ messageId: original.messageId,
152
+ referenceTaskIds: original.referenceTaskIds,
153
+ nativeMessageId: message.id,
154
+ queuedAt: message.source.queuedAt,
155
+ ...fields,
156
+ });
157
+ }
158
+ catch (error) {
159
+ console.error('[dsh-a2a] inbox observer failed:', error);
160
+ }
161
+ }
162
+ flush(session) {
163
+ return this.ctx.parallel('session/flush', session);
164
+ }
165
+ checkpoint(task, context) {
166
+ return this.store.checkpoint
167
+ ? this.store.checkpoint(task, context)
168
+ : this.store.save(task, context);
169
+ }
170
+ pending(run) {
171
+ return [...run.entry.handle.agent.inbox.nextStep, ...run.entry.handle.agent.inbox.nextTurn];
172
+ }
173
+ task(task, bus, context) {
174
+ let complete;
175
+ let finished = false;
176
+ const selectedAt = new Map();
177
+ const done = new Promise((resolve) => {
178
+ complete = resolve;
179
+ });
180
+ const finish = () => {
181
+ if (finished)
182
+ return;
183
+ finished = true;
184
+ const phase = this.outcome(task);
185
+ const run = this.contexts.get(task.contextId);
186
+ if (phase && run)
187
+ this.observe(phase, run, { taskId: task.id });
188
+ selectedAt.clear();
189
+ complete();
190
+ };
191
+ return {
192
+ task,
193
+ translator: new SessionTranslator(task.id, task.contextId),
194
+ bus,
195
+ done,
196
+ finish,
197
+ settled: false,
198
+ headers: new Map(),
199
+ selectedAt,
200
+ controller: new AbortController(),
201
+ pendingRequests: 0,
202
+ context: new ServerCallContext({ tenant: context.tenant, user: context.user }),
203
+ };
204
+ }
205
+ controls(message, context) {
206
+ const value = message.metadata?.[INBOX_EXTENSION];
207
+ if (value === undefined)
208
+ return { target: 'next-turn', wakeup: true };
209
+ if (!context.requestedExtensions?.includes(INBOX_EXTENSION))
210
+ throw new Error('Inbox controls require A2A-Extensions negotiation.');
211
+ if (!value ||
212
+ typeof value !== 'object' ||
213
+ Array.isArray(value) ||
214
+ Object.keys(value).some((key) => key !== 'target' && key !== 'wakeup'))
215
+ throw new Error('Invalid inbox controls.');
216
+ if (value.target !== undefined && value.target !== 'next-step' && value.target !== 'next-turn')
217
+ throw new Error('Invalid inbox target.');
218
+ if (value.wakeup !== undefined && typeof value.wakeup !== 'boolean')
219
+ throw new Error('Invalid inbox wakeup.');
220
+ context.addActivatedExtension(INBOX_EXTENSION);
221
+ return {
222
+ target: value.target === 'next-step' ? 'next-step' : 'next-turn',
223
+ wakeup: value.wakeup !== false,
224
+ };
225
+ }
226
+ /** Reject follow-up admission at the request boundary, without terminating accepted work. */
227
+ async beforeMessage(message, context) {
228
+ if (!message.messageId)
229
+ throw new Error('message.messageId is required.');
230
+ const controls = this.controls(message, context);
231
+ if (!message.taskId) {
232
+ if (controls.target === 'next-step')
233
+ throw new Error('Steering must address an active task.');
234
+ return;
235
+ }
236
+ const snapshot = await this.store.load(message.taskId, context);
237
+ if (!snapshot ||
238
+ (message.contextId && snapshot.contextId !== message.contextId) ||
239
+ this.terminal(snapshot))
240
+ return;
241
+ const run = await this.context(snapshot.contextId, context);
242
+ let task = run.tasks.get(snapshot.id);
243
+ if (!task)
244
+ task = this.restoreTask(run, snapshot, context);
245
+ task.pendingRequests++;
246
+ try {
247
+ await this.admit(run, task, message, context, controls.target, controls.wakeup);
248
+ }
249
+ catch (error) {
250
+ task.pendingRequests--;
251
+ this.settle(run, task);
252
+ throw error;
253
+ }
254
+ let admitted = this.preadmitted.get(context);
255
+ if (!admitted) {
256
+ admitted = new Set();
257
+ this.preadmitted.set(context, admitted);
258
+ }
259
+ admitted.add(message.messageId);
260
+ return () => {
261
+ if (!admitted.delete(message.messageId))
262
+ return;
263
+ task.pendingRequests--;
264
+ this.settle(run, task);
265
+ };
266
+ }
267
+ /** Reattach a cold task before the SDK decides whether cancellation has a live executor. */
268
+ async beforeTask(taskId, context) {
269
+ const snapshot = await this.store.load(taskId, context);
270
+ if (!snapshot || this.terminal(snapshot))
271
+ return;
272
+ const run = await this.context(snapshot.contextId, context);
273
+ const task = run.tasks.get(taskId) ?? this.restoreTask(run, snapshot, context);
274
+ if (task.settled) {
275
+ await task.done;
276
+ await this.checkpoint(structuredClone(task.task), task.context);
277
+ }
278
+ }
279
+ outcome(task) {
280
+ if (task.status?.state === TaskState.TASK_STATE_COMPLETED)
281
+ return 'completed';
282
+ if (task.status?.state === TaskState.TASK_STATE_CANCELED)
283
+ return 'canceled';
284
+ if (task.status?.state === TaskState.TASK_STATE_FAILED ||
285
+ task.status?.state === TaskState.TASK_STATE_REJECTED)
286
+ return 'failed';
287
+ }
288
+ terminal(task) {
289
+ return (!!task.status &&
290
+ [
291
+ TaskState.TASK_STATE_COMPLETED,
292
+ TaskState.TASK_STATE_CANCELED,
293
+ TaskState.TASK_STATE_FAILED,
294
+ TaskState.TASK_STATE_REJECTED,
295
+ ].includes(task.status.state));
296
+ }
297
+ async admit(run, task, a2aMessage, context, target, wakeup, accepted) {
298
+ if (target === 'next-step' && run.active !== task)
299
+ throw new Error('Steering must address an active task.');
300
+ const duplicate = () => [...run.tasks.values()].some((owned) => owned.task.history.some((message) => message.messageId === a2aMessage.messageId));
301
+ if (duplicate())
302
+ throw new Error('This message is already queued.');
303
+ const abort = new AbortController();
304
+ run.admissions.add(abort);
305
+ const onTaskAbort = () => abort.abort();
306
+ task.controller.signal.addEventListener('abort', onTaskAbort, { once: true });
307
+ if (task.controller.signal.aborted)
308
+ abort.abort();
309
+ const headers = context.state.get(STATE_HEADERS_KEY);
310
+ const work = run.admissionTail.then(async () => {
311
+ abort.signal.throwIfAborted();
312
+ const content = await this.content(a2aMessage, run.entry.sessionId);
313
+ abort.signal.throwIfAborted();
314
+ const message = createUserMessage({
315
+ content,
316
+ source: {
317
+ kind: 'a2a',
318
+ taskId: task.task.id,
319
+ message: JSON.stringify(Message.toJSON(a2aMessage)),
320
+ target,
321
+ queuedAt: Date.now(),
322
+ },
323
+ });
324
+ await this.ctx.serial('a2a/before-followup', {
325
+ agent: run.entry.handle.agent,
326
+ contextId: run.entry.sessionId,
327
+ taskId: task.task.id,
328
+ a2aMessage,
329
+ dshMessageId: message.id,
330
+ requestHeaders: headers,
331
+ signal: abort.signal,
332
+ });
333
+ abort.signal.throwIfAborted();
334
+ if (this.stopping || this.bridge.isClearing(run.entry.sessionId) || task.settled)
335
+ throw new Error('Context or task is unavailable.');
336
+ if (duplicate())
337
+ throw new Error('This message is already queued.');
338
+ const saved = await this.store.load(task.task.id, context);
339
+ abort.signal.throwIfAborted();
340
+ const history = saved?.history ?? task.task.history;
341
+ const acceptedHistory = history.some((item) => item.messageId === a2aMessage.messageId)
342
+ ? history
343
+ : [...history, a2aMessage];
344
+ await this.checkpoint({ ...task.task, history: acceptedHistory }, context);
345
+ abort.signal.throwIfAborted();
346
+ task.task.history = acceptedHistory;
347
+ task.headers.set(message.id, headers);
348
+ accepted?.();
349
+ run.entry.handle.agent.send(message, target, wakeup);
350
+ this.observeInput('admitted', run, message);
351
+ await this.flush(run.entry.handle.agent.session);
352
+ void this.ctx
353
+ .parallel('a2a/message-admitted', {
354
+ contextId: run.entry.sessionId,
355
+ taskId: task.task.id,
356
+ a2aMessageId: a2aMessage.messageId,
357
+ dshMessageId: message.id,
358
+ requestHeaders: headers,
359
+ })
360
+ .catch((error) => console.error('[dsh-a2a] message-admitted listener failed:', error));
361
+ });
362
+ let onAbort;
363
+ const admitted = Promise.race([
364
+ work,
365
+ new Promise((_resolve, reject) => {
366
+ onAbort = () => reject(new Error('Inbox admission interrupted.'));
367
+ abort.signal.addEventListener('abort', onAbort, { once: true });
368
+ if (abort.signal.aborted)
369
+ onAbort();
370
+ }),
371
+ ]);
372
+ run.admissionTail = admitted.catch(() => { });
373
+ try {
374
+ await admitted;
375
+ }
376
+ finally {
377
+ abort.signal.removeEventListener('abort', onAbort);
378
+ run.admissions.delete(abort);
379
+ task.controller.signal.removeEventListener('abort', onTaskAbort);
380
+ }
381
+ }
382
+ async execute(request, bus) {
383
+ const { userMessage, taskId, contextId, context } = request;
384
+ let task;
385
+ let ownsTask = false;
386
+ try {
387
+ const controls = this.controls(userMessage, context);
388
+ const run = await this.context(contextId, context);
389
+ task = run.tasks.get(taskId);
390
+ if (!task) {
391
+ task = this.task({
392
+ id: taskId,
393
+ contextId,
394
+ status: {
395
+ state: TaskState.TASK_STATE_SUBMITTED,
396
+ timestamp: new Date().toISOString(),
397
+ message: undefined,
398
+ },
399
+ history: [],
400
+ artifacts: [],
401
+ metadata: undefined,
402
+ }, bus, context);
403
+ run.tasks.set(taskId, task);
404
+ ownsTask = true;
405
+ }
406
+ const preadmitted = this.preadmitted.get(context)?.delete(userMessage.messageId);
407
+ if (!preadmitted)
408
+ await this.admit(run, task, userMessage, context, controls.target, controls.wakeup, () => bus.publish(AgentEvent.task(structuredClone(task.task))));
409
+ else {
410
+ task.pendingRequests--;
411
+ bus.publish(AgentEvent.task(structuredClone(task.task)));
412
+ }
413
+ this.settle(run, task);
414
+ await task.done;
415
+ if (task.bus !== bus && task.final)
416
+ bus.publish(task.final);
417
+ }
418
+ catch (error) {
419
+ if (task?.settled)
420
+ return;
421
+ if (task && !ownsTask) {
422
+ bus.publish(AgentEvent.task(structuredClone(task.task)));
423
+ return;
424
+ }
425
+ const text = error instanceof Error ? error.message : String(error);
426
+ const snapshot = {
427
+ id: taskId,
428
+ contextId,
429
+ status: {
430
+ state: TaskState.TASK_STATE_FAILED,
431
+ message: agentTextMessage(taskId, contextId, text),
432
+ timestamp: new Date().toISOString(),
433
+ },
434
+ history: task?.task.history ?? [],
435
+ artifacts: [],
436
+ metadata: undefined,
437
+ };
438
+ bus.publish(AgentEvent.task(snapshot));
439
+ if (task) {
440
+ this.fail(task, contextId, error);
441
+ await task.done;
442
+ }
443
+ else
444
+ bus.publish(terminalStatusUpdate(taskId, contextId, 'failed', text));
445
+ }
446
+ finally {
447
+ bus.finished();
448
+ }
449
+ }
450
+ restoreTask(run, snapshot, context) {
451
+ if (!this.server)
452
+ throw new Error('A2A task recovery requires the running server.');
453
+ const bus = this.server.restoreTask(snapshot, context);
454
+ const restored = this.task(snapshot, bus, context);
455
+ run.tasks.set(snapshot.id, restored);
456
+ void restored.done.finally(() => bus.finished());
457
+ bus.publish(AgentEvent.task(snapshot));
458
+ if (snapshot.status?.state !== TaskState.TASK_STATE_INPUT_REQUIRED &&
459
+ !this.pending(run).some((message) => message.source.kind === 'a2a' && message.source.taskId === snapshot.id))
460
+ this.fail(restored, snapshot.contextId, new Error('Task execution was interrupted; DSH has no pending input to resume.'));
461
+ return restored;
462
+ }
463
+ select(run, task) {
464
+ run.active = task;
465
+ this.bridge.bindInboxTask(run.entry, task.task.id, task.translator, task.bus, task.done, task.controller);
466
+ if (run.turnStart) {
467
+ this.publish(task, run.turnStart);
468
+ run.turnStart = undefined;
469
+ }
470
+ }
471
+ async prepare(detail) {
472
+ const run = this.contexts.get(detail.agent.session.id);
473
+ if (!run)
474
+ return;
475
+ if (this.stopping)
476
+ throw new Error('Inbox is unloading.');
477
+ if (!run.active && detail.messages.some((message) => message.source.kind !== 'a2a'))
478
+ throw new Error('Native host input requires task-attributed a2aInbox.enqueue().');
479
+ // Restarted steering and an ordinary queued turn may be claimed together by the core.
480
+ // Requeue other owners before committing any user message, keeping task lifecycles separate.
481
+ const owner = detail.messages.find((message) => message.source.kind === 'a2a');
482
+ if (owner?.source.kind === 'a2a') {
483
+ const taskId = owner.source.taskId;
484
+ const deferred = detail.messages.filter((message) => message.source.kind === 'a2a' && message.source.taskId !== taskId);
485
+ for (const message of deferred.slice().reverse()) {
486
+ detail.agent.inbox.prepend('next-turn', message);
487
+ if (message.source.kind === 'a2a') {
488
+ const task = run.tasks.get(message.source.taskId);
489
+ if (task)
490
+ run.claimed.delete(task);
491
+ }
492
+ }
493
+ for (let index = detail.messages.length - 1; index >= 0; index--)
494
+ if (deferred.includes(detail.messages[index]))
495
+ detail.messages.splice(index, 1);
496
+ }
497
+ for (const message of detail.messages) {
498
+ if (message.source.kind !== 'a2a')
499
+ continue;
500
+ const task = run.tasks.get(message.source.taskId);
501
+ if (!task || task.settled)
502
+ throw new Error('Queued input has no live A2A task; restore it before waking.');
503
+ if (run.active && run.active !== task)
504
+ throw new Error('A model step cannot consume inputs from different A2A tasks.');
505
+ this.select(run, task);
506
+ const selectedAt = task.selectedAt.get(message.id) ?? Date.now();
507
+ const timing = { selectedAt, turn: detail.turn, step: detail.step };
508
+ try {
509
+ await this.flush(detail.agent.session);
510
+ await this.ctx.serial('a2a/before-step', {
511
+ agent: detail.agent,
512
+ contextId: detail.agent.session.id,
513
+ taskId: task.task.id,
514
+ a2aMessage: Message.fromJSON(JSON.parse(message.source.message)),
515
+ dshMessageId: message.id,
516
+ requestHeaders: task.headers.get(message.id),
517
+ signal: detail.signal,
518
+ });
519
+ detail.signal.throwIfAborted();
520
+ }
521
+ catch (error) {
522
+ this.observeInput(detail.signal.aborted ? 'prepare-aborted' : 'prepare-failed', run, message, timing);
523
+ throw error;
524
+ }
525
+ this.observeInput('prepared', run, message, timing);
526
+ }
527
+ }
528
+ publish(task, event) {
529
+ for (const output of task.translator.handle(event)) {
530
+ if (output.kind === 'statusUpdate' && output.data.status)
531
+ task.task.status = output.data.status;
532
+ task.bus.publish(output);
533
+ }
534
+ }
535
+ onSessionEvent(session, event) {
536
+ const run = this.contexts.get(session.id);
537
+ if (!run)
538
+ return false;
539
+ if (event.type === 'turn/start') {
540
+ run.claimed.clear();
541
+ run.turnStart = event;
542
+ return true;
543
+ }
544
+ const task = run.active;
545
+ if (event.type === 'turn/end') {
546
+ if (!this.stopping && ['error', 'blocked', 'aborted'].includes(event.data.reason.kind))
547
+ for (const owner of run.claimed)
548
+ if (owner !== task && !owner.settled)
549
+ this.fail(owner, session.id, new Error('Selected input was interrupted before its task step.'));
550
+ run.claimed.clear();
551
+ }
552
+ if (!task)
553
+ return true;
554
+ if (task.settled) {
555
+ if (event.type === 'turn/end')
556
+ run.active = undefined;
557
+ return true;
558
+ }
559
+ try {
560
+ if (event.type !== 'turn/end') {
561
+ this.publish(task, event);
562
+ return true;
563
+ }
564
+ run.active = undefined;
565
+ if (this.stopping)
566
+ return true;
567
+ const outputs = task.translator.handle(event);
568
+ task.final = outputs.at(-1);
569
+ if (task.final?.kind === 'statusUpdate' &&
570
+ [TaskState.TASK_STATE_FAILED, TaskState.TASK_STATE_CANCELED].includes(task.final.data.status.state)) {
571
+ // Official inbox edits append session events; wait until turn/end publication exits.
572
+ void Promise.resolve()
573
+ .then(() => this.settle(run, task))
574
+ .catch((error) => this.fail(task, session.id, error));
575
+ return true;
576
+ }
577
+ if (!task.settlementStarted) {
578
+ task.settlementStarted = true;
579
+ void Promise.resolve(this.ctx.waterfall('a2a/task-settlement', {
580
+ agent: run.entry.handle.agent,
581
+ contextId: session.id,
582
+ taskId: task.task.id,
583
+ reason: event.data.reason,
584
+ signal: task.controller.signal,
585
+ }, () => { }))
586
+ .then(() => {
587
+ task.settlementReady = true;
588
+ this.settle(run, task);
589
+ })
590
+ .catch((error) => this.fail(task, session.id, error));
591
+ }
592
+ else
593
+ this.settle(run, task);
594
+ }
595
+ catch (error) {
596
+ this.fail(task, session.id, error);
597
+ }
598
+ return true;
599
+ }
600
+ settle(run, task) {
601
+ const terminal = task.final?.kind === 'statusUpdate' &&
602
+ [TaskState.TASK_STATE_FAILED, TaskState.TASK_STATE_CANCELED].includes(task.final.data.status.state);
603
+ if (task.settled || this.stopping)
604
+ return;
605
+ if (terminal) {
606
+ this.discard(run, task.task.id);
607
+ }
608
+ else if (!task.settlementReady ||
609
+ task.pendingRequests ||
610
+ run.active === task ||
611
+ this.pending(run).some((message) => message.source.kind === 'a2a' && message.source.taskId === task.task.id))
612
+ return;
613
+ const output = task.final;
614
+ if (output?.kind === 'statusUpdate' &&
615
+ output.data.status?.state === TaskState.TASK_STATE_INPUT_REQUIRED)
616
+ output.data.status.state = TaskState.TASK_STATE_COMPLETED;
617
+ if (output?.kind === 'statusUpdate')
618
+ task.task.status = output.data.status;
619
+ if (output)
620
+ task.bus.publish(output);
621
+ task.settled = true;
622
+ task.controller.abort();
623
+ task.headers.clear();
624
+ task.finish();
625
+ }
626
+ onAssistantStream(agent, frame) {
627
+ const run = this.contexts.get(agent.session.id);
628
+ if (!run)
629
+ return false;
630
+ const task = run.active;
631
+ if (task && !task.settled) {
632
+ try {
633
+ for (const output of task.translator.handleStreamFrame(frame))
634
+ task.bus.publish(output);
635
+ }
636
+ catch (error) {
637
+ console.error('[dsh-a2a] failed to translate inbox stream:', error);
638
+ }
639
+ }
640
+ return true;
641
+ }
642
+ fail(task, contextId, error) {
643
+ console.error('[dsh-a2a] native inbox failed:', error);
644
+ if (task.settled)
645
+ return;
646
+ task.settled = true;
647
+ task.controller.abort();
648
+ task.final = terminalStatusUpdate(task.task.id, contextId, 'failed', error instanceof Error ? error.message : String(error));
649
+ if (task.final.kind === 'statusUpdate')
650
+ task.task.status = task.final.data.status;
651
+ // Cleanup may append inbox events, so it must run outside session publication.
652
+ queueMicrotask(() => {
653
+ try {
654
+ const run = this.contexts.get(contextId);
655
+ if (run) {
656
+ this.discard(run, task.task.id);
657
+ if (run.active === task)
658
+ run.entry.handle.agent.cancel({ kind: 'user' }, { keepInbox: true });
659
+ }
660
+ }
661
+ catch (cleanupError) {
662
+ console.error('[dsh-a2a] failed task cleanup failed:', cleanupError);
663
+ }
664
+ finally {
665
+ task.headers.clear();
666
+ task.bus.publish(task.final);
667
+ task.finish();
668
+ }
669
+ });
670
+ }
671
+ discard(run, taskId) {
672
+ for (const message of this.pending(run))
673
+ if (message.source.kind === 'a2a' && message.source.taskId === taskId)
674
+ run.entry.handle.agent.inbox.remove(message.id);
675
+ }
676
+ async cancel(taskId) {
677
+ if (this.stopping)
678
+ throw new Error('Native inbox is unloading.');
679
+ for (const [contextId, run] of this.contexts) {
680
+ if (!run.tasks.has(taskId))
681
+ continue;
682
+ const task = run.tasks.get(taskId);
683
+ if (!task)
684
+ continue;
685
+ if (this.stopping && !task.cancellation)
686
+ throw new Error('Native inbox is unloading.');
687
+ if (task.settled && task.task.status?.state !== TaskState.TASK_STATE_CANCELED)
688
+ return contextId;
689
+ let operation = task.cancellation;
690
+ if (!operation) {
691
+ operation = this.cancelTask(run, task);
692
+ task.cancellation = operation;
693
+ void operation.catch(() => {
694
+ if (task.cancellation === operation)
695
+ task.cancellation = undefined;
696
+ });
697
+ }
698
+ await operation;
699
+ return contextId;
700
+ }
701
+ return undefined;
702
+ }
703
+ async cancelTask(run, task) {
704
+ const contextId = run.entry.sessionId;
705
+ const taskId = task.task.id;
706
+ this.discard(run, taskId);
707
+ task.controller.abort();
708
+ task.settled = true;
709
+ task.final = terminalStatusUpdate(taskId, contextId, 'canceled', 'Task canceled by request.');
710
+ if (task.final.kind === 'statusUpdate')
711
+ task.task.status = task.final.data.status;
712
+ if (run.active === task) {
713
+ run.entry.handle.agent.cancel({ kind: 'user' }, { keepInbox: true });
714
+ }
715
+ try {
716
+ try {
717
+ await this.flush(run.entry.handle.agent.session);
718
+ }
719
+ finally {
720
+ await this.checkpoint(structuredClone(task.task), task.context);
721
+ }
722
+ }
723
+ finally {
724
+ task.bus.publish(task.final);
725
+ task.finish();
726
+ }
727
+ }
728
+ async clear(contextId) {
729
+ await this.resolving.get(contextId);
730
+ const run = this.contexts.get(contextId);
731
+ if (!run)
732
+ return;
733
+ const taskIds = [
734
+ ...new Set([...run.tasks.values()]
735
+ .filter((task) => !task.settled)
736
+ .map((task) => task.task.id)
737
+ .concat(this.pending(run).flatMap((message) => message.source.kind === 'a2a' ? [message.source.taskId] : []))),
738
+ ];
739
+ for (const admission of run.admissions)
740
+ admission.abort();
741
+ run.entry.handle.agent.inbox.clear();
742
+ run.entry.handle.agent.cancel({ kind: 'user' });
743
+ await run.entry.handle.agent.whenIdle();
744
+ this.observe('cleared', run, { taskIds });
745
+ for (const task of run.tasks.values()) {
746
+ if (!task.settled)
747
+ task.bus.publish(terminalStatusUpdate(task.task.id, contextId, 'canceled', 'Context cleared.'));
748
+ task.settled = true;
749
+ task.controller.abort();
750
+ task.finish();
751
+ }
752
+ await this.flush(run.entry.handle.agent.session);
753
+ this.contexts.delete(contextId);
754
+ }
755
+ onDisposed(session) {
756
+ const run = this.contexts.get(session.id);
757
+ if (!run)
758
+ return;
759
+ for (const admission of run.admissions)
760
+ admission.abort();
761
+ for (const task of run.tasks.values()) {
762
+ if (!task.settled) {
763
+ const final = terminalStatusUpdate(task.task.id, session.id, 'canceled', 'Agent disposed.');
764
+ if (final.kind === 'statusUpdate')
765
+ task.task.status = final.data.status;
766
+ task.bus.publish(final);
767
+ }
768
+ task.settled = true;
769
+ task.controller.abort();
770
+ task.finish();
771
+ }
772
+ this.observe('disposed', run);
773
+ this.contexts.delete(session.id);
774
+ }
775
+ shutdown() {
776
+ if (this.shutdownPromise)
777
+ return this.shutdownPromise;
778
+ this.stopping = true;
779
+ this.shutdownPromise = (async () => {
780
+ const failures = [];
781
+ await Promise.allSettled([...this.resolving.values()]);
782
+ const cancellations = [...this.contexts.values()].flatMap((run) => [...run.tasks.values()].flatMap((task) => (task.cancellation ? [task.cancellation] : [])));
783
+ for (const outcome of await Promise.allSettled(cancellations))
784
+ if (outcome.status === 'rejected')
785
+ failures.push(outcome.reason);
786
+ for (const run of this.contexts.values()) {
787
+ for (const admission of run.admissions)
788
+ admission.abort();
789
+ try {
790
+ run.entry.handle.agent.cancel({ kind: 'disposed' });
791
+ }
792
+ catch (error) {
793
+ failures.push(error);
794
+ }
795
+ try {
796
+ await run.entry.handle.agent.whenIdle();
797
+ }
798
+ catch (error) {
799
+ failures.push(error);
800
+ }
801
+ try {
802
+ await this.flush(run.entry.handle.agent.session);
803
+ }
804
+ catch (error) {
805
+ failures.push(error);
806
+ }
807
+ finally {
808
+ for (const task of run.tasks.values()) {
809
+ try {
810
+ if (!task.settled)
811
+ await this.cancelTask(run, task);
812
+ }
813
+ catch (error) {
814
+ failures.push(error);
815
+ }
816
+ task.finish();
817
+ }
818
+ this.observe('disposed', run);
819
+ }
820
+ }
821
+ if (failures.length)
822
+ throw new AggregateError(failures, 'Native inbox shutdown failed.');
823
+ })();
824
+ return this.shutdownPromise;
825
+ }
826
+ }