@amaster.ai/dsh-a2a 0.1.26 → 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 +4 -33
- package/lib/bridge.js +1 -3
- package/lib/inbox.js +156 -274
- package/lib/index.js +1 -1
- package/lib/types/bridge.d.ts +1 -1
- package/lib/types/inbox.d.ts +8 -45
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -14,42 +14,13 @@ dsh plugin --profile my-agent add @amaster.ai/dsh-a2a
|
|
|
14
14
|
|
|
15
15
|
## Native inbox execution
|
|
16
16
|
|
|
17
|
-
`inbox.enabled: true`
|
|
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
18
|
|
|
19
|
-
|
|
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
20
|
|
|
21
|
-
The
|
|
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
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
const inbox = ctx.get('a2aInbox') as A2aInbox;
|
|
27
|
-
const agent = await inbox.resolve(contextId);
|
|
28
|
-
const blocks = await inbox.content(message, contextId);
|
|
29
|
-
await inbox.enqueue(taskId, message); // Host follow-up with explicit task ownership.
|
|
30
|
-
await inbox.pause(contextId);
|
|
31
|
-
await inbox.wake(contextId); // Leaves a persisted pause intact.
|
|
32
|
-
await inbox.wake(contextId, { resume: true }); // Explicitly unpauses existing work.
|
|
33
|
-
await inbox.cancel(taskId, { retainInbox: true }); // Terminal task; pending input stays parked.
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
`pause` cancels active tasks while retaining their unconsumed input and other queued tasks; resumed input from a canceled task gets a fresh task identity. `CancelTask` remains terminal and removes that task's pending input, retaining other tasks' input. The service's explicit retention option parks the context; resuming retained input creates a new task when its former owner is terminal. `a2aTasks.clearContext` discards pending work even when idle. Plugin unload preserves the native inbox and drains its session writes. For restart recovery, configure durable DSH session persistence and a Redis or GCS TaskStore, then call `wake` for the contexts the host chooses to restore. `SubscribeToTask` observes recovered work without submitting a wake message.
|
|
37
|
-
|
|
38
|
-
In native mode, host settlement hooks submit follow-ups through `a2aInbox.enqueue(taskId, message)`; it binds and persists the task identity before enqueueing. Raw `Agent.send` / `followup` inputs without A2A attribution are rejected rather than assigned to the most recent hook.
|
|
39
|
-
|
|
40
|
-
`a2a/before-followup` validates admission; use `a2a/before-assembly` for per-input runtime state. The latter runs after the model-step batch is selected, before prompt assembly, and receives the same `A2aTurnContext` fields. Request headers are process-local: restored inputs receive `requestHeaders: undefined`. Persist replay-safe metadata in the message, never credentials in the queue. Admission observers remain non-vetoing.
|
|
41
|
-
|
|
42
|
-
Recovery only restores an interrupted claim without a committed `user/message`; it never replays committed model input or promises exactly-once tool effects. A task with committed input, no remaining pending input, and no durable terminal A2A checkpoint becomes failed on recovery, including when the turn ended before the process stopped. The host can then decide what to do next. Keep file parts on persistent storage with `uploadsDir` or `a2aFileMaterializer` when queued inputs must survive host or machine restarts. The memory TaskStore and temporary upload default do not provide that durability. In-process task handles remain until context clear or plugin unload; long-lived hosts should clear finished contexts.
|
|
43
|
-
|
|
44
|
-
Run the real loop / A2A HTTP / JSONL recovery checks against a prepared upstream checkout:
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
DSH_SOURCE_ROOT=/absolute/path/to/deepseek-harness node .github/scripts/check-native-inbox.mjs
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
The Integration workflow runs this suite without secrets, including on fork PRs, against a pinned upstream source checkout plus the checked-in native inbox source companion. It verifies insertion during an active turn, FIFO execution, independent task completion, and exactly one user-message commit per input.
|
|
51
|
-
|
|
52
|
-
The checkout needs its installed dependencies and `pnpm run build:native-system`; model responses are scripted, so this check needs no gateway or model credentials. The upstream source companion is required before native mode can be released; no dependency artifact patch is installed by this plugin.
|
|
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.
|
|
53
24
|
|
|
54
25
|
## Configuration
|
|
55
26
|
|
package/lib/bridge.js
CHANGED
|
@@ -191,14 +191,12 @@ export class A2aBridge {
|
|
|
191
191
|
resumeSessionId: sessionId,
|
|
192
192
|
agentOptions,
|
|
193
193
|
setup,
|
|
194
|
-
...(this.inbox ? { keepInboxOnDispose: true } : {}),
|
|
195
194
|
})
|
|
196
195
|
: await this.ctx.agents.create({
|
|
197
196
|
sessionId,
|
|
198
197
|
meta: { cwd: this.options.cwd, ...(agentPreset ? { agentPreset } : {}) },
|
|
199
198
|
agentOptions,
|
|
200
199
|
setup,
|
|
201
|
-
...(this.inbox ? { keepInboxOnDispose: true } : {}),
|
|
202
200
|
});
|
|
203
201
|
const entry = {
|
|
204
202
|
taskId,
|
|
@@ -299,7 +297,7 @@ export class A2aBridge {
|
|
|
299
297
|
async waitForTurn(entry) {
|
|
300
298
|
await Promise.allSettled([...entry.running]);
|
|
301
299
|
}
|
|
302
|
-
/** The
|
|
300
|
+
/** The official inbox claim selects the task that owns the next model step. */
|
|
303
301
|
bindInboxTask(entry, taskId, translator, bus, completion, controller) {
|
|
304
302
|
this.tasks.delete(entry.taskId);
|
|
305
303
|
entry.taskId = taskId;
|
package/lib/inbox.js
CHANGED
|
@@ -5,48 +5,6 @@ import { AgentEvent, ServerCallContext, STATE_HEADERS_KEY, } from '@a2a-js/sdk/s
|
|
|
5
5
|
import { createUserMessage } from '@deepseek-ai/dsh-llm/message';
|
|
6
6
|
import { agentTextMessage, SessionTranslator, terminalStatusUpdate } from './translator.js';
|
|
7
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
8
|
export class NativeInbox {
|
|
51
9
|
ctx;
|
|
52
10
|
bridge;
|
|
@@ -55,17 +13,33 @@ export class NativeInbox {
|
|
|
55
13
|
resolving = new Map();
|
|
56
14
|
stopping = false;
|
|
57
15
|
shutdownPromise;
|
|
58
|
-
waking = new Map();
|
|
59
16
|
preadmitted = new WeakMap();
|
|
60
17
|
server;
|
|
61
18
|
constructor(ctx, bridge, store) {
|
|
62
19
|
this.ctx = ctx;
|
|
63
20
|
this.bridge = bridge;
|
|
64
21
|
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
22
|
bridge.inbox = this;
|
|
68
|
-
ctx.on('agent/
|
|
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
|
+
});
|
|
69
43
|
}
|
|
70
44
|
content(message, contextId) {
|
|
71
45
|
return this.bridge.buildContent(message, contextId);
|
|
@@ -85,9 +59,7 @@ export class NativeInbox {
|
|
|
85
59
|
const admitted = { ...message, taskId, contextId: run.entry.sessionId };
|
|
86
60
|
task.pendingRequests++;
|
|
87
61
|
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 });
|
|
62
|
+
await this.admit(run, task, admitted, task.context, options.target ?? 'next-turn', options.wakeup !== false);
|
|
91
63
|
}
|
|
92
64
|
finally {
|
|
93
65
|
task.pendingRequests--;
|
|
@@ -97,7 +69,7 @@ export class NativeInbox {
|
|
|
97
69
|
}
|
|
98
70
|
throw new Error('Host input must address a live A2A task.');
|
|
99
71
|
}
|
|
100
|
-
context(contextId) {
|
|
72
|
+
context(contextId, context = new ServerCallContext()) {
|
|
101
73
|
if (this.stopping)
|
|
102
74
|
return Promise.reject(new Error('Native inbox is unloading.'));
|
|
103
75
|
const existing = this.contexts.get(contextId);
|
|
@@ -107,25 +79,29 @@ export class NativeInbox {
|
|
|
107
79
|
if (!creating) {
|
|
108
80
|
creating = (async () => {
|
|
109
81
|
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
82
|
const run = {
|
|
116
83
|
entry,
|
|
117
|
-
paused: recovered.paused,
|
|
118
84
|
tasks: new Map(),
|
|
85
|
+
claimed: new Set(),
|
|
119
86
|
admissions: new Set(),
|
|
120
87
|
admissionTail: Promise.resolve(),
|
|
121
88
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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);
|
|
127
100
|
}
|
|
128
|
-
await this.flush(agent.session);
|
|
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);
|
|
129
105
|
this.observe('resolved', run);
|
|
130
106
|
for (const message of this.pending(run))
|
|
131
107
|
this.observeInput('restored', run, message);
|
|
@@ -144,7 +120,6 @@ export class NativeInbox {
|
|
|
144
120
|
contextId: run.entry.sessionId,
|
|
145
121
|
at: Date.now(),
|
|
146
122
|
pending: pending.length,
|
|
147
|
-
paused: run.paused,
|
|
148
123
|
agentStatus: run.entry.handle.agent.status,
|
|
149
124
|
taskIds: [
|
|
150
125
|
...new Set([...run.tasks.values()]
|
|
@@ -198,6 +173,7 @@ export class NativeInbox {
|
|
|
198
173
|
task(task, bus, context) {
|
|
199
174
|
let complete;
|
|
200
175
|
let finished = false;
|
|
176
|
+
const selectedAt = new Map();
|
|
201
177
|
const done = new Promise((resolve) => {
|
|
202
178
|
complete = resolve;
|
|
203
179
|
});
|
|
@@ -209,6 +185,7 @@ export class NativeInbox {
|
|
|
209
185
|
const run = this.contexts.get(task.contextId);
|
|
210
186
|
if (phase && run)
|
|
211
187
|
this.observe(phase, run, { taskId: task.id });
|
|
188
|
+
selectedAt.clear();
|
|
212
189
|
complete();
|
|
213
190
|
};
|
|
214
191
|
return {
|
|
@@ -219,6 +196,7 @@ export class NativeInbox {
|
|
|
219
196
|
finish,
|
|
220
197
|
settled: false,
|
|
221
198
|
headers: new Map(),
|
|
199
|
+
selectedAt,
|
|
222
200
|
controller: new AbortController(),
|
|
223
201
|
pendingRequests: 0,
|
|
224
202
|
context: new ServerCallContext({ tenant: context.tenant, user: context.user }),
|
|
@@ -260,13 +238,13 @@ export class NativeInbox {
|
|
|
260
238
|
(message.contextId && snapshot.contextId !== message.contextId) ||
|
|
261
239
|
this.terminal(snapshot))
|
|
262
240
|
return;
|
|
263
|
-
const run = await this.context(snapshot.contextId);
|
|
241
|
+
const run = await this.context(snapshot.contextId, context);
|
|
264
242
|
let task = run.tasks.get(snapshot.id);
|
|
265
243
|
if (!task)
|
|
266
244
|
task = this.restoreTask(run, snapshot, context);
|
|
267
245
|
task.pendingRequests++;
|
|
268
246
|
try {
|
|
269
|
-
await this.admit(run, task, message, context, controls.target);
|
|
247
|
+
await this.admit(run, task, message, context, controls.target, controls.wakeup);
|
|
270
248
|
}
|
|
271
249
|
catch (error) {
|
|
272
250
|
task.pendingRequests--;
|
|
@@ -291,9 +269,12 @@ export class NativeInbox {
|
|
|
291
269
|
const snapshot = await this.store.load(taskId, context);
|
|
292
270
|
if (!snapshot || this.terminal(snapshot))
|
|
293
271
|
return;
|
|
294
|
-
const run = await this.context(snapshot.contextId);
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
}
|
|
297
278
|
}
|
|
298
279
|
outcome(task) {
|
|
299
280
|
if (task.status?.state === TaskState.TASK_STATE_COMPLETED)
|
|
@@ -313,7 +294,7 @@ export class NativeInbox {
|
|
|
313
294
|
TaskState.TASK_STATE_REJECTED,
|
|
314
295
|
].includes(task.status.state));
|
|
315
296
|
}
|
|
316
|
-
async admit(run, task, a2aMessage, context, target) {
|
|
297
|
+
async admit(run, task, a2aMessage, context, target, wakeup, accepted) {
|
|
317
298
|
if (target === 'next-step' && run.active !== task)
|
|
318
299
|
throw new Error('Steering must address an active task.');
|
|
319
300
|
const duplicate = () => [...run.tasks.values()].some((owned) => owned.task.history.some((message) => message.messageId === a2aMessage.messageId));
|
|
@@ -364,9 +345,10 @@ export class NativeInbox {
|
|
|
364
345
|
abort.signal.throwIfAborted();
|
|
365
346
|
task.task.history = acceptedHistory;
|
|
366
347
|
task.headers.set(message.id, headers);
|
|
367
|
-
|
|
368
|
-
|
|
348
|
+
accepted?.();
|
|
349
|
+
run.entry.handle.agent.send(message, target, wakeup);
|
|
369
350
|
this.observeInput('admitted', run, message);
|
|
351
|
+
await this.flush(run.entry.handle.agent.session);
|
|
370
352
|
void this.ctx
|
|
371
353
|
.parallel('a2a/message-admitted', {
|
|
372
354
|
contextId: run.entry.sessionId,
|
|
@@ -403,7 +385,7 @@ export class NativeInbox {
|
|
|
403
385
|
let ownsTask = false;
|
|
404
386
|
try {
|
|
405
387
|
const controls = this.controls(userMessage, context);
|
|
406
|
-
const run = await this.context(contextId);
|
|
388
|
+
const run = await this.context(contextId, context);
|
|
407
389
|
task = run.tasks.get(taskId);
|
|
408
390
|
if (!task) {
|
|
409
391
|
task = this.task({
|
|
@@ -423,13 +405,12 @@ export class NativeInbox {
|
|
|
423
405
|
}
|
|
424
406
|
const preadmitted = this.preadmitted.get(context)?.delete(userMessage.messageId);
|
|
425
407
|
if (!preadmitted)
|
|
426
|
-
await this.admit(run, task, userMessage, context, controls.target);
|
|
427
|
-
else
|
|
408
|
+
await this.admit(run, task, userMessage, context, controls.target, controls.wakeup, () => bus.publish(AgentEvent.task(structuredClone(task.task))));
|
|
409
|
+
else {
|
|
428
410
|
task.pendingRequests--;
|
|
429
|
-
|
|
411
|
+
bus.publish(AgentEvent.task(structuredClone(task.task)));
|
|
412
|
+
}
|
|
430
413
|
this.settle(run, task);
|
|
431
|
-
if (controls.wakeup && !run.paused && !task.settled)
|
|
432
|
-
await this.wake(contextId, { context });
|
|
433
414
|
await task.done;
|
|
434
415
|
if (task.bus !== bus && task.final)
|
|
435
416
|
bus.publish(task.final);
|
|
@@ -455,13 +436,12 @@ export class NativeInbox {
|
|
|
455
436
|
metadata: undefined,
|
|
456
437
|
};
|
|
457
438
|
bus.publish(AgentEvent.task(snapshot));
|
|
458
|
-
bus.publish(terminalStatusUpdate(taskId, contextId, 'failed', text));
|
|
459
439
|
if (task) {
|
|
460
|
-
|
|
461
|
-
task.
|
|
462
|
-
task.controller.abort();
|
|
463
|
-
task.finish();
|
|
440
|
+
this.fail(task, contextId, error);
|
|
441
|
+
await task.done;
|
|
464
442
|
}
|
|
443
|
+
else
|
|
444
|
+
bus.publish(terminalStatusUpdate(taskId, contextId, 'failed', text));
|
|
465
445
|
}
|
|
466
446
|
finally {
|
|
467
447
|
bus.finished();
|
|
@@ -475,18 +455,26 @@ export class NativeInbox {
|
|
|
475
455
|
run.tasks.set(snapshot.id, restored);
|
|
476
456
|
void restored.done.finally(() => bus.finished());
|
|
477
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.'));
|
|
478
461
|
return restored;
|
|
479
462
|
}
|
|
480
|
-
|
|
481
|
-
run.
|
|
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
|
+
}
|
|
482
470
|
}
|
|
483
471
|
async prepare(detail) {
|
|
484
472
|
const run = this.contexts.get(detail.agent.session.id);
|
|
485
473
|
if (!run)
|
|
486
474
|
return;
|
|
487
|
-
if (this.stopping
|
|
488
|
-
throw new Error('Inbox is
|
|
489
|
-
if (detail.messages.some((message) => message.source.kind !== 'a2a'))
|
|
475
|
+
if (this.stopping)
|
|
476
|
+
throw new Error('Inbox is unloading.');
|
|
477
|
+
if (!run.active && detail.messages.some((message) => message.source.kind !== 'a2a'))
|
|
490
478
|
throw new Error('Native host input requires task-attributed a2aInbox.enqueue().');
|
|
491
479
|
// Restarted steering and an ordinary queued turn may be claimed together by the core.
|
|
492
480
|
// Requeue other owners before committing any user message, keeping task lifecycles separate.
|
|
@@ -494,8 +482,14 @@ export class NativeInbox {
|
|
|
494
482
|
if (owner?.source.kind === 'a2a') {
|
|
495
483
|
const taskId = owner.source.taskId;
|
|
496
484
|
const deferred = detail.messages.filter((message) => message.source.kind === 'a2a' && message.source.taskId !== taskId);
|
|
497
|
-
for (const message of deferred.slice().reverse())
|
|
485
|
+
for (const message of deferred.slice().reverse()) {
|
|
498
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
|
+
}
|
|
499
493
|
for (let index = detail.messages.length - 1; index >= 0; index--)
|
|
500
494
|
if (deferred.includes(detail.messages[index]))
|
|
501
495
|
detail.messages.splice(index, 1);
|
|
@@ -508,18 +502,12 @@ export class NativeInbox {
|
|
|
508
502
|
throw new Error('Queued input has no live A2A task; restore it before waking.');
|
|
509
503
|
if (run.active && run.active !== task)
|
|
510
504
|
throw new Error('A model step cannot consume inputs from different A2A tasks.');
|
|
511
|
-
run
|
|
512
|
-
|
|
513
|
-
if (run.turnStart) {
|
|
514
|
-
this.publish(task, run.turnStart);
|
|
515
|
-
run.turnStart = undefined;
|
|
516
|
-
}
|
|
517
|
-
const selectedAt = Date.now();
|
|
505
|
+
this.select(run, task);
|
|
506
|
+
const selectedAt = task.selectedAt.get(message.id) ?? Date.now();
|
|
518
507
|
const timing = { selectedAt, turn: detail.turn, step: detail.step };
|
|
519
|
-
this.observeInput('selected', run, message, timing);
|
|
520
508
|
try {
|
|
521
509
|
await this.flush(detail.agent.session);
|
|
522
|
-
await this.ctx.serial('a2a/before-
|
|
510
|
+
await this.ctx.serial('a2a/before-step', {
|
|
523
511
|
agent: detail.agent,
|
|
524
512
|
contextId: detail.agent.session.id,
|
|
525
513
|
taskId: task.task.id,
|
|
@@ -548,15 +536,19 @@ export class NativeInbox {
|
|
|
548
536
|
const run = this.contexts.get(session.id);
|
|
549
537
|
if (!run)
|
|
550
538
|
return false;
|
|
551
|
-
if (event.type === 'agent/inbox/paused') {
|
|
552
|
-
run.paused = event.data.paused;
|
|
553
|
-
return true;
|
|
554
|
-
}
|
|
555
539
|
if (event.type === 'turn/start') {
|
|
540
|
+
run.claimed.clear();
|
|
556
541
|
run.turnStart = event;
|
|
557
542
|
return true;
|
|
558
543
|
}
|
|
559
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
|
+
}
|
|
560
552
|
if (!task)
|
|
561
553
|
return true;
|
|
562
554
|
if (task.settled) {
|
|
@@ -573,19 +565,14 @@ export class NativeInbox {
|
|
|
573
565
|
if (this.stopping)
|
|
574
566
|
return true;
|
|
575
567
|
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
568
|
task.final = outputs.at(-1);
|
|
581
|
-
if (
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
.catch((error) => console.error('[dsh-a2a] queue continuation failed:', error));
|
|
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;
|
|
589
576
|
}
|
|
590
577
|
if (!task.settlementStarted) {
|
|
591
578
|
task.settlementStarted = true;
|
|
@@ -611,10 +598,14 @@ export class NativeInbox {
|
|
|
611
598
|
return true;
|
|
612
599
|
}
|
|
613
600
|
settle(run, task) {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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 ||
|
|
618
609
|
task.pendingRequests ||
|
|
619
610
|
run.active === task ||
|
|
620
611
|
this.pending(run).some((message) => message.source.kind === 'a2a' && message.source.taskId === task.task.id))
|
|
@@ -650,146 +641,44 @@ export class NativeInbox {
|
|
|
650
641
|
}
|
|
651
642
|
fail(task, contextId, error) {
|
|
652
643
|
console.error('[dsh-a2a] native inbox failed:', error);
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
if (task.final.kind === 'statusUpdate')
|
|
656
|
-
task.task.status = task.final.data.status;
|
|
657
|
-
task.bus.publish(task.final);
|
|
658
|
-
}
|
|
644
|
+
if (task.settled)
|
|
645
|
+
return;
|
|
659
646
|
task.settled = true;
|
|
660
647
|
task.controller.abort();
|
|
661
|
-
task.
|
|
662
|
-
task.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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);
|
|
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 });
|
|
737
659
|
}
|
|
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
660
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (
|
|
769
|
-
|
|
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];
|
|
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);
|
|
785
675
|
}
|
|
786
|
-
async cancel(taskId
|
|
676
|
+
async cancel(taskId) {
|
|
787
677
|
if (this.stopping)
|
|
788
678
|
throw new Error('Native inbox is unloading.');
|
|
789
679
|
for (const [contextId, run] of this.contexts) {
|
|
790
680
|
if (!run.tasks.has(taskId))
|
|
791
681
|
continue;
|
|
792
|
-
await this.waking.get(contextId);
|
|
793
682
|
const task = run.tasks.get(taskId);
|
|
794
683
|
if (!task)
|
|
795
684
|
continue;
|
|
@@ -799,7 +688,7 @@ export class NativeInbox {
|
|
|
799
688
|
return contextId;
|
|
800
689
|
let operation = task.cancellation;
|
|
801
690
|
if (!operation) {
|
|
802
|
-
operation = this.cancelTask(run, task
|
|
691
|
+
operation = this.cancelTask(run, task);
|
|
803
692
|
task.cancellation = operation;
|
|
804
693
|
void operation.catch(() => {
|
|
805
694
|
if (task.cancellation === operation)
|
|
@@ -811,19 +700,10 @@ export class NativeInbox {
|
|
|
811
700
|
}
|
|
812
701
|
return undefined;
|
|
813
702
|
}
|
|
814
|
-
async cancelTask(run, task
|
|
703
|
+
async cancelTask(run, task) {
|
|
815
704
|
const contextId = run.entry.sessionId;
|
|
816
705
|
const taskId = task.task.id;
|
|
817
|
-
|
|
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
|
-
}
|
|
706
|
+
this.discard(run, taskId);
|
|
827
707
|
task.controller.abort();
|
|
828
708
|
task.settled = true;
|
|
829
709
|
task.final = terminalStatusUpdate(taskId, contextId, 'canceled', 'Task canceled by request.');
|
|
@@ -831,13 +711,6 @@ export class NativeInbox {
|
|
|
831
711
|
task.task.status = task.final.data.status;
|
|
832
712
|
if (run.active === task) {
|
|
833
713
|
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
714
|
}
|
|
842
715
|
try {
|
|
843
716
|
try {
|
|
@@ -853,7 +726,6 @@ export class NativeInbox {
|
|
|
853
726
|
}
|
|
854
727
|
}
|
|
855
728
|
async clear(contextId) {
|
|
856
|
-
await this.waking.get(contextId);
|
|
857
729
|
await this.resolving.get(contextId);
|
|
858
730
|
const run = this.contexts.get(contextId);
|
|
859
731
|
if (!run)
|
|
@@ -864,7 +736,6 @@ export class NativeInbox {
|
|
|
864
736
|
.map((task) => task.task.id)
|
|
865
737
|
.concat(this.pending(run).flatMap((message) => message.source.kind === 'a2a' ? [message.source.taskId] : []))),
|
|
866
738
|
];
|
|
867
|
-
run.paused = true;
|
|
868
739
|
for (const admission of run.admissions)
|
|
869
740
|
admission.abort();
|
|
870
741
|
run.entry.handle.agent.inbox.clear();
|
|
@@ -885,15 +756,20 @@ export class NativeInbox {
|
|
|
885
756
|
const run = this.contexts.get(session.id);
|
|
886
757
|
if (!run)
|
|
887
758
|
return;
|
|
888
|
-
this.observe('disposed', run);
|
|
889
759
|
for (const admission of run.admissions)
|
|
890
760
|
admission.abort();
|
|
891
761
|
for (const task of run.tasks.values()) {
|
|
892
|
-
|
|
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
|
+
}
|
|
893
768
|
task.settled = true;
|
|
894
769
|
task.controller.abort();
|
|
895
770
|
task.finish();
|
|
896
771
|
}
|
|
772
|
+
this.observe('disposed', run);
|
|
897
773
|
this.contexts.delete(session.id);
|
|
898
774
|
}
|
|
899
775
|
shutdown() {
|
|
@@ -902,7 +778,7 @@ export class NativeInbox {
|
|
|
902
778
|
this.stopping = true;
|
|
903
779
|
this.shutdownPromise = (async () => {
|
|
904
780
|
const failures = [];
|
|
905
|
-
await Promise.allSettled([...this.
|
|
781
|
+
await Promise.allSettled([...this.resolving.values()]);
|
|
906
782
|
const cancellations = [...this.contexts.values()].flatMap((run) => [...run.tasks.values()].flatMap((task) => (task.cancellation ? [task.cancellation] : [])));
|
|
907
783
|
for (const outcome of await Promise.allSettled(cancellations))
|
|
908
784
|
if (outcome.status === 'rejected')
|
|
@@ -911,7 +787,7 @@ export class NativeInbox {
|
|
|
911
787
|
for (const admission of run.admissions)
|
|
912
788
|
admission.abort();
|
|
913
789
|
try {
|
|
914
|
-
run.entry.handle.agent.cancel({ kind: 'disposed' }
|
|
790
|
+
run.entry.handle.agent.cancel({ kind: 'disposed' });
|
|
915
791
|
}
|
|
916
792
|
catch (error) {
|
|
917
793
|
failures.push(error);
|
|
@@ -929,11 +805,17 @@ export class NativeInbox {
|
|
|
929
805
|
failures.push(error);
|
|
930
806
|
}
|
|
931
807
|
finally {
|
|
932
|
-
this.observe('disposed', run);
|
|
933
808
|
for (const task of run.tasks.values()) {
|
|
934
|
-
|
|
809
|
+
try {
|
|
810
|
+
if (!task.settled)
|
|
811
|
+
await this.cancelTask(run, task);
|
|
812
|
+
}
|
|
813
|
+
catch (error) {
|
|
814
|
+
failures.push(error);
|
|
815
|
+
}
|
|
935
816
|
task.finish();
|
|
936
817
|
}
|
|
818
|
+
this.observe('disposed', run);
|
|
937
819
|
}
|
|
938
820
|
}
|
|
939
821
|
if (failures.length)
|
package/lib/index.js
CHANGED
|
@@ -29,7 +29,7 @@ export const name = 'dsh-a2a';
|
|
|
29
29
|
/** The bridge creates and owns agents through the registry service. */
|
|
30
30
|
export const inject = ['agents'];
|
|
31
31
|
export const Config = Schema.object({
|
|
32
|
-
inbox: Schema.object({ enabled: Schema.boolean().default(false) }).description('Use
|
|
32
|
+
inbox: Schema.object({ enabled: Schema.boolean().default(false) }).description('Use the official DSH inbox for queued turns and steering.'),
|
|
33
33
|
enabled: Schema.boolean().default(false),
|
|
34
34
|
host: Schema.string().default('127.0.0.1'),
|
|
35
35
|
port: Schema.natural().default(41241),
|
package/lib/types/bridge.d.ts
CHANGED
|
@@ -102,7 +102,7 @@ export declare class A2aBridge {
|
|
|
102
102
|
};
|
|
103
103
|
private findApproval;
|
|
104
104
|
waitForTurn(entry: TaskEntry): Promise<void>;
|
|
105
|
-
/** The
|
|
105
|
+
/** The official inbox claim selects the task that owns the next model step. */
|
|
106
106
|
bindInboxTask(entry: TaskEntry, taskId: string, translator: SessionTranslator, bus: ExecutionEventBus, completion: Promise<void>, controller: AbortController): void;
|
|
107
107
|
/** Inspect a message for a live task before the SDK starts its next request. */
|
|
108
108
|
beforeWorkingTaskMessage(a2aMessage: Message, requestHeaders: unknown): Promise<void>;
|
package/lib/types/inbox.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Message } from '@a2a-js/sdk';
|
|
|
2
2
|
import { type ExecutionEventBus, type RequestContext, ServerCallContext } from '@a2a-js/sdk/server';
|
|
3
3
|
import type { Context } from '@deepseek-ai/cordis';
|
|
4
4
|
import type { Agent, AssistantStreamFrame, InboxTarget } from '@deepseek-ai/dsh-agent';
|
|
5
|
-
import type { ContentBlock
|
|
5
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types';
|
|
6
6
|
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
|
|
7
7
|
import type { A2aBridge } from './bridge.js';
|
|
8
8
|
import type { A2aTurnContext } from './index.js';
|
|
@@ -17,7 +17,6 @@ export interface A2aInboxEvent {
|
|
|
17
17
|
target?: 'next-turn' | 'next-step';
|
|
18
18
|
input?: string;
|
|
19
19
|
position?: number;
|
|
20
|
-
paused?: boolean;
|
|
21
20
|
agentStatus?: string;
|
|
22
21
|
taskId?: string;
|
|
23
22
|
taskIds?: string[];
|
|
@@ -42,26 +41,12 @@ declare module '@deepseek-ai/dsh-llm/types' {
|
|
|
42
41
|
a2a: InboxSource;
|
|
43
42
|
}
|
|
44
43
|
}
|
|
45
|
-
declare module '@deepseek-ai/dsh-session/types' {
|
|
46
|
-
interface SessionEventMap {
|
|
47
|
-
'agent/inbox/paused': {
|
|
48
|
-
paused: boolean;
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
44
|
declare module '@deepseek-ai/cordis' {
|
|
53
45
|
interface Events {
|
|
54
46
|
/** Native inbox lifecycle; content consumers must honor their capture settings. */
|
|
55
47
|
'a2a/inbox'(detail: A2aInboxEvent): void;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
messages: UserMessage[];
|
|
59
|
-
turn: number;
|
|
60
|
-
step: number;
|
|
61
|
-
signal: AbortSignal;
|
|
62
|
-
}): void | Promise<void>;
|
|
63
|
-
/** Selected input, before system-prompt assembly; restored input has no original headers. */
|
|
64
|
-
'a2a/before-assembly'(detail: A2aTurnContext): void | Promise<void>;
|
|
48
|
+
/** Selected input at the official pre-step boundary, after system-prompt assembly. */
|
|
49
|
+
'a2a/before-step'(detail: A2aTurnContext): void | Promise<void>;
|
|
65
50
|
}
|
|
66
51
|
}
|
|
67
52
|
/** Same-process queue control; wire messages and task subscriptions remain standard A2A. */
|
|
@@ -73,23 +58,9 @@ export interface A2aInbox {
|
|
|
73
58
|
target?: InboxTarget;
|
|
74
59
|
wakeup?: boolean;
|
|
75
60
|
}): Promise<void>;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
cancel(taskId: string, options?: {
|
|
79
|
-
retainInbox?: boolean;
|
|
80
|
-
}): Promise<string | undefined>;
|
|
81
|
-
/** Recovery does not override a persisted pause unless resume is explicitly true. */
|
|
82
|
-
wake(contextId: string, options?: {
|
|
83
|
-
resume?: boolean;
|
|
84
|
-
context?: ServerCallContext;
|
|
85
|
-
}): Promise<string[]>;
|
|
61
|
+
/** Standard cancellation is terminal and removes only this task's pending input. */
|
|
62
|
+
cancel(taskId: string): Promise<string | undefined>;
|
|
86
63
|
}
|
|
87
|
-
/** Fold native input mutations; a claimed input is recoverable only before user/message. */
|
|
88
|
-
export declare function recoverInbox(events: readonly SessionEvent[]): {
|
|
89
|
-
paused: boolean;
|
|
90
|
-
pending: UserMessage[];
|
|
91
|
-
committed: Set<string>;
|
|
92
|
-
};
|
|
93
64
|
export declare class NativeInbox implements A2aInbox {
|
|
94
65
|
private readonly ctx;
|
|
95
66
|
private readonly bridge;
|
|
@@ -98,7 +69,6 @@ export declare class NativeInbox implements A2aInbox {
|
|
|
98
69
|
private readonly resolving;
|
|
99
70
|
private stopping;
|
|
100
71
|
private shutdownPromise?;
|
|
101
|
-
private readonly waking;
|
|
102
72
|
private readonly preadmitted;
|
|
103
73
|
server?: A2aServer;
|
|
104
74
|
constructor(ctx: Context, bridge: A2aBridge, store: ManagedTaskStore);
|
|
@@ -125,22 +95,15 @@ export declare class NativeInbox implements A2aInbox {
|
|
|
125
95
|
private admit;
|
|
126
96
|
execute(request: RequestContext, bus: ExecutionEventBus): Promise<void>;
|
|
127
97
|
private restoreTask;
|
|
128
|
-
private
|
|
98
|
+
private select;
|
|
129
99
|
private prepare;
|
|
130
100
|
private publish;
|
|
131
101
|
onSessionEvent(session: Session, event: SessionEvent): boolean;
|
|
132
102
|
private settle;
|
|
133
103
|
onAssistantStream(agent: Agent, frame: AssistantStreamFrame): boolean;
|
|
134
104
|
private fail;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
resume?: boolean;
|
|
138
|
-
context?: ServerCallContext;
|
|
139
|
-
}): Promise<string[]>;
|
|
140
|
-
private wakeInner;
|
|
141
|
-
cancel(taskId: string, options?: {
|
|
142
|
-
retainInbox?: boolean;
|
|
143
|
-
}): Promise<string | undefined>;
|
|
105
|
+
private discard;
|
|
106
|
+
cancel(taskId: string): Promise<string | undefined>;
|
|
144
107
|
private cancelTask;
|
|
145
108
|
clear(contextId: string): Promise<void>;
|
|
146
109
|
onDisposed(session: Session): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amaster.ai/dsh-a2a",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"description": "A2A protocol (JSON-RPC + SSE) server plugin for DeepSeek Harness: expose dsh agents as A2A agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
41
|
-
"@deepseek-ai/dsh-agent": ">=0.
|
|
41
|
+
"@deepseek-ai/dsh-agent": ">=0.2.0-rc.2 || >=0.2.1-alpha.1",
|
|
42
42
|
"@deepseek-ai/dsh-attachment": ">=0.1.5-rc.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.2.0-rc.1",
|
|
43
43
|
"@deepseek-ai/dsh-llm": ">=0.1.5-rc.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.2.0-rc.1",
|
|
44
44
|
"@deepseek-ai/dsh-session": ">=0.2.0-rc.1",
|
|
@@ -63,14 +63,14 @@
|
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@a2a-js/sdk-v03": "npm:@a2a-js/sdk@0.3.14",
|
|
66
|
-
"@deepseek-ai/cordis": "
|
|
67
|
-
"@deepseek-ai/dsh-agent": "0.2.
|
|
68
|
-
"@deepseek-ai/dsh-attachment": "0.2.
|
|
69
|
-
"@deepseek-ai/dsh-llm": "0.2.
|
|
70
|
-
"@deepseek-ai/dsh-session": "0.2.
|
|
71
|
-
"@deepseek-ai/dsh-session-projection": "0.2.
|
|
72
|
-
"@deepseek-ai/dsh-tools": "0.2.
|
|
73
|
-
"@deepseek-ai/dsh-user-approval": "0.2.
|
|
66
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
67
|
+
"@deepseek-ai/dsh-agent": "0.2.1-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-attachment": "0.2.1-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-user-approval": "0.2.1-rc.2",
|
|
74
74
|
"@google-cloud/storage": "^7.0.0",
|
|
75
75
|
"@types/express": "^4.17.21",
|
|
76
76
|
"@types/node": "^22.0.0",
|