@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/README.md CHANGED
@@ -12,6 +12,16 @@ A2A protocol server plugin for [DeepSeek Harness (dsh)](https://github.com/deeps
12
12
  dsh plugin --profile my-agent add @amaster.ai/dsh-a2a
13
13
  ```
14
14
 
15
+ ## Native inbox execution
16
+
17
+ `inbox.enabled: true` uses the official DSH inbox (`Agent.send`) for ordinary `next-turn` input and `next-step` steering. Tested with the unmodified npm runtime `0.2.1-rc.2`; no core patch is required. The queue and its durable projection belong to DSH. The plugin only materializes messages, attaches A2A task identities, and translates execution events.
18
+
19
+ Standard `SendMessage` / `SendStreamingMessage`, task queries, cancellation and subscriptions remain the wire interface. The negotiated [inbox extension](../../docs/protocol/inbox-v1.md) forwards `target` and `wakeup` to DSH. `wakeup: false` leaves an idle driver parked; it does not prevent an active driver from consuming pending input. A later real waking message releases pending input.
20
+
21
+ The same-process `a2aInbox` service exposes `resolve(contextId)`, `content(message, contextId)`, `enqueue(taskId, message, { target?, wakeup? })`, and `cancel(taskId)`. There is no separate pause, wake-only, resume, or retained-task restart operation. Cancellation is terminal and removes that task's pending input. Other tasks' input is retained according to official DSH cancellation semantics, and may wait for the next waking message. Clearing a context discards all its pending work.
22
+
23
+ `a2a/before-followup` validates admission; `a2a/before-step` runs at the official pre-step boundary, after system-prompt assembly and before model execution. It cannot change the already assembled system prompt. Original request headers remain process-local. DSH persistence restores its own pending projection; the plugin does not replay splice logs or recover consumed claims. Graceful disposal follows the official runtime's cancellation behavior. Durable A2A task snapshots still require a Redis or GCS TaskStore; an in-memory store is sufficient without restart requirements.
24
+
15
25
  ## Configuration
16
26
 
17
27
  Disabled by default. Configure via the profile's `cordis.patch.yml`:
package/lib/bridge.js CHANGED
@@ -14,6 +14,7 @@
14
14
  * autonomous turns through `a2a/task-settlement`.
15
15
  */
16
16
  import { AsyncLocalStorage } from 'node:async_hooks';
17
+ import { randomUUID } from 'node:crypto';
17
18
  import { TaskState } from '@a2a-js/sdk';
18
19
  import { AgentEvent } from '@a2a-js/sdk/server';
19
20
  // installModelSelection is a root-module runtime import: the dsh-agent index
@@ -30,6 +31,7 @@ import { agentTextMessage, SessionTranslator, terminalStatusUpdate } from './tra
30
31
  export class A2aBridge {
31
32
  ctx;
32
33
  options;
34
+ inbox;
33
35
  tasks = new Map(); // taskId → entry
34
36
  bySession = new Map(); // sessionId (== contextId) → entry
35
37
  clearing = new Set();
@@ -117,8 +119,8 @@ export class A2aBridge {
117
119
  this.creating.delete(contextId);
118
120
  }
119
121
  }
120
- async ensureTaskInner(taskId, contextId) {
121
- if (this.clearing.has(contextId))
122
+ async ensureTaskInner(taskId, contextId, clearing = false) {
123
+ if (this.clearing.has(contextId) && !clearing)
122
124
  throw new Error('Context is being cleared.');
123
125
  const existing = this.tasks.get(taskId);
124
126
  if (existing)
@@ -185,7 +187,11 @@ export class A2aBridge {
185
187
  const persistence = this.ctx.get('sessionPersistence');
186
188
  const persisted = await persistence?.stat(sessionId);
187
189
  const handle = persisted
188
- ? await this.ctx.agents.resume({ resumeSessionId: sessionId, agentOptions, setup })
190
+ ? await this.ctx.agents.resume({
191
+ resumeSessionId: sessionId,
192
+ agentOptions,
193
+ setup,
194
+ })
189
195
  : await this.ctx.agents.create({
190
196
  sessionId,
191
197
  meta: { cwd: this.options.cwd, ...(agentPreset ? { agentPreset } : {}) },
@@ -291,6 +297,19 @@ export class A2aBridge {
291
297
  async waitForTurn(entry) {
292
298
  await Promise.allSettled([...entry.running]);
293
299
  }
300
+ /** The official inbox claim selects the task that owns the next model step. */
301
+ bindInboxTask(entry, taskId, translator, bus, completion, controller) {
302
+ this.tasks.delete(entry.taskId);
303
+ entry.taskId = taskId;
304
+ entry.translator = translator;
305
+ entry.bus = bus;
306
+ entry.interrupted = false;
307
+ entry.settlementAbort = controller;
308
+ entry.running.clear();
309
+ entry.running.add(completion);
310
+ void completion.then(() => entry.running.delete(completion));
311
+ this.tasks.set(taskId, entry);
312
+ }
294
313
  /** Inspect a message for a live task before the SDK starts its next request. */
295
314
  async beforeWorkingTaskMessage(a2aMessage, requestHeaders) {
296
315
  const entry = this.tasks.get(a2aMessage.taskId);
@@ -484,9 +503,16 @@ export class A2aBridge {
484
503
  throw new Error('Context is already being cleared.');
485
504
  this.clearing.add(contextId);
486
505
  try {
506
+ await this.inbox?.clear(contextId);
487
507
  await Promise.allSettled([...(this.creating.get(contextId) ?? [])]);
508
+ if (this.inbox && !this.bySession.has(contextId)) {
509
+ const persistence = this.ctx.get('sessionPersistence');
510
+ if (await persistence?.stat(SessionId(contextId)))
511
+ await this.ensureTaskInner(randomUUID(), contextId, true);
512
+ }
488
513
  const entry = this.bySession.get(contextId);
489
514
  if (entry) {
515
+ entry.handle.agent.inbox?.clear();
490
516
  const active = entry.turnActive || entry.running.size > 0;
491
517
  this.settleEntry(entry, 'Context cleared.');
492
518
  if (active) {
@@ -530,6 +556,12 @@ export class A2aBridge {
530
556
  }
531
557
  /** Dispose every owned agent (plugin unload / server shutdown). */
532
558
  async dispose() {
559
+ try {
560
+ await this.inbox?.shutdown();
561
+ }
562
+ catch (error) {
563
+ console.error('[dsh-a2a] native inbox shutdown failed:', error);
564
+ }
533
565
  const entries = [...this.tasks.values()];
534
566
  this.tasks.clear();
535
567
  this.bySession.clear();
@@ -575,6 +607,14 @@ export class A2aBridge {
575
607
  }
576
608
  if (event.type === 'turn/start')
577
609
  entry.turnActive = true;
610
+ if (this.inbox?.onSessionEvent(session, event)) {
611
+ if (event.type === 'turn/end') {
612
+ this.withdrawApprovals(entry);
613
+ this.asked.delete(session.id);
614
+ entry.turnActive = false;
615
+ }
616
+ return;
617
+ }
578
618
  // No-throw seam: translation errors must never reach the agent loop. The
579
619
  // turn/end waiter still resolves below, so a poisoned event cannot hang
580
620
  // an in-flight execute() either.
@@ -667,6 +707,8 @@ export class A2aBridge {
667
707
  * `assistant/message` events). Same no-throw seam as the session feed.
668
708
  */
669
709
  onAssistantStream(agent, frame) {
710
+ if (this.inbox?.onAssistantStream(agent, frame))
711
+ return;
670
712
  const entry = this.bySession.get(agent.session.id);
671
713
  if (!entry || entry.staleTurn)
672
714
  return;
@@ -679,6 +721,7 @@ export class A2aBridge {
679
721
  }
680
722
  }
681
723
  onSessionDisposed(session) {
724
+ this.inbox?.onDisposed(session);
682
725
  this.children.delete(session);
683
726
  const entry = this.bySession.get(session.id);
684
727
  if (!entry)
package/lib/executor.js CHANGED
@@ -29,6 +29,9 @@ export class DshAgentExecutor {
29
29
  this.suppressTask = suppressTask;
30
30
  }
31
31
  async execute(requestContext, eventBus) {
32
+ if (this.bridge.inbox && !this.bridge.validateApprovalMessage(requestContext.userMessage)) {
33
+ return this.bridge.trackExecution(requestContext.contextId, () => this.bridge.inbox.execute(requestContext, eventBus));
34
+ }
32
35
  if (this.bridge.isClearing(requestContext.contextId)) {
33
36
  this.suppressTask?.(requestContext.taskId);
34
37
  }
@@ -87,7 +90,8 @@ export class DshAgentExecutor {
87
90
  // canceled statusUpdate via the translator — a harmless duplicate); the
88
91
  // published event moves the persisted shell to canceled through the
89
92
  // handler's ResultManager either way.
90
- const contextId = this.bridge.cancel(taskId) ?? '';
93
+ const contextId = (this.bridge.inbox ? await this.bridge.inbox.cancel(taskId) : this.bridge.cancel(taskId)) ??
94
+ '';
91
95
  eventBus.publish(terminalStatusUpdate(taskId, contextId, 'canceled', 'Task canceled by request.'));
92
96
  };
93
97
  }