@amaster.ai/dsh-a2a 0.1.20 → 0.1.21
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 +11 -1
- package/lib/bridge.js +63 -1
- package/lib/index.js +4 -0
- package/lib/translator.js +6 -2
- package/lib/types/bridge.d.ts +6 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/translator.d.ts +9 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -30,6 +30,8 @@ Disabled by default. Configure via the profile's `cordis.patch.yml`:
|
|
|
30
30
|
intervalMs: 15000 # positive integer, at most 2147483647
|
|
31
31
|
cwd: /srv/agent-workspaces
|
|
32
32
|
uploadsDir: '' # file-part upload root; empty = <OS temp>/dsh-a2a-uploads/<date>
|
|
33
|
+
subagentTools:
|
|
34
|
+
enabled: false # opt in to attributed local descendant tool calls/results
|
|
33
35
|
agent:
|
|
34
36
|
provider: '' # dsh provider/model for A2A sessions; empty = profile default
|
|
35
37
|
model: ''
|
|
@@ -103,6 +105,14 @@ queue or catch-up burst. Timers stop on completion, disconnect, response errors,
|
|
|
103
105
|
and server disposal. Heartbeats require a responsive event loop; they cannot
|
|
104
106
|
repair event-loop starvation or guarantee delivery during backpressure.
|
|
105
107
|
|
|
108
|
+
## Subagent tools
|
|
109
|
+
|
|
110
|
+
`subagentTools.enabled` defaults to `false`. When enabled, tool calls/results from local descendant sessions created inside `tools/execute` are forwarded to the owning A2A task. Each child has a separate translator: its text, reasoning, usage and turn state never enter the parent's translator, and child completion or disposal never settles the parent task. Main-agent streaming keeps its existing format.
|
|
111
|
+
|
|
112
|
+
Child events retain the parent's A2A `taskId` and `contextId`. `metadata.dshAgent.source` carries `{ sessionId, agentId, parentSessionId, parentCallId }` (exported as `SubagentToolSource`): the source child, its immediate parent, and the exact dispatch call that created it. DSH agent IDs are session IDs. Use `(source.sessionId, data.callId)` to identify a child tool call; use `(source.parentSessionId, source.parentCallId)` to attach it to its delegation, including nested descendants. The link is captured with async dispatch context, never inferred from event ordering or descriptions. Verified against DSH 0.2.0-rc.1 and 0.2.0-rc.2's in-process child creation path.
|
|
113
|
+
|
|
114
|
+
Forwarding follows the owning task's active request bus. Background child events between streams are dropped, with no buffering, replay, extra stream or extension of parent settlement; if the same task opens another stream, live events can appear there. Rebinding the parent context to a new task permanently excludes children created for the old task. Providers running outside this process expose no local child events, so only their parent delegation call/result remain visible. Sessions without a captured dispatch link (including children loaded after a process restart, or created outside tool dispatch) are not forwarded; durable attribution for those cases requires an upstream DSH lineage field.
|
|
115
|
+
|
|
106
116
|
## Task stores
|
|
107
117
|
|
|
108
118
|
A2A task snapshots retain protocol history and artifacts; [dsh-storage](../dsh-storage) separately records the full conversation. Backends save on task-state transitions, including the complete reply at `input-required`, so token-rate stream events never reach Redis or GCS. While a task is working, in-process history accumulates text deltas for `GetTask` and resubscription; a restart before the next state change can lose those transient deltas.
|
|
@@ -119,7 +129,7 @@ dsh ships **no authentication or authorization**. The server binds `127.0.0.1` b
|
|
|
119
129
|
|
|
120
130
|
## Compatibility
|
|
121
131
|
|
|
122
|
-
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,attachment}@0.2.0-rc.2` — the attachment store is an optional peer) — check the `TODO(verify)` markers in `src/` before upgrading dsh.
|
|
132
|
+
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,attachment,tools}@0.2.0-rc.2` — the attachment store is an optional peer) — the session peer floor is `>=0.2.0-rc.1` because child attribution uses `firstLifecycleSeq` to reject restored sessions; check the `TODO(verify)` markers in `src/` before upgrading dsh.
|
|
123
133
|
|
|
124
134
|
## License
|
|
125
135
|
|
package/lib/bridge.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* settles at the next `turn/end`; a host may keep the same task open for
|
|
14
14
|
* autonomous turns through `a2a/task-settlement`.
|
|
15
15
|
*/
|
|
16
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
16
17
|
import { TaskState } from '@a2a-js/sdk';
|
|
17
18
|
import { AgentEvent } from '@a2a-js/sdk/server';
|
|
18
19
|
// installModelSelection is a root-module runtime import: the dsh-agent index
|
|
@@ -37,6 +38,8 @@ export class A2aBridge {
|
|
|
37
38
|
asked = new Map();
|
|
38
39
|
approvals = new Map();
|
|
39
40
|
codec;
|
|
41
|
+
delegation = new AsyncLocalStorage();
|
|
42
|
+
children = new WeakMap();
|
|
40
43
|
constructor(ctx, options) {
|
|
41
44
|
this.ctx = ctx;
|
|
42
45
|
this.options = options;
|
|
@@ -46,6 +49,49 @@ export class A2aBridge {
|
|
|
46
49
|
ctx.on('session/disposed', (session) => this.onSessionDisposed(session));
|
|
47
50
|
ctx.on('agent/assistant-stream', ({ agent, frame }) => this.onAssistantStream(agent, frame));
|
|
48
51
|
ctx.on('approval/request', (req, next) => this.onApprovalRequest(req, next));
|
|
52
|
+
if (options.subagentTools?.enabled) {
|
|
53
|
+
// Async context distinguishes concurrent dispatches without relying on event order.
|
|
54
|
+
ctx.on('tools/execute', (exec, next) => {
|
|
55
|
+
const session = exec.agent?.session;
|
|
56
|
+
if (!session)
|
|
57
|
+
return next();
|
|
58
|
+
const child = this.children.get(session);
|
|
59
|
+
const entry = this.bySession.get(session.id) ?? child?.entry;
|
|
60
|
+
if (!entry || entry.staleTurn || (child && child.taskId !== entry.taskId))
|
|
61
|
+
return next();
|
|
62
|
+
return this.delegation.run({
|
|
63
|
+
entry,
|
|
64
|
+
taskId: entry.taskId,
|
|
65
|
+
parentSessionId: session.id,
|
|
66
|
+
parentCallId: exec.callId,
|
|
67
|
+
}, next);
|
|
68
|
+
});
|
|
69
|
+
// Verified against dsh-subagent/session 0.2.0-rc.2: local child creation runs inside dispatch.
|
|
70
|
+
ctx.on('session/created', (session) => {
|
|
71
|
+
try {
|
|
72
|
+
const delegation = this.delegation.getStore();
|
|
73
|
+
if (!delegation ||
|
|
74
|
+
session.header.origin !== 'subagent' ||
|
|
75
|
+
session.header.parentSession !== delegation.parentSessionId ||
|
|
76
|
+
// A wake/resume dispatch is not the original creating delegation.
|
|
77
|
+
session.firstLifecycleSeq !== session.inheritedEventCount ||
|
|
78
|
+
delegation.taskId !== delegation.entry.taskId)
|
|
79
|
+
return;
|
|
80
|
+
this.children.set(session, {
|
|
81
|
+
...delegation,
|
|
82
|
+
translator: new SessionTranslator(delegation.taskId, delegation.entry.sessionId, undefined, {
|
|
83
|
+
sessionId: session.id,
|
|
84
|
+
agentId: session.id,
|
|
85
|
+
parentSessionId: delegation.parentSessionId,
|
|
86
|
+
parentCallId: delegation.parentCallId,
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error('[dsh-a2a] failed to attribute child session:', error);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
49
95
|
}
|
|
50
96
|
/**
|
|
51
97
|
* Resolve the live task entry, creating the session/agent when needed.
|
|
@@ -500,8 +546,23 @@ export class A2aBridge {
|
|
|
500
546
|
}
|
|
501
547
|
onSessionEvent(session, event) {
|
|
502
548
|
const entry = this.bySession.get(session.id);
|
|
503
|
-
if (!entry)
|
|
549
|
+
if (!entry) {
|
|
550
|
+
const child = this.children.get(session);
|
|
551
|
+
if (!child ||
|
|
552
|
+
child.taskId !== child.entry.taskId ||
|
|
553
|
+
!child.entry.bus ||
|
|
554
|
+
child.entry.staleTurn ||
|
|
555
|
+
(event.type !== 'tool/call' && event.type !== 'tool/result'))
|
|
556
|
+
return;
|
|
557
|
+
try {
|
|
558
|
+
for (const out of child.translator.handle(event))
|
|
559
|
+
child.entry.bus.publish(out);
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
console.error('[dsh-a2a] failed to translate child tool event:', error);
|
|
563
|
+
}
|
|
504
564
|
return;
|
|
565
|
+
}
|
|
505
566
|
if (entry.staleTurn) {
|
|
506
567
|
if (event.type === 'turn/end')
|
|
507
568
|
entry.staleTurn = false;
|
|
@@ -618,6 +679,7 @@ export class A2aBridge {
|
|
|
618
679
|
}
|
|
619
680
|
}
|
|
620
681
|
onSessionDisposed(session) {
|
|
682
|
+
this.children.delete(session);
|
|
621
683
|
const entry = this.bySession.get(session.id);
|
|
622
684
|
if (!entry)
|
|
623
685
|
return;
|
package/lib/index.js
CHANGED
|
@@ -42,6 +42,9 @@ export const Config = Schema.object({
|
|
|
42
42
|
* below it). Empty = <OS temp>/dsh-a2a-uploads.
|
|
43
43
|
*/
|
|
44
44
|
uploadsDir: Schema.string().default(''),
|
|
45
|
+
subagentTools: Schema.object({
|
|
46
|
+
enabled: Schema.boolean().default(false),
|
|
47
|
+
}),
|
|
45
48
|
agent: Schema.object({
|
|
46
49
|
provider: Schema.string().default(''),
|
|
47
50
|
model: Schema.string().default(''),
|
|
@@ -102,6 +105,7 @@ export function apply(ctx, config) {
|
|
|
102
105
|
model: config.agent.model || undefined,
|
|
103
106
|
},
|
|
104
107
|
preset: config.agent.preset || undefined,
|
|
108
|
+
subagentTools: config.subagentTools,
|
|
105
109
|
});
|
|
106
110
|
const store = createTaskStore(config);
|
|
107
111
|
await store.init?.();
|
package/lib/translator.js
CHANGED
|
@@ -87,6 +87,7 @@ export class SessionTranslator {
|
|
|
87
87
|
taskId;
|
|
88
88
|
contextId;
|
|
89
89
|
model;
|
|
90
|
+
source;
|
|
90
91
|
/** messageId shared by this turn's streamed text deltas (client-side aggregation key). */
|
|
91
92
|
textMessageId = randomUUID();
|
|
92
93
|
thoughtMessageId = randomUUID();
|
|
@@ -98,10 +99,11 @@ export class SessionTranslator {
|
|
|
98
99
|
usage;
|
|
99
100
|
/** callId → tool name, for naming `tool/result` payloads. */
|
|
100
101
|
toolNames = new Map();
|
|
101
|
-
constructor(taskId, contextId, model) {
|
|
102
|
+
constructor(taskId, contextId, model, source) {
|
|
102
103
|
this.taskId = taskId;
|
|
103
104
|
this.contextId = contextId;
|
|
104
105
|
this.model = model;
|
|
106
|
+
this.source = source;
|
|
105
107
|
}
|
|
106
108
|
/** Translate one session event into zero or more A2A events. Never throws on unknown types. */
|
|
107
109
|
handle(event) {
|
|
@@ -199,7 +201,9 @@ export class SessionTranslator {
|
|
|
199
201
|
}
|
|
200
202
|
}
|
|
201
203
|
status(state, dshAgent, message, error) {
|
|
202
|
-
const metadata = {
|
|
204
|
+
const metadata = {
|
|
205
|
+
dshAgent: this.source ? { ...dshAgent, source: this.source } : dshAgent,
|
|
206
|
+
};
|
|
203
207
|
if (this.model)
|
|
204
208
|
metadata.model = this.model;
|
|
205
209
|
const terminal = state !== TaskState.TASK_STATE_WORKING;
|
package/lib/types/bridge.d.ts
CHANGED
|
@@ -32,6 +32,10 @@ export interface BridgeOptions {
|
|
|
32
32
|
};
|
|
33
33
|
/** Agent preset id; empty/absent = the deployment default (when a roster exists). */
|
|
34
34
|
preset?: string;
|
|
35
|
+
/** Forward attributed descendant tool events; disabled by default. */
|
|
36
|
+
subagentTools?: {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
};
|
|
35
39
|
}
|
|
36
40
|
export interface TaskEntry {
|
|
37
41
|
taskId: string;
|
|
@@ -67,6 +71,8 @@ export declare class A2aBridge {
|
|
|
67
71
|
private readonly asked;
|
|
68
72
|
private readonly approvals;
|
|
69
73
|
private readonly codec;
|
|
74
|
+
private readonly delegation;
|
|
75
|
+
private readonly children;
|
|
70
76
|
constructor(ctx: Context, options: BridgeOptions);
|
|
71
77
|
/**
|
|
72
78
|
* Resolve the live task entry, creating the session/agent when needed.
|
package/lib/types/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ import Schema from '@deepseek-ai/schemastery';
|
|
|
19
19
|
export type { A2aApproval, A2aApprovalCodec, A2aApprovalReply } from './approval.js';
|
|
20
20
|
export { defaultApprovalCodec } from './approval.js';
|
|
21
21
|
export type { A2aFileMaterializer } from './content.js';
|
|
22
|
+
export type { SubagentToolSource } from './translator.js';
|
|
22
23
|
/** Event emitted when one A2A user message enters the dsh inbox. */
|
|
23
24
|
export declare const A2A_MESSAGE_ADMITTED_EVENT: "a2a/message-admitted";
|
|
24
25
|
export interface A2aTurnContext {
|
|
@@ -84,6 +85,11 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
84
85
|
* below it). Empty = <OS temp>/dsh-a2a-uploads.
|
|
85
86
|
*/
|
|
86
87
|
uploadsDir: Schema<string, string>;
|
|
88
|
+
subagentTools: Schema<Schemastery.ObjectS<{
|
|
89
|
+
enabled: Schema<boolean, boolean>;
|
|
90
|
+
}>, Schemastery.ObjectT<{
|
|
91
|
+
enabled: Schema<boolean, boolean>;
|
|
92
|
+
}>>;
|
|
87
93
|
agent: Schema<Schemastery.ObjectS<{
|
|
88
94
|
provider: Schema<string, string>;
|
|
89
95
|
model: Schema<string, string>;
|
|
@@ -156,6 +162,11 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
156
162
|
* below it). Empty = <OS temp>/dsh-a2a-uploads.
|
|
157
163
|
*/
|
|
158
164
|
uploadsDir: Schema<string, string>;
|
|
165
|
+
subagentTools: Schema<Schemastery.ObjectS<{
|
|
166
|
+
enabled: Schema<boolean, boolean>;
|
|
167
|
+
}>, Schemastery.ObjectT<{
|
|
168
|
+
enabled: Schema<boolean, boolean>;
|
|
169
|
+
}>>;
|
|
159
170
|
agent: Schema<Schemastery.ObjectS<{
|
|
160
171
|
provider: Schema<string, string>;
|
|
161
172
|
model: Schema<string, string>;
|
|
@@ -221,6 +232,9 @@ export interface A2aPluginConfig {
|
|
|
221
232
|
};
|
|
222
233
|
cwd: string;
|
|
223
234
|
uploadsDir: string;
|
|
235
|
+
subagentTools: {
|
|
236
|
+
enabled: boolean;
|
|
237
|
+
};
|
|
224
238
|
agent: {
|
|
225
239
|
provider: string;
|
|
226
240
|
model: string;
|
|
@@ -40,6 +40,13 @@ import { type Message } from '@a2a-js/sdk';
|
|
|
40
40
|
import { type AgentExecutionEvent } from '@a2a-js/sdk/server';
|
|
41
41
|
import type { AssistantStreamFrame } from '@deepseek-ai/dsh-agent';
|
|
42
42
|
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
43
|
+
/** Immediate delegation edge; tool call IDs are scoped by their source session. */
|
|
44
|
+
export interface SubagentToolSource {
|
|
45
|
+
sessionId: string;
|
|
46
|
+
agentId: string;
|
|
47
|
+
parentSessionId: string;
|
|
48
|
+
parentCallId: string;
|
|
49
|
+
}
|
|
43
50
|
/** One agent-role text message — the single construction site for the plugin's message payloads. */
|
|
44
51
|
export declare function agentTextMessage(taskId: string, contextId: string, text: string, messageId?: string): Message;
|
|
45
52
|
/** A terminal (stream-closing) statusUpdate — shared by the bridge's settle path and the executor's failure/cancel paths. */
|
|
@@ -48,6 +55,7 @@ export declare class SessionTranslator {
|
|
|
48
55
|
private readonly taskId;
|
|
49
56
|
private readonly contextId;
|
|
50
57
|
private readonly model?;
|
|
58
|
+
private readonly source?;
|
|
51
59
|
/** messageId shared by this turn's streamed text deltas (client-side aggregation key). */
|
|
52
60
|
private textMessageId;
|
|
53
61
|
private thoughtMessageId;
|
|
@@ -59,7 +67,7 @@ export declare class SessionTranslator {
|
|
|
59
67
|
private usage;
|
|
60
68
|
/** callId → tool name, for naming `tool/result` payloads. */
|
|
61
69
|
private readonly toolNames;
|
|
62
|
-
constructor(taskId: string, contextId: string, model?: string | undefined);
|
|
70
|
+
constructor(taskId: string, contextId: string, model?: string | undefined, source?: SubagentToolSource | undefined);
|
|
63
71
|
/** Translate one session event into zero or more A2A events. Never throws on unknown types. */
|
|
64
72
|
handle(event: SessionEvent): AgentExecutionEvent[];
|
|
65
73
|
/**
|
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.21",
|
|
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",
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"@deepseek-ai/dsh-agent": ">=0.1.5-rc.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.2.0-rc.1",
|
|
42
42
|
"@deepseek-ai/dsh-attachment": ">=0.1.5-rc.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.2.0-rc.1",
|
|
43
43
|
"@deepseek-ai/dsh-llm": ">=0.1.5-rc.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.2.0-rc.1",
|
|
44
|
-
"@deepseek-ai/dsh-session": ">=0.
|
|
44
|
+
"@deepseek-ai/dsh-session": ">=0.2.0-rc.1",
|
|
45
|
+
"@deepseek-ai/dsh-tools": ">=0.1.5-rc.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.2.0-rc.1",
|
|
45
46
|
"@deepseek-ai/dsh-user-approval": ">=0.1.6-alpha.2 || >=0.1.7-alpha.1 || >=0.2.0-rc.1",
|
|
46
47
|
"@google-cloud/storage": "^7.0.0",
|
|
47
48
|
"ioredis": "^5.0.0"
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
"@deepseek-ai/dsh-llm": "0.2.0-rc.2",
|
|
69
70
|
"@deepseek-ai/dsh-session": "0.2.0-rc.2",
|
|
70
71
|
"@deepseek-ai/dsh-session-projection": "0.2.0-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-tools": "0.2.0-rc.2",
|
|
71
73
|
"@deepseek-ai/dsh-user-approval": "0.2.0-rc.2",
|
|
72
74
|
"@google-cloud/storage": "^7.0.0",
|
|
73
75
|
"@types/express": "^4.17.21",
|