@amaster.ai/dsh-a2a 0.1.29 → 0.1.30

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
@@ -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 [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.
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 && task.status.message?.messageId;
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 (task.status?.state === TaskState.TASK_STATE_WORKING && current) {
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 turn's messageId so
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
- /** messageId shared by this turn's streamed text deltas (client-side aggregation key). */
91
+ /** Shared by streamed text deltas until the next committed input or turn. */
92
92
  textMessageId = randomUUID();
93
93
  thoughtMessageId = randomUUID();
94
- /** Authoritative turn text, assembled from `assistant/message` events. */
94
+ /** Current reply segment, assembled from `assistant/message` events. */
95
95
  turnText = '';
96
- /** Fallback turn text, accumulated from `text-delta` chunks. */
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) {
@@ -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 { ManagedTaskStore } from './task-store.js';
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 turn's messageId so
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
- /** messageId shared by this turn's streamed text deltas (client-side aggregation key). */
59
+ /** Shared by streamed text deltas until the next committed input or turn. */
60
60
  private textMessageId;
61
61
  private thoughtMessageId;
62
- /** Authoritative turn text, assembled from `assistant/message` events. */
62
+ /** Current reply segment, assembled from `assistant/message` events. */
63
63
  private turnText;
64
- /** Fallback turn text, accumulated from `text-delta` chunks. */
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amaster.ai/dsh-a2a",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
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",