@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 +10 -0
- package/lib/bridge.js +46 -3
- package/lib/executor.js +5 -1
- package/lib/inbox.js +826 -0
- package/lib/index.js +38 -3
- package/lib/legacy-stream.js +1 -1
- package/lib/server.js +43 -2
- package/lib/task-store.js +46 -12
- package/lib/types/bridge.d.ts +4 -0
- package/lib/types/inbox.d.ts +112 -0
- package/lib/types/index.d.ts +15 -0
- package/lib/types/server.d.ts +7 -2
- package/lib/types/task-store.d.ts +5 -1
- package/package.json +10 -10
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({
|
|
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
|
}
|