@amaster.ai/dsh-a2a 0.1.6 → 0.1.7
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 -1
- package/lib/bridge.js +3 -2
- package/lib/content.js +31 -14
- package/lib/executor.js +2 -2
- package/lib/types/bridge.d.ts +1 -1
- package/lib/types/content.d.ts +24 -9
- package/lib/types/executor.d.ts +1 -1
- package/lib/types/index.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,7 +57,8 @@ Disabled by default. Configure via the profile's `cordis.patch.yml`:
|
|
|
57
57
|
- **One task = one dsh session.** The A2A `contextId` IS the dsh session id. A completed turn ends `input-required`, not `completed` — the task is a conversation and stays continuable; `CancelTask` and turn errors are terminal (`canceled` / `failed`), and the SDK rejects follow-ups addressed at a terminal `taskId` (send with only the `contextId` to continue the session under a fresh task id).
|
|
58
58
|
- **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.
|
|
59
59
|
- **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.
|
|
60
|
-
- **Message parts beyond text.** File parts (inline bytes or a `url` — the plugin downloads those over http/https, bounded to 64 MiB and 30 s)
|
|
60
|
+
- **Message parts beyond text.** File parts (inline bytes or a `url` — the plugin downloads those over http/https, bounded to 64 MiB and 30 s) use the composed attachment store (`ctx.attachments`, e.g. `@deepseek-ai/dsh-attachment-local`) unless a non-image materializer is configured: images reach vision-capable models natively, and stored files project to deterministic handle text with a read-only path when dsh can map one into the execution world. Without a store or materializer, files persist under the configured `uploadsDir` (default `<OS temp>/dsh-a2a-uploads/<date>/`, names sanitized cross-platform, collisions suffixed) and the prompt references them by absolute path inside a `<document>` tag. Ensure that directory is readable by the agent's tools. `data` parts become `<data>` JSON text. A turn never fails because a part kind is unsupported.
|
|
61
|
+
- **Remote execution files.** A deployment can register `ctx.provide('a2aFileMaterializer', { materializeFile })`, using the exported `A2aFileMaterializer` type. The callback receives `{ contextId, bytes, filename, mediaType }` (with a sanitized basename) and returns `{ readablePath }`, a path readable by that context's file/shell tools. When configured, non-image FileParts use the materializer instead of `ctx.attachments.saveFile()`; the model receives the returned `<document path="...">`. Images keep the existing attachment handling. A failed callback yields a delivery note, never a host-path fallback. The deployment owns sandbox selection, upload, and lifetime; no E2B SDK is required by this plugin.
|
|
61
62
|
- **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`.
|
|
62
63
|
- **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.
|
|
63
64
|
- **Restart**: persisted task shells survive in Redis/GCS. With `sessionPersistence` composed, a `contextId` already present on disk resumes its dsh session after restart; a new id creates a new session.
|
package/lib/bridge.js
CHANGED
|
@@ -143,9 +143,10 @@ export class A2aBridge {
|
|
|
143
143
|
return { entry, freshTask: true };
|
|
144
144
|
}
|
|
145
145
|
/** A2A parts → dsh content blocks; throws only when no part is usable. */
|
|
146
|
-
async buildContent(message) {
|
|
146
|
+
async buildContent(message, contextId) {
|
|
147
147
|
const attachments = this.ctx.get('attachments');
|
|
148
|
-
|
|
148
|
+
const materializer = this.ctx.get('a2aFileMaterializer');
|
|
149
|
+
return buildMessageContent(message.parts, attachments, uploadsDir(this.options.uploadsDir), materializer ? { contextId, materializer } : undefined);
|
|
149
150
|
}
|
|
150
151
|
isClearing(contextId) {
|
|
151
152
|
return this.clearing.has(contextId);
|
package/lib/content.js
CHANGED
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
* part kinds:
|
|
4
4
|
*
|
|
5
5
|
* - `text` parts stay text;
|
|
6
|
-
* - file parts (inline `raw` bytes or a `url`)
|
|
7
|
-
*
|
|
8
|
-
* (
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* - file parts (inline `raw` bytes or a `url`) use the composed attachment
|
|
7
|
+
* store unless a non-image materializer is configured; url parts are
|
|
8
|
+
* downloaded by the plugin (http/https only, size- and time-bounded);
|
|
9
|
+
* - an optional deployment materializer puts non-image files into the
|
|
10
|
+
* active execution world instead of saving a host attachment;
|
|
11
|
+
* - without a materializer, files with no store (or refused by a store)
|
|
11
12
|
* persist under the configured upload root (default `<OS temp>/
|
|
12
|
-
* dsh-a2a-uploads/<date>/`) and the prompt references them by path
|
|
13
|
-
* the agent reads or converts them with its own tools (the same
|
|
14
|
-
* path-based model dsh-llm's file-block projection uses);
|
|
13
|
+
* dsh-a2a-uploads/<date>/`) and the prompt references them by path;
|
|
15
14
|
* - `data` parts become JSON text.
|
|
16
15
|
*
|
|
17
16
|
* The SDK's admission helpers (admitEncodedFile/admitPromptContent) are not
|
|
@@ -27,7 +26,7 @@ const DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
|
27
26
|
// TODO(verify): the confirmation data-part protocol ({callId, outcome}) maps
|
|
28
27
|
// onto the optional dsh-user-approval service (0.1.2+, asks carry the exact
|
|
29
28
|
// tool call) — bridge it once a deployment composes that service.
|
|
30
|
-
export async function buildMessageContent(parts, attachments, dir) {
|
|
29
|
+
export async function buildMessageContent(parts, attachments, dir, materialization) {
|
|
31
30
|
const content = [];
|
|
32
31
|
for (const part of parts) {
|
|
33
32
|
const c = part.content;
|
|
@@ -43,7 +42,7 @@ export async function buildMessageContent(parts, attachments, dir) {
|
|
|
43
42
|
break;
|
|
44
43
|
case 'raw':
|
|
45
44
|
case 'url':
|
|
46
|
-
content.push(await fileContent(c, part, attachments, dir));
|
|
45
|
+
content.push(await fileContent(c, part, attachments, dir, materialization));
|
|
47
46
|
break;
|
|
48
47
|
default:
|
|
49
48
|
// A part kind this SDK version predates: keep it visible, never drop.
|
|
@@ -67,7 +66,7 @@ function failNote(part, mediaType, uri, label, error) {
|
|
|
67
66
|
return note(part, mediaType, uri, error instanceof Error ? error.message : String(error));
|
|
68
67
|
}
|
|
69
68
|
/** Resolve one file part to an attachment block or a workspace file. */
|
|
70
|
-
async function fileContent(c, part, attachments, dir) {
|
|
69
|
+
async function fileContent(c, part, attachments, dir, materialization) {
|
|
71
70
|
let bytes;
|
|
72
71
|
let mediaType = part.mediaType;
|
|
73
72
|
let uri;
|
|
@@ -85,11 +84,29 @@ async function fileContent(c, part, attachments, dir) {
|
|
|
85
84
|
catch (error) {
|
|
86
85
|
return failNote(part, mediaType, uri, 'failed to read a file part', error);
|
|
87
86
|
}
|
|
88
|
-
|
|
87
|
+
const image = attachments?.imageLimits.mediaTypes.includes(mediaType.toLowerCase());
|
|
88
|
+
if (attachments && (!materialization || image)) {
|
|
89
89
|
const block = await saveAttachment(attachments, bytes, mediaType, part.filename);
|
|
90
90
|
if (block)
|
|
91
91
|
return block;
|
|
92
92
|
}
|
|
93
|
+
if (materialization && !image) {
|
|
94
|
+
try {
|
|
95
|
+
const { readablePath } = await materialization.materializer.materializeFile({
|
|
96
|
+
contextId: materialization.contextId,
|
|
97
|
+
bytes,
|
|
98
|
+
filename: sanitizeUploadFileName(part.filename),
|
|
99
|
+
mediaType,
|
|
100
|
+
});
|
|
101
|
+
if (!readablePath)
|
|
102
|
+
throw new Error('materializer returned no readable path');
|
|
103
|
+
return documentBlock(uri, mediaType, bytes.byteLength, readablePath);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.error('[dsh-a2a] failed to materialize a file part:', error);
|
|
107
|
+
return note(part, mediaType, uri, 'file materialization failed');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
93
110
|
try {
|
|
94
111
|
const name = sanitizeUploadFileName(part.filename);
|
|
95
112
|
const filePath = await persistUpload(dir, name, bytes);
|
|
@@ -156,12 +173,12 @@ function sanitizeUploadFileName(name) {
|
|
|
156
173
|
const stem = cleaned.split('.')[0].toUpperCase();
|
|
157
174
|
return !cleaned || WINDOWS_RESERVED_STEMS.has(stem) ? 'unnamed-file' : cleaned;
|
|
158
175
|
}
|
|
159
|
-
/**
|
|
176
|
+
/** Tool-readable path stand-in for a file part. */
|
|
160
177
|
function documentBlock(uri, mediaType, bytes, filePath) {
|
|
161
178
|
const attrs = [
|
|
162
179
|
// The basename of the final path, so name= always matches the file on
|
|
163
180
|
// disk — including collision suffixes.
|
|
164
|
-
`name="${attr(path.basename(filePath))}"`,
|
|
181
|
+
`name="${attr(path.win32.basename(filePath))}"`,
|
|
165
182
|
uri && `uri="${attr(uri)}"`,
|
|
166
183
|
mediaType && `type="${attr(mediaType)}"`,
|
|
167
184
|
`size="${bytes}"`,
|
package/lib/executor.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* bridge. One `execute()` = one user-message turn:
|
|
4
4
|
*
|
|
5
5
|
* 1. map the message's parts onto dsh content blocks (file parts become
|
|
6
|
-
*
|
|
6
|
+
* execution-world paths with a materializer, attachments or local files otherwise)
|
|
7
7
|
* 2. ensure the task's session/agent exists (creating it on first contact)
|
|
8
8
|
* 3. publish a `task` event first — A2A 1.0 stream ordering REQUIRES the
|
|
9
9
|
* first event of every execute to be a task or message, including
|
|
@@ -37,7 +37,7 @@ export class DshAgentExecutor {
|
|
|
37
37
|
const { userMessage, taskId, contextId } = requestContext;
|
|
38
38
|
let anchored = false;
|
|
39
39
|
try {
|
|
40
|
-
const content = await this.bridge.buildContent(userMessage);
|
|
40
|
+
const content = await this.bridge.buildContent(userMessage, contextId);
|
|
41
41
|
const { entry, freshTask } = await this.bridge.ensureTask(taskId, contextId);
|
|
42
42
|
eventBus.publish(freshTask
|
|
43
43
|
? // First contact: the submitted anchor carries the user message
|
package/lib/types/bridge.d.ts
CHANGED
|
@@ -73,7 +73,7 @@ export declare class A2aBridge {
|
|
|
73
73
|
}>;
|
|
74
74
|
private ensureTaskInner;
|
|
75
75
|
/** A2A parts → dsh content blocks; throws only when no part is usable. */
|
|
76
|
-
buildContent(message: Message): Promise<ContentBlock[]>;
|
|
76
|
+
buildContent(message: Message, contextId: string): Promise<ContentBlock[]>;
|
|
77
77
|
isClearing(contextId: string): boolean;
|
|
78
78
|
/** Include content preparation in the work a context clear must drain. */
|
|
79
79
|
trackExecution(contextId: string, run: () => Promise<void>): Promise<void>;
|
package/lib/types/content.d.ts
CHANGED
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
* part kinds:
|
|
4
4
|
*
|
|
5
5
|
* - `text` parts stay text;
|
|
6
|
-
* - file parts (inline `raw` bytes or a `url`)
|
|
7
|
-
*
|
|
8
|
-
* (
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* - file parts (inline `raw` bytes or a `url`) use the composed attachment
|
|
7
|
+
* store unless a non-image materializer is configured; url parts are
|
|
8
|
+
* downloaded by the plugin (http/https only, size- and time-bounded);
|
|
9
|
+
* - an optional deployment materializer puts non-image files into the
|
|
10
|
+
* active execution world instead of saving a host attachment;
|
|
11
|
+
* - without a materializer, files with no store (or refused by a store)
|
|
11
12
|
* persist under the configured upload root (default `<OS temp>/
|
|
12
|
-
* dsh-a2a-uploads/<date>/`) and the prompt references them by path
|
|
13
|
-
* the agent reads or converts them with its own tools (the same
|
|
14
|
-
* path-based model dsh-llm's file-block projection uses);
|
|
13
|
+
* dsh-a2a-uploads/<date>/`) and the prompt references them by path;
|
|
15
14
|
* - `data` parts become JSON text.
|
|
16
15
|
*
|
|
17
16
|
* The SDK's admission helpers (admitEncodedFile/admitPromptContent) are not
|
|
@@ -21,6 +20,22 @@
|
|
|
21
20
|
import type { Part } from '@a2a-js/sdk';
|
|
22
21
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
23
22
|
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types';
|
|
24
|
-
|
|
23
|
+
/** Deployment-owned writer for files read by an agent's active tools. */
|
|
24
|
+
export interface A2aFileMaterializer {
|
|
25
|
+
materializeFile(input: {
|
|
26
|
+
contextId: string;
|
|
27
|
+
bytes: Uint8Array;
|
|
28
|
+
filename: string;
|
|
29
|
+
mediaType: string;
|
|
30
|
+
}): Promise<{
|
|
31
|
+
readablePath: string;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
interface Materialization {
|
|
35
|
+
contextId: string;
|
|
36
|
+
materializer: A2aFileMaterializer;
|
|
37
|
+
}
|
|
38
|
+
export declare function buildMessageContent(parts: readonly Part[], attachments: AttachmentStore | undefined, dir: string, materialization?: Materialization): Promise<ContentBlock[]>;
|
|
25
39
|
/** The directory file parts persist into when they are not attachments. */
|
|
26
40
|
export declare function uploadsDir(root?: string): string;
|
|
41
|
+
export {};
|
package/lib/types/executor.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* bridge. One `execute()` = one user-message turn:
|
|
4
4
|
*
|
|
5
5
|
* 1. map the message's parts onto dsh content blocks (file parts become
|
|
6
|
-
*
|
|
6
|
+
* execution-world paths with a materializer, attachments or local files otherwise)
|
|
7
7
|
* 2. ensure the task's session/agent exists (creating it on first contact)
|
|
8
8
|
* 3. publish a `task` event first — A2A 1.0 stream ordering REQUIRES the
|
|
9
9
|
* first event of every execute to be a task or message, including
|
package/lib/types/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { Context } from '@deepseek-ai/cordis';
|
|
15
15
|
import Schema from '@deepseek-ai/schemastery';
|
|
16
|
+
export type { A2aFileMaterializer } from './content.js';
|
|
16
17
|
export declare const name = "dsh-a2a";
|
|
17
18
|
/** The bridge creates and owns agents through the registry service. */
|
|
18
19
|
export declare const inject: string[];
|
package/package.json
CHANGED