@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 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 0.1.2 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`.
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.2-rc.1`) — check the `TODO(verify)` markers in `src/` before upgrading dsh.
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 through a per-task {@link SessionTranslator} onto the executing
8
- * request's {@link ExecutionEventBus}.
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.2-rc.1's run() — entry points are
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.2-rc.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
- * assistant/chunk (text-delta) → statusUpdate(WORKING, text part),
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
- * assistant/chunk (reasoning-delta) → statusUpdate(WORKING, thought metadata)
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 0.1.2 ships the approval seam as the optional
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
@@ -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 through a per-task {@link SessionTranslator} onto the executing
8
- * request's {@link ExecutionEventBus}.
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.2-rc.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
- * assistant/chunk (text-delta) → statusUpdate(WORKING, text part),
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
- * assistant/chunk (reasoning-delta) → statusUpdate(WORKING, thought metadata)
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 0.1.2 ships the approval seam as the optional
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.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.2-rc.1",
42
- "@deepseek-ai/dsh-llm": "0.1.2-rc.1",
43
- "@deepseek-ai/dsh-session": "0.1.2-rc.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.2-rc.1",
58
- "@deepseek-ai/dsh-llm": "0.1.2-rc.1",
59
- "@deepseek-ai/dsh-session": "0.1.2-rc.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",