@amaster.ai/dsh-a2a 0.1.29 → 0.1.31
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 +1 -1
- package/lib/inbox.js +28 -3
- package/lib/task-store.js +32 -3
- package/lib/translator.js +14 -4
- package/lib/types/inbox.d.ts +1 -1
- package/lib/types/task-store.d.ts +4 -1
- package/lib/types/translator.d.ts +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ dsh plugin --profile my-agent add @amaster.ai/dsh-a2a
|
|
|
16
16
|
|
|
17
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
|
-
Standard `SendMessage` / `SendStreamingMessage`, task queries, cancellation and subscriptions remain the wire interface. The negotiated
|
|
19
|
+
Standard `SendMessage` / `SendStreamingMessage`, task queries, cancellation and subscriptions remain the wire interface. The negotiated inbox extension 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
21
|
The same-process `a2aInbox` service exposes `resolve(contextId)`, `content(message, contextId)`, `enqueue(taskId, message, { target?, wakeup? })`, `promote(contextId, nativeMessageId, activeTaskId)`, 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
|
|
package/lib/inbox.js
CHANGED
|
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { Message, TaskState } from '@a2a-js/sdk';
|
|
4
4
|
import { AgentEvent, ServerCallContext, STATE_HEADERS_KEY, } from '@a2a-js/sdk/server';
|
|
5
5
|
import { createUserMessage } from '@deepseek-ai/dsh-llm/message';
|
|
6
|
+
import { commitInboxInput, markInboxInputPending } from './task-store.js';
|
|
6
7
|
import { agentTextMessage, SessionTranslator, terminalStatusUpdate } from './translator.js';
|
|
7
8
|
export const INBOX_EXTENSION = 'https://github.com/TGYD-helige/dsh-plugins/blob/main/docs/protocol/inbox-v1.md';
|
|
8
9
|
const RETAINED_INPUTS = `${INBOX_EXTENSION}#retained-inputs`;
|
|
@@ -201,7 +202,7 @@ export class NativeInbox {
|
|
|
201
202
|
await this.checkpoint(target.task, target.context);
|
|
202
203
|
if (!valid())
|
|
203
204
|
throw new Error('Promotion raced input consumption or task settlement.');
|
|
204
|
-
target.task.history = [...target.task.history, original];
|
|
205
|
+
target.task.history = [...target.task.history, markInboxInputPending(original)];
|
|
205
206
|
target.headers.set(message.id, source.headers.get(message.id));
|
|
206
207
|
if (!agent.inbox.replace(message.id, promoted))
|
|
207
208
|
throw new Error('Queued input has already been claimed.');
|
|
@@ -293,7 +294,7 @@ export class NativeInbox {
|
|
|
293
294
|
throw new Error('Promoted A2A task is not accessible.');
|
|
294
295
|
const original = Message.fromJSON(JSON.parse(message.source.message));
|
|
295
296
|
if (!recipient.history.some((item) => item.messageId === original.messageId)) {
|
|
296
|
-
recipient.history.push(original);
|
|
297
|
+
recipient.history.push(markInboxInputPending(original));
|
|
297
298
|
await this.checkpoint(recipient, context);
|
|
298
299
|
}
|
|
299
300
|
}
|
|
@@ -651,7 +652,7 @@ export class NativeInbox {
|
|
|
651
652
|
const history = saved?.history ?? task.task.history;
|
|
652
653
|
const acceptedHistory = history.some((item) => item.messageId === a2aMessage.messageId)
|
|
653
654
|
? history
|
|
654
|
-
: [...history, a2aMessage];
|
|
655
|
+
: [...history, markInboxInputPending(a2aMessage)];
|
|
655
656
|
await this.checkpoint({ ...task.task, history: acceptedHistory }, context);
|
|
656
657
|
abort.signal.throwIfAborted();
|
|
657
658
|
task.task.history = acceptedHistory;
|
|
@@ -759,6 +760,12 @@ export class NativeInbox {
|
|
|
759
760
|
}
|
|
760
761
|
}
|
|
761
762
|
restoreTask(run, snapshot, context) {
|
|
763
|
+
for (const event of run.entry.handle.agent.session.snapshotEvents()) {
|
|
764
|
+
if (event.type === 'user/message' &&
|
|
765
|
+
event.data.source.kind === 'a2a' &&
|
|
766
|
+
event.data.source.taskId === snapshot.id)
|
|
767
|
+
commitInboxInput(snapshot.history, Message.fromJSON(JSON.parse(event.data.source.message)));
|
|
768
|
+
}
|
|
762
769
|
if (!this.server)
|
|
763
770
|
throw new Error('A2A task recovery requires the running server.');
|
|
764
771
|
const bus = this.server.restoreTask(snapshot, context);
|
|
@@ -922,6 +929,24 @@ export class NativeInbox {
|
|
|
922
929
|
return true;
|
|
923
930
|
}
|
|
924
931
|
try {
|
|
932
|
+
if (event.type === 'user/message' && event.data.source.kind === 'a2a') {
|
|
933
|
+
// Commit on the task bus before subsequent assistant deltas, not on admission.
|
|
934
|
+
const status = {
|
|
935
|
+
state: TaskState.TASK_STATE_WORKING,
|
|
936
|
+
message: { ...this.protocolMessage(run, task, event.data), taskId: task.task.id },
|
|
937
|
+
timestamp: new Date().toISOString(),
|
|
938
|
+
};
|
|
939
|
+
task.translator.handle(event);
|
|
940
|
+
commitInboxInput(task.task.history, status.message);
|
|
941
|
+
task.task.status = status;
|
|
942
|
+
task.bus.publish(AgentEvent.statusUpdate({
|
|
943
|
+
taskId: task.task.id,
|
|
944
|
+
contextId: session.id,
|
|
945
|
+
status,
|
|
946
|
+
metadata: { dshAgent: { kind: 'user-message' } },
|
|
947
|
+
}));
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
925
950
|
if (event.type !== 'turn/end') {
|
|
926
951
|
this.publish(task, event);
|
|
927
952
|
return true;
|
package/lib/task-store.js
CHANGED
|
@@ -16,11 +16,38 @@
|
|
|
16
16
|
* the shared filter/sort/paginate logic so the backends only enumerate shells.
|
|
17
17
|
*/
|
|
18
18
|
import { createHash } from 'node:crypto';
|
|
19
|
-
import { TaskState } from '@a2a-js/sdk';
|
|
19
|
+
import { Role, TaskState, } from '@a2a-js/sdk';
|
|
20
20
|
import { resolveUserScope } from '@a2a-js/sdk/server';
|
|
21
|
+
function inboxMetadata(message) {
|
|
22
|
+
const value = message.metadata?.dshAgent;
|
|
23
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
24
|
+
}
|
|
25
|
+
export function markInboxInputPending(message) {
|
|
26
|
+
return {
|
|
27
|
+
...message,
|
|
28
|
+
metadata: { ...message.metadata, dshAgent: { ...inboxMetadata(message), inboxPending: true } },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** SDK status updates do not replace admitted history entries with the same ID. */
|
|
32
|
+
export function commitInboxInput(history, message) {
|
|
33
|
+
const index = history.findIndex((item) => item.messageId === message.messageId && inboxMetadata(item).inboxPending === true);
|
|
34
|
+
if (index < 0)
|
|
35
|
+
return;
|
|
36
|
+
const previous = history[index];
|
|
37
|
+
const dshAgent = { ...inboxMetadata(previous), ...inboxMetadata(message) };
|
|
38
|
+
delete dshAgent.inboxPending;
|
|
39
|
+
history.splice(index, 1);
|
|
40
|
+
history.push({
|
|
41
|
+
...previous,
|
|
42
|
+
...structuredClone(message),
|
|
43
|
+
metadata: { ...previous.metadata, ...message.metadata, dshAgent },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
21
46
|
/** Omit an unfinished current message from a WORKING snapshot. */
|
|
22
47
|
export function sanitizeTask(task) {
|
|
23
|
-
const current = task.status?.state === TaskState.TASK_STATE_WORKING &&
|
|
48
|
+
const current = task.status?.state === TaskState.TASK_STATE_WORKING &&
|
|
49
|
+
task.status.message?.role === Role.ROLE_AGENT &&
|
|
50
|
+
task.status.message.messageId;
|
|
24
51
|
return current
|
|
25
52
|
? { ...task, history: task.history.filter((message) => message.messageId !== current) }
|
|
26
53
|
: task;
|
|
@@ -115,7 +142,9 @@ export class SanitizedTaskStore {
|
|
|
115
142
|
this.terminalStates.set(task.id, state);
|
|
116
143
|
const history = structuredClone(task.history ?? []);
|
|
117
144
|
const current = task.status?.message;
|
|
118
|
-
if (
|
|
145
|
+
if (current?.role === Role.ROLE_USER)
|
|
146
|
+
commitInboxInput(history, current);
|
|
147
|
+
if (task.status?.state === TaskState.TASK_STATE_WORKING && current?.role === Role.ROLE_AGENT) {
|
|
119
148
|
const previous = this.liveHistory
|
|
120
149
|
.get(task.id)
|
|
121
150
|
?.history.find((message) => message.messageId === current.messageId);
|
package/lib/translator.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* turn/start → statusUpdate(WORKING), ids rotate
|
|
15
15
|
* stream chunk (text-delta) → statusUpdate(WORKING, text part),
|
|
16
|
-
* reusing the
|
|
16
|
+
* reusing the reply segment's messageId so
|
|
17
17
|
* clients aggregate deltas into one message
|
|
18
18
|
* stream chunk (reasoning-delta) → statusUpdate(WORKING, thought metadata)
|
|
19
19
|
* assistant/message → no event; text + usage captured for the
|
|
@@ -88,12 +88,12 @@ export class SessionTranslator {
|
|
|
88
88
|
contextId;
|
|
89
89
|
model;
|
|
90
90
|
source;
|
|
91
|
-
/**
|
|
91
|
+
/** Shared by streamed text deltas until the next committed input or turn. */
|
|
92
92
|
textMessageId = randomUUID();
|
|
93
93
|
thoughtMessageId = randomUUID();
|
|
94
|
-
/**
|
|
94
|
+
/** Current reply segment, assembled from `assistant/message` events. */
|
|
95
95
|
turnText = '';
|
|
96
|
-
/** Fallback
|
|
96
|
+
/** Fallback reply segment, accumulated from `text-delta` chunks. */
|
|
97
97
|
deltaText = '';
|
|
98
98
|
sawAssistantMessage = false;
|
|
99
99
|
usage;
|
|
@@ -117,6 +117,16 @@ export class SessionTranslator {
|
|
|
117
117
|
this.usage = undefined;
|
|
118
118
|
return [this.status(TaskState.TASK_STATE_WORKING, { kind: 'state-change' })];
|
|
119
119
|
}
|
|
120
|
+
case 'user/message': {
|
|
121
|
+
if (event.data.source.kind === 'a2a') {
|
|
122
|
+
this.textMessageId = randomUUID();
|
|
123
|
+
this.thoughtMessageId = randomUUID();
|
|
124
|
+
this.turnText = '';
|
|
125
|
+
this.deltaText = '';
|
|
126
|
+
this.sawAssistantMessage = false;
|
|
127
|
+
}
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
120
130
|
case 'assistant/message': {
|
|
121
131
|
this.sawAssistantMessage = true;
|
|
122
132
|
for (const block of event.data.message.content) {
|
package/lib/types/inbox.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
|
|
|
8
8
|
import type { A2aBridge } from './bridge.js';
|
|
9
9
|
import type { A2aTurnContext } from './index.js';
|
|
10
10
|
import type { A2aServer } from './server.js';
|
|
11
|
-
import type
|
|
11
|
+
import { type ManagedTaskStore } from './task-store.js';
|
|
12
12
|
export declare const INBOX_EXTENSION = "https://github.com/TGYD-helige/dsh-plugins/blob/main/docs/protocol/inbox-v1.md";
|
|
13
13
|
export interface A2aInboxEvent {
|
|
14
14
|
phase: string;
|
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
* A2A 1.0 added `list()` to the TaskStore contract; {@link listShells} holds
|
|
16
16
|
* the shared filter/sort/paginate logic so the backends only enumerate shells.
|
|
17
17
|
*/
|
|
18
|
-
import { type ListTasksRequest, type ListTasksResponse, type Task } from '@a2a-js/sdk';
|
|
18
|
+
import { type ListTasksRequest, type ListTasksResponse, type Message, type Task } from '@a2a-js/sdk';
|
|
19
19
|
import { type ServerCallContext, type TaskStore } from '@a2a-js/sdk/server';
|
|
20
|
+
export declare function markInboxInputPending(message: Message): Message;
|
|
21
|
+
/** SDK status updates do not replace admitted history entries with the same ID. */
|
|
22
|
+
export declare function commitInboxInput(history: Message[], message: Message): void;
|
|
20
23
|
/** Omit an unfinished current message from a WORKING snapshot. */
|
|
21
24
|
export declare function sanitizeTask(task: Task): Task;
|
|
22
25
|
/** TaskStore with the optional lifecycle our backends add. */
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* turn/start → statusUpdate(WORKING), ids rotate
|
|
15
15
|
* stream chunk (text-delta) → statusUpdate(WORKING, text part),
|
|
16
|
-
* reusing the
|
|
16
|
+
* reusing the reply segment's messageId so
|
|
17
17
|
* clients aggregate deltas into one message
|
|
18
18
|
* stream chunk (reasoning-delta) → statusUpdate(WORKING, thought metadata)
|
|
19
19
|
* assistant/message → no event; text + usage captured for the
|
|
@@ -56,12 +56,12 @@ export declare class SessionTranslator {
|
|
|
56
56
|
private readonly contextId;
|
|
57
57
|
private readonly model?;
|
|
58
58
|
private readonly source?;
|
|
59
|
-
/**
|
|
59
|
+
/** Shared by streamed text deltas until the next committed input or turn. */
|
|
60
60
|
private textMessageId;
|
|
61
61
|
private thoughtMessageId;
|
|
62
|
-
/**
|
|
62
|
+
/** Current reply segment, assembled from `assistant/message` events. */
|
|
63
63
|
private turnText;
|
|
64
|
-
/** Fallback
|
|
64
|
+
/** Fallback reply segment, accumulated from `text-delta` chunks. */
|
|
65
65
|
private deltaText;
|
|
66
66
|
private sawAssistantMessage;
|
|
67
67
|
private usage;
|
package/package.json
CHANGED