@amaster.ai/dsh-a2a 0.1.0 → 0.1.2
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 +2 -2
- package/lib/bridge.js +22 -3
- package/lib/executor.js +3 -1
- package/lib/translator.js +32 -23
- package/lib/types/bridge.d.ts +9 -2
- package/lib/types/translator.d.ts +21 -8
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ Disabled by default. Configure via the profile's `cordis.patch.yml`:
|
|
|
57
57
|
- **Agents are full preset citizens.** Each task's agent is created with the deployment's default model selection (`agentDefaultModel`) and mounts its agent preset (the web profile keeps all tools inside presets — without one the agent would see an empty tool catalog). `agent.preset` pins a specific preset.
|
|
58
58
|
- **Streaming aggregation.** Text deltas of a turn share one `messageId`, so clients accumulate them into a single message; reasoning deltas ride a separate `messageId` and are marked `metadata.dshAgent.kind: 'thought'`. The turn-final event's message carries the full assembled text, so blocking `SendMessage` clients read the answer from `result.task.status.message`. Tool calls/results are data parts marked `tool-call` / `tool-result`; token usage lands in `metadata.usage` of the final event. A2A 1.0 has no `final` flag — terminal and interrupted states close the stream.
|
|
59
59
|
- **Text-only boundary** for now: `SendMessage` rejects messages with non-text parts (file/url/data) with a failed task.
|
|
60
|
-
- **No approval bridge**: dsh
|
|
60
|
+
- **No approval bridge**: dsh ships the mid-turn approval seam only as the optional `dsh-user-approval` package, which headless profiles do not compose, so tools that would ask are governed by the profile's own approval setup; the A2A side never enters a mid-turn `input-required`.
|
|
61
61
|
- **One in-flight message per task** is the supported flow (send the next message after the turn-final event). dsh serializes queued follow-ups into successive turns, but concurrent requests share one event bus — a second in-flight request may resolve with the first turn's final event.
|
|
62
62
|
- **Restart**: persisted task shells survive in Redis/GCS, but live agents do not — continuing after a restart starts a fresh session (a client-supplied `contextId` names it).
|
|
63
63
|
|
|
@@ -75,7 +75,7 @@ dsh ships **no authentication or authorization**. The server binds `127.0.0.1` b
|
|
|
75
75
|
|
|
76
76
|
## Compatibility
|
|
77
77
|
|
|
78
|
-
Pinned dsh/cordis versions live in the [root compat matrix](../../README.md#compatibility). Event payloads ride pre-release dsh APIs (`@deepseek-ai/dsh-{agent,session,llm}@0.1.
|
|
78
|
+
Pinned dsh/cordis versions live in the [root compat matrix](../../README.md#compatibility). Event payloads ride pre-release dsh APIs (`@deepseek-ai/dsh-{agent,session,llm}@0.1.5-rc.1`) — check the `TODO(verify)` markers in `src/` before upgrading dsh.
|
|
79
79
|
|
|
80
80
|
## License
|
|
81
81
|
|
package/lib/bridge.js
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* One A2A task maps to one dsh session/agent; the A2A `contextId` IS the dsh
|
|
5
5
|
* sessionId. The bridge owns every agent it creates (the consumer-handle
|
|
6
6
|
* pattern dsh's own ACP bridge uses) and routes the durable `session/event`
|
|
7
|
-
* stream
|
|
8
|
-
*
|
|
7
|
+
* stream plus the live `agent/assistant-stream` frames (the V3 session log no
|
|
8
|
+
* longer carries incremental chunks) through a per-task
|
|
9
|
+
* {@link SessionTranslator} onto the executing request's
|
|
10
|
+
* {@link ExecutionEventBus}.
|
|
9
11
|
*
|
|
10
12
|
* Turn settlement: `agent.followup()` is fire-and-forget, so callers await a
|
|
11
13
|
* FIFO waiter resolved by the session's next `turn/end` event (dsh's inbox
|
|
@@ -32,6 +34,7 @@ export class A2aBridge {
|
|
|
32
34
|
this.options = options;
|
|
33
35
|
ctx.on('session/event', (session, event) => this.onSessionEvent(session, event));
|
|
34
36
|
ctx.on('session/disposed', (session) => this.onSessionDisposed(session));
|
|
37
|
+
ctx.on('agent/assistant-stream', ({ agent, frame }) => this.onAssistantStream(agent, frame));
|
|
35
38
|
}
|
|
36
39
|
/**
|
|
37
40
|
* Resolve the live task entry, creating the session/agent when needed.
|
|
@@ -77,7 +80,7 @@ export class A2aBridge {
|
|
|
77
80
|
// the agent's installed model selection (dsh-agent-loop's variables read
|
|
78
81
|
// agent.options; the scoped waterfalls wire provider/model into prompt
|
|
79
82
|
// assembly and the request config). Verified against
|
|
80
|
-
// @deepseek-ai/dsh-headless@0.1.
|
|
83
|
+
// @deepseek-ai/dsh-headless@0.1.5-rc.1's run() — entry points are
|
|
81
84
|
// expected to resolve the deployment default themselves.
|
|
82
85
|
setup: selection || agentPreset
|
|
83
86
|
? async (agentCtx) => {
|
|
@@ -193,6 +196,22 @@ export class A2aBridge {
|
|
|
193
196
|
entry.settled.shift()?.();
|
|
194
197
|
}
|
|
195
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Live assistant deltas (V3: the durable log only commits settled
|
|
201
|
+
* `assistant/message` events). Same no-throw seam as the session feed.
|
|
202
|
+
*/
|
|
203
|
+
onAssistantStream(agent, frame) {
|
|
204
|
+
const entry = this.bySession.get(agent.session.id);
|
|
205
|
+
if (!entry)
|
|
206
|
+
return;
|
|
207
|
+
try {
|
|
208
|
+
for (const out of entry.translator.handleStreamFrame(frame))
|
|
209
|
+
entry.bus?.publish(out);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
console.error('[dsh-a2a] failed to translate assistant stream frame:', error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
196
215
|
onSessionDisposed(session) {
|
|
197
216
|
const entry = this.bySession.get(session.id);
|
|
198
217
|
if (!entry)
|
package/lib/executor.js
CHANGED
|
@@ -82,7 +82,9 @@ function taskEvent(taskId, contextId, state, extra = {}) {
|
|
|
82
82
|
}
|
|
83
83
|
// TODO(verify): non-text parts (file/raw/url/data) — dsh supports image
|
|
84
84
|
// content blocks; map them when a client needs it. The confirmation data-part
|
|
85
|
-
// protocol ({callId, outcome})
|
|
85
|
+
// protocol ({callId, outcome}) maps onto the optional dsh-user-approval
|
|
86
|
+
// service (0.1.2+, asks carry the exact tool call) — bridge it once a
|
|
87
|
+
// deployment composes that service.
|
|
86
88
|
function extractText(message) {
|
|
87
89
|
const nonText = message.parts.filter((part) => part.content?.$case !== 'text');
|
|
88
90
|
if (nonText.length > 0) {
|
package/lib/translator.js
CHANGED
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
* deliberately free of cordis/HTTP concerns so the mapping is unit-testable
|
|
6
6
|
* without a harness. The mapping ports the source project's
|
|
7
7
|
* `packages/a2a-server/src/agent/task.ts` event switch onto dsh's
|
|
8
|
-
* `SessionEventMap` (verified against @deepseek-ai/dsh-session@0.1.
|
|
9
|
-
* emitting the A2A 1.0 data model (@a2a-js/sdk 1.1.0)
|
|
8
|
+
* `SessionEventMap` (verified against @deepseek-ai/dsh-session@0.1.5-rc.1),
|
|
9
|
+
* emitting the A2A 1.0 data model (@a2a-js/sdk 1.1.0). As of the V3 session
|
|
10
|
+
* format the durable log carries no incremental chunks — live text/reasoning
|
|
11
|
+
* deltas arrive as `agent/assistant-stream` chunk frames
|
|
12
|
+
* (@deepseek-ai/dsh-agent@0.1.5-rc.1) through {@link handleStreamFrame}:
|
|
10
13
|
*
|
|
11
14
|
* turn/start → statusUpdate(WORKING), ids rotate
|
|
12
|
-
*
|
|
15
|
+
* stream chunk (text-delta) → statusUpdate(WORKING, text part),
|
|
13
16
|
* reusing the turn's messageId so
|
|
14
17
|
* clients aggregate deltas into one message
|
|
15
|
-
*
|
|
18
|
+
* stream chunk (reasoning-delta) → statusUpdate(WORKING, thought metadata)
|
|
16
19
|
* assistant/message → no event; text + usage captured for the
|
|
17
20
|
* turn-final message (blocking `SendMessage`
|
|
18
21
|
* clients read the answer from the final
|
|
@@ -30,10 +33,12 @@
|
|
|
30
33
|
* A2A 1.0 removed the `final` flag: the SDK's event queue stops on terminal
|
|
31
34
|
* and interrupted (input-required) states, which is exactly the set above.
|
|
32
35
|
*
|
|
33
|
-
* dsh
|
|
34
|
-
* dsh-user-approval package
|
|
35
|
-
*
|
|
36
|
-
* exists here. TODO(verify)
|
|
36
|
+
* dsh ships the approval seam as the optional
|
|
37
|
+
* @deepseek-ai/dsh-user-approval package (the `ctx.approval` service), but
|
|
38
|
+
* headless profiles do not compose it and `tools/pre-execute`'s `ask` fails
|
|
39
|
+
* closed without one, so no approval bridge exists here. TODO(verify):
|
|
40
|
+
* bridge approval asks to A2A input-required once a deployment composes the
|
|
41
|
+
* service.
|
|
37
42
|
*/
|
|
38
43
|
import { randomUUID } from 'node:crypto';
|
|
39
44
|
import { Role, TaskState } from '@a2a-js/sdk';
|
|
@@ -114,21 +119,6 @@ export class SessionTranslator {
|
|
|
114
119
|
this.usage = undefined;
|
|
115
120
|
return [this.status(TaskState.TASK_STATE_WORKING, { kind: 'state-change' })];
|
|
116
121
|
}
|
|
117
|
-
case 'assistant/chunk': {
|
|
118
|
-
const chunk = event.data.chunk;
|
|
119
|
-
if (chunk.type === 'text-delta') {
|
|
120
|
-
this.deltaText += chunk.text;
|
|
121
|
-
return [
|
|
122
|
-
this.status(TaskState.TASK_STATE_WORKING, { kind: 'text-content' }, agentTextMessage(this.taskId, this.contextId, chunk.text, this.textMessageId)),
|
|
123
|
-
];
|
|
124
|
-
}
|
|
125
|
-
if (chunk.type === 'reasoning-delta') {
|
|
126
|
-
return [
|
|
127
|
-
this.status(TaskState.TASK_STATE_WORKING, { kind: 'thought' }, agentTextMessage(this.taskId, this.contextId, chunk.text, this.thoughtMessageId)),
|
|
128
|
-
];
|
|
129
|
-
}
|
|
130
|
-
return [];
|
|
131
|
-
}
|
|
132
122
|
case 'assistant/message': {
|
|
133
123
|
this.sawAssistantMessage = true;
|
|
134
124
|
for (const block of event.data.message.content) {
|
|
@@ -173,6 +163,25 @@ export class SessionTranslator {
|
|
|
173
163
|
return [];
|
|
174
164
|
}
|
|
175
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Translate one live `agent/assistant-stream` frame into zero or more A2A
|
|
168
|
+
* events. Chunk frames carry the transient deltas; start/end frames bracket
|
|
169
|
+
* an attempt and produce nothing (the settled `assistant/message` session
|
|
170
|
+
* event captures text + usage).
|
|
171
|
+
*/
|
|
172
|
+
handleStreamFrame(frame) {
|
|
173
|
+
if (frame.type !== 'chunk')
|
|
174
|
+
return [];
|
|
175
|
+
const { chunk } = frame;
|
|
176
|
+
if (chunk.type !== 'text-delta' && chunk.type !== 'reasoning-delta')
|
|
177
|
+
return [];
|
|
178
|
+
const text = chunk.type === 'text-delta';
|
|
179
|
+
if (text)
|
|
180
|
+
this.deltaText += chunk.text;
|
|
181
|
+
return [
|
|
182
|
+
this.status(TaskState.TASK_STATE_WORKING, { kind: text ? 'text-content' : 'thought' }, agentTextMessage(this.taskId, this.contextId, chunk.text, text ? this.textMessageId : this.thoughtMessageId)),
|
|
183
|
+
];
|
|
184
|
+
}
|
|
176
185
|
turnEnd(reason) {
|
|
177
186
|
const text = this.sawAssistantMessage ? this.turnText : this.deltaText;
|
|
178
187
|
const finalMessage = text
|
package/lib/types/bridge.d.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* One A2A task maps to one dsh session/agent; the A2A `contextId` IS the dsh
|
|
5
5
|
* sessionId. The bridge owns every agent it creates (the consumer-handle
|
|
6
6
|
* pattern dsh's own ACP bridge uses) and routes the durable `session/event`
|
|
7
|
-
* stream
|
|
8
|
-
*
|
|
7
|
+
* stream plus the live `agent/assistant-stream` frames (the V3 session log no
|
|
8
|
+
* longer carries incremental chunks) through a per-task
|
|
9
|
+
* {@link SessionTranslator} onto the executing request's
|
|
10
|
+
* {@link ExecutionEventBus}.
|
|
9
11
|
*
|
|
10
12
|
* Turn settlement: `agent.followup()` is fire-and-forget, so callers await a
|
|
11
13
|
* FIFO waiter resolved by the session's next `turn/end` event (dsh's inbox
|
|
@@ -65,6 +67,11 @@ export declare class A2aBridge {
|
|
|
65
67
|
/** Dispose every owned agent (plugin unload / server shutdown). */
|
|
66
68
|
dispose(): Promise<void>;
|
|
67
69
|
private onSessionEvent;
|
|
70
|
+
/**
|
|
71
|
+
* Live assistant deltas (V3: the durable log only commits settled
|
|
72
|
+
* `assistant/message` events). Same no-throw seam as the session feed.
|
|
73
|
+
*/
|
|
74
|
+
private onAssistantStream;
|
|
68
75
|
private onSessionDisposed;
|
|
69
76
|
/** Close an in-flight stream honestly and release every turn waiter. */
|
|
70
77
|
private settleEntry;
|
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
* deliberately free of cordis/HTTP concerns so the mapping is unit-testable
|
|
6
6
|
* without a harness. The mapping ports the source project's
|
|
7
7
|
* `packages/a2a-server/src/agent/task.ts` event switch onto dsh's
|
|
8
|
-
* `SessionEventMap` (verified against @deepseek-ai/dsh-session@0.1.
|
|
9
|
-
* emitting the A2A 1.0 data model (@a2a-js/sdk 1.1.0)
|
|
8
|
+
* `SessionEventMap` (verified against @deepseek-ai/dsh-session@0.1.5-rc.1),
|
|
9
|
+
* emitting the A2A 1.0 data model (@a2a-js/sdk 1.1.0). As of the V3 session
|
|
10
|
+
* format the durable log carries no incremental chunks — live text/reasoning
|
|
11
|
+
* deltas arrive as `agent/assistant-stream` chunk frames
|
|
12
|
+
* (@deepseek-ai/dsh-agent@0.1.5-rc.1) through {@link handleStreamFrame}:
|
|
10
13
|
*
|
|
11
14
|
* turn/start → statusUpdate(WORKING), ids rotate
|
|
12
|
-
*
|
|
15
|
+
* stream chunk (text-delta) → statusUpdate(WORKING, text part),
|
|
13
16
|
* reusing the turn's messageId so
|
|
14
17
|
* clients aggregate deltas into one message
|
|
15
|
-
*
|
|
18
|
+
* stream chunk (reasoning-delta) → statusUpdate(WORKING, thought metadata)
|
|
16
19
|
* assistant/message → no event; text + usage captured for the
|
|
17
20
|
* turn-final message (blocking `SendMessage`
|
|
18
21
|
* clients read the answer from the final
|
|
@@ -30,13 +33,16 @@
|
|
|
30
33
|
* A2A 1.0 removed the `final` flag: the SDK's event queue stops on terminal
|
|
31
34
|
* and interrupted (input-required) states, which is exactly the set above.
|
|
32
35
|
*
|
|
33
|
-
* dsh
|
|
34
|
-
* dsh-user-approval package
|
|
35
|
-
*
|
|
36
|
-
* exists here. TODO(verify)
|
|
36
|
+
* dsh ships the approval seam as the optional
|
|
37
|
+
* @deepseek-ai/dsh-user-approval package (the `ctx.approval` service), but
|
|
38
|
+
* headless profiles do not compose it and `tools/pre-execute`'s `ask` fails
|
|
39
|
+
* closed without one, so no approval bridge exists here. TODO(verify):
|
|
40
|
+
* bridge approval asks to A2A input-required once a deployment composes the
|
|
41
|
+
* service.
|
|
37
42
|
*/
|
|
38
43
|
import { type Message } from '@a2a-js/sdk';
|
|
39
44
|
import { type AgentExecutionEvent } from '@a2a-js/sdk/server';
|
|
45
|
+
import type { AssistantStreamFrame } from '@deepseek-ai/dsh-agent';
|
|
40
46
|
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
41
47
|
/** One agent-role text message — the single construction site for the plugin's message payloads. */
|
|
42
48
|
export declare function agentTextMessage(taskId: string, contextId: string, text: string, messageId?: string): Message;
|
|
@@ -60,6 +66,13 @@ export declare class SessionTranslator {
|
|
|
60
66
|
constructor(taskId: string, contextId: string, model?: string | undefined);
|
|
61
67
|
/** Translate one session event into zero or more A2A events. Never throws on unknown types. */
|
|
62
68
|
handle(event: SessionEvent): AgentExecutionEvent[];
|
|
69
|
+
/**
|
|
70
|
+
* Translate one live `agent/assistant-stream` frame into zero or more A2A
|
|
71
|
+
* events. Chunk frames carry the transient deltas; start/end frames bracket
|
|
72
|
+
* an attempt and produce nothing (the settled `assistant/message` session
|
|
73
|
+
* event captures text + usage).
|
|
74
|
+
*/
|
|
75
|
+
handleStreamFrame(frame: AssistantStreamFrame): AgentExecutionEvent[];
|
|
63
76
|
private turnEnd;
|
|
64
77
|
private status;
|
|
65
78
|
private dataMessage;
|
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.2",
|
|
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,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
41
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
42
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
43
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
41
|
+
"@deepseek-ai/dsh-agent": "0.1.5-rc.1",
|
|
42
|
+
"@deepseek-ai/dsh-llm": "0.1.5-rc.1",
|
|
43
|
+
"@deepseek-ai/dsh-session": "0.1.5-rc.1",
|
|
44
44
|
"@google-cloud/storage": "^7.0.0",
|
|
45
45
|
"ioredis": "^5.0.0"
|
|
46
46
|
},
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
57
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
58
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
59
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
57
|
+
"@deepseek-ai/dsh-agent": "0.1.5-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-llm": "0.1.5-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-session": "0.1.5-rc.1",
|
|
60
60
|
"@google-cloud/storage": "^7.0.0",
|
|
61
61
|
"@types/express": "^4.17.21",
|
|
62
62
|
"@types/node": "^22.0.0",
|