@amaster.ai/dsh-a2a 0.1.7 → 0.1.8

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,8 +57,9 @@ 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) 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.
60
+ - **Message parts beyond text.** File parts can carry inline bytes or a `url`; the plugin downloads URLs over http/https, bounded to 64 MiB and 30 s. Supported images use the composed attachment store (`ctx.attachments`, e.g. `@deepseek-ai/dsh-attachment-local`) to reach vision-capable models. Non-image files use a configured materializer or persist under `uploadsDir` (default `<OS temp>/dsh-a2a-uploads/<date>/`, names sanitized cross-platform, collisions suffixed), even when an attachment store exists. The prompt references the resulting readable path inside a `<document>` tag. Ensure the local 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
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.
62
+ - **Local execution files.** Without a remote materializer, inline and downloaded non-image FileParts persist under `uploadsDir`; the `<document>` envelope carries the path, source URL, media type, size, and original filename when sanitization or a collision changes it. Images still use native attachment handling.
62
63
  - **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`.
63
64
  - **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.
64
65
  - **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/content.js CHANGED
@@ -3,13 +3,12 @@
3
3
  * part kinds:
4
4
  *
5
5
  * - `text` parts stay text;
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);
6
+ * - file parts (inline `raw` bytes or a `url`) are downloaded by the plugin
7
+ * (http/https only, size- and time-bounded) when they carry a URL;
9
8
  * - an optional deployment materializer puts non-image files into the
10
9
  * active execution world instead of saving a host attachment;
11
- * - without a materializer, files with no store (or refused by a store)
12
- * persist under the configured upload root (default `<OS temp>/
10
+ * - without a materializer, non-image files persist under the configured
11
+ * upload root even when an attachment store exists (default `<OS temp>/
13
12
  * dsh-a2a-uploads/<date>/`) and the prompt references them by path;
14
13
  * - `data` parts become JSON text.
15
14
  *
@@ -54,11 +53,11 @@ export async function buildMessageContent(parts, attachments, dir, materializati
54
53
  }
55
54
  return content;
56
55
  }
57
- /** The directory file parts persist into when they are not attachments. */
56
+ /** The directory non-image file parts persist into without a materializer. */
58
57
  export function uploadsDir(root) {
59
58
  // 'sv-SE' renders YYYY-MM-DD; the bucket is local time.
60
59
  const segment = new Date().toLocaleDateString('sv-SE').replaceAll('-', '');
61
- return path.join(root || path.join(os.tmpdir(), 'dsh-a2a-uploads'), segment);
60
+ return path.resolve(root || path.join(os.tmpdir(), 'dsh-a2a-uploads'), segment);
62
61
  }
63
62
  /** Log and degrade one file-part failure to a metadata note. */
