@amaster.ai/dsh-a2a 0.1.25 → 0.1.26

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