@amaster.ai/dsh-a2a 0.1.1 → 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/translator.js +27 -20
- package/lib/types/bridge.d.ts +9 -2
- package/lib/types/translator.d.ts +16 -5
- 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/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,7 +33,7 @@
|
|
|
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
|
|
36
|
+
* dsh ships the approval seam as the optional
|
|
34
37
|
* @deepseek-ai/dsh-user-approval package (the `ctx.approval` service), but
|
|
35
38
|
* headless profiles do not compose it and `tools/pre-execute`'s `ask` fails
|
|
36
39
|
* closed without one, so no approval bridge exists here. TODO(verify):
|
|
@@ -116,21 +119,6 @@ export class SessionTranslator {
|
|
|
116
119
|
this.usage = undefined;
|
|
117
120
|
return [this.status(TaskState.TASK_STATE_WORKING, { kind: 'state-change' })];
|
|
118
121
|
}
|
|
119
|
-
case 'assistant/chunk': {
|
|
120
|
-
const chunk = event.data.chunk;
|
|
121
|
-
if (chunk.type === 'text-delta') {
|
|
122
|
-
this.deltaText += chunk.text;
|
|
123
|
-
return [
|
|
124
|
-
this.status(TaskState.TASK_STATE_WORKING, { kind: 'text-content' }, agentTextMessage(this.taskId, this.contextId, chunk.text, this.textMessageId)),
|
|
125
|
-
];
|
|
126
|
-
}
|
|
127
|
-
if (chunk.type === 'reasoning-delta') {
|
|
128
|
-
return [
|
|
129
|
-
this.status(TaskState.TASK_STATE_WORKING, { kind: 'thought' }, agentTextMessage(this.taskId, this.contextId, chunk.text, this.thoughtMessageId)),
|
|
130
|
-
];
|
|
131
|
-
}
|
|
132
|
-
return [];
|
|
133
|
-
}
|
|
134
122
|
case 'assistant/message': {
|
|
135
123
|
this.sawAssistantMessage = true;
|
|
136
124
|
for (const block of event.data.message.content) {
|
|
@@ -175,6 +163,25 @@ export class SessionTranslator {
|
|
|
175
163
|
return [];
|
|
176
164
|
}
|
|
177
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
|
+
}
|
|
178
185
|
turnEnd(reason) {
|
|
179
186
|
const text = this.sawAssistantMessage ? this.turnText : this.deltaText;
|
|
180
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,7 +33,7 @@
|
|
|
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
|
|
36
|
+
* dsh ships the approval seam as the optional
|
|
34
37
|
* @deepseek-ai/dsh-user-approval package (the `ctx.approval` service), but
|
|
35
38
|
* headless profiles do not compose it and `tools/pre-execute`'s `ask` fails
|
|
36
39
|
* closed without one, so no approval bridge exists here. TODO(verify):
|
|
@@ -39,6 +42,7 @@
|
|
|
39
42
|
*/
|
|
40
43
|
import { type Message } from '@a2a-js/sdk';
|
|
41
44
|
import { type AgentExecutionEvent } from '@a2a-js/sdk/server';
|
|
45
|
+
import type { AssistantStreamFrame } from '@deepseek-ai/dsh-agent';
|
|
42
46
|
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
43
47
|
/** One agent-role text message — the single construction site for the plugin's message payloads. */
|
|
44
48
|
export declare function agentTextMessage(taskId: string, contextId: string, text: string, messageId?: string): Message;
|
|
@@ -62,6 +66,13 @@ export declare class SessionTranslator {
|
|
|
62
66
|
constructor(taskId: string, contextId: string, model?: string | undefined);
|
|
63
67
|
/** Translate one session event into zero or more A2A events. Never throws on unknown types. */
|
|
64
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[];
|
|
65
76
|
private turnEnd;
|
|
66
77
|
private status;
|
|
67
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",
|