64
63
  function failNote(part, mediaType, uri, label, error) {
@@ -85,7 +84,7 @@ async function fileContent(c, part, attachments, dir, materialization) {
85
84
  return failNote(part, mediaType, uri, 'failed to read a file part', error);
86
85
  }
87
86
  const image = attachments?.imageLimits.mediaTypes.includes(mediaType.toLowerCase());
88
- if (attachments && (!materialization || image)) {
87
+ if (attachments && image) {
89
88
  const block = await saveAttachment(attachments, bytes, mediaType, part.filename);
90
89
  if (block)
91
90
  return block;
@@ -100,7 +99,7 @@ async function fileContent(c, part, attachments, dir, materialization) {
100
99
  });
101
100
  if (!readablePath)
102
101
  throw new Error('materializer returned no readable path');
103
- return documentBlock(uri, mediaType, bytes.byteLength, readablePath);
102
+ return documentBlock(uri, mediaType, bytes.byteLength, readablePath, part.filename);
104
103
  }
105
104
  catch (error) {
106
105
  console.error('[dsh-a2a] failed to materialize a file part:', error);
@@ -110,7 +109,7 @@ async function fileContent(c, part, attachments, dir, materialization) {
110
109
  try {
111
110
  const name = sanitizeUploadFileName(part.filename);
112
111
  const filePath = await persistUpload(dir, name, bytes);
113
- return documentBlock(uri, mediaType, bytes.byteLength, filePath);
112
+ return documentBlock(uri, mediaType, bytes.byteLength, filePath, part.filename);
114
113
  }
115
114
  catch (error) {
116
115
  return failNote(part, mediaType, uri, 'failed to persist a file part', error);
@@ -174,11 +173,14 @@ function sanitizeUploadFileName(name) {
174
173
  return !cleaned || WINDOWS_RESERVED_STEMS.has(stem) ? 'unnamed-file' : cleaned;
175
174
  }
176
175
  /** Tool-readable path stand-in for a file part. */
177
- function documentBlock(uri, mediaType, bytes, filePath) {
176
+ function documentBlock(uri, mediaType, bytes, filePath, originalName) {
178
177
  const attrs = [
179
178
  // The basename of the final path, so name= always matches the file on
180
179
  // disk — including collision suffixes.
181
180
  `name="${attr(path.win32.basename(filePath))}"`,
181
+ originalName &&
182
+ originalName !== path.win32.basename(filePath) &&
183
+ `originalName="${attr(originalName)}"`,
182
184
  uri && `uri="${attr(uri)}"`,
183
185
  mediaType && `type="${attr(mediaType)}"`,
184
186
  `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
- * execution-world paths with a materializer, attachments or local files otherwise)
6
+ * execution-world paths with a materializer, local paths 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/index.js CHANGED
@@ -31,8 +31,8 @@ export const Config = Schema.object({
31
31
  /** Working directory for agents spawned by A2A tasks. Must be absolute. */
32
32
  cwd: Schema.string().default(process.cwd()),
33
33
  /**
34
- * Root directory file parts persist into when no attachment store is
35
- * composed (date-layered below it). Empty = <OS temp>/dsh-a2a-uploads.
34
+ * Root directory for fallback or opted-in local file parts (date-layered
35
+ * below it). Empty = <OS temp>/dsh-a2a-uploads.
36
36
  */
37
37
  uploadsDir: Schema.string().default(''),
38
38
  agent: Schema.object({
@@ -3,13 +3,12 @@
3
3
  * part kinds:
4
4
  *
5
5
  * - `text` parts stay text;
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);
6
+ * - file parts (inline `raw` bytes or a `url`) are downloaded by the plugin
7
+ * (http/https only, size- and time-bounded) when they carry a URL;
9
8
  * - an optional deployment materializer puts non-image files into the
10
9
  * active execution world instead of saving a host attachment;
11
- * - without a materializer, files with no store (or refused by a store)
12
- * persist under the configured upload root (default `<OS temp>/
10
+ * - without a materializer, non-image files persist under the configured
11
+ * upload root even when an attachment store exists (default `<OS temp>/
13
12
  * dsh-a2a-uploads/<date>/`) and the prompt references them by path;
14
13
  * - `data` parts become JSON text.
15
14
  *
@@ -36,6 +35,6 @@ interface Materialization {
36
35
  materializer: A2aFileMaterializer;
37
36
  }
38
37
  export declare function buildMessageContent(parts: readonly Part[], attachments: AttachmentStore | undefined, dir: string, materialization?: Materialization): Promise<ContentBlock[]>;
39
- /** The directory file parts persist into when they are not attachments. */
38
+ /** The directory non-image file parts persist into without a materializer. */
40
39
  export declare function uploadsDir(root?: string): string;
41
40
  export {};
@@ -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
- * execution-world paths with a materializer, attachments or local files otherwise)
6
+ * execution-world paths with a materializer, local paths 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
@@ -25,8 +25,8 @@ export declare const Config: Schema<Schemastery.ObjectS<{
25
25
  /** Working directory for agents spawned by A2A tasks. Must be absolute. */
26
26
  cwd: Schema<string, string>;
27
27
  /**
28
- * Root directory file parts persist into when no attachment store is
29
- * composed (date-layered below it). Empty = <OS temp>/dsh-a2a-uploads.
28
+ * Root directory for fallback or opted-in local file parts (date-layered
29
+ * below it). Empty = <OS temp>/dsh-a2a-uploads.
30
30
  */
31
31
  uploadsDir: Schema<string, string>;
32
32
  agent: Schema<Schemastery.ObjectS<{
@@ -90,8 +90,8 @@ export declare const Config: Schema<Schemastery.ObjectS<{
90
90
  /** Working directory for agents spawned by A2A tasks. Must be absolute. */
91
91
  cwd: Schema<string, string>;
92
92
  /**
93
- * Root directory file parts persist into when no attachment store is
94
- * composed (date-layered below it). Empty = <OS temp>/dsh-a2a-uploads.
93
+ * Root directory for fallback or opted-in local file parts (date-layered
94
+ * below it). Empty = <OS temp>/dsh-a2a-uploads.
95
95
  */
96
96
  uploadsDir: Schema<string, string>;
97
97
  agent: Schema<Schemastery.ObjectS<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amaster.ai/dsh-a2a",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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",