@amaster.ai/dsh-a2a 0.1.0

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.
@@ -0,0 +1,171 @@
1
+ /**
2
+ * dsh-a2a — exposes DeepSeek Harness agents over the A2A protocol.
3
+ *
4
+ * Plugin shape mirrors dsh's own ACP bridge (packages/acp/acp in the dsh
5
+ * repo): the plugin owns the agents it creates, serves a protocol endpoint
6
+ * for the process lifetime, and disposes everything on unload.
7
+ *
8
+ * Security note: dsh ships no authn/authz. This plugin binds 127.0.0.1 by
9
+ * default; put a real authentication layer in front before exposing it
10
+ * beyond loopback.
11
+ *
12
+ * @module dsh-a2a
13
+ */
14
+ import type { Context } from '@deepseek-ai/cordis';
15
+ import Schema from '@deepseek-ai/schemastery';
16
+ export declare const name = "dsh-a2a";
17
+ /** The bridge creates and owns agents through the registry service. */
18
+ export declare const inject: string[];
19
+ export declare const Config: Schema<Schemastery.ObjectS<{
20
+ enabled: Schema<boolean, boolean>;
21
+ host: Schema<string, string>;
22
+ port: Schema<number, number>;
23
+ basePath: Schema<string, string>;
24
+ /** Working directory for agents spawned by A2A tasks. Must be absolute. */
25
+ cwd: Schema<string, string>;
26
+ agent: Schema<Schemastery.ObjectS<{
27
+ provider: Schema<string, string>;
28
+ model: Schema<string, string>;
29
+ /**
30
+ * Agent preset to mount per task (empty = the deployment default when the
31
+ * profile composes a preset roster). The web profile keeps tools inside
32
+ * presets — without one the agent sees an empty tool catalog.
33
+ */
34
+ preset: Schema<string, string>;
35
+ }>, Schemastery.ObjectT<{
36
+ provider: Schema<string, string>;
37
+ model: Schema<string, string>;
38
+ /**
39
+ * Agent preset to mount per task (empty = the deployment default when the
40
+ * profile composes a preset roster). The web profile keeps tools inside
41
+ * presets — without one the agent sees an empty tool catalog.
42
+ */
43
+ preset: Schema<string, string>;
44
+ }>>;
45
+ card: Schema<Schemastery.ObjectS<{
46
+ name: Schema<string, string>;
47
+ description: Schema<string, string>;
48
+ version: Schema<string, string>;
49
+ publicUrl: Schema<string, string>;
50
+ }>, Schemastery.ObjectT<{
51
+ name: Schema<string, string>;
52
+ description: Schema<string, string>;
53
+ version: Schema<string, string>;
54
+ publicUrl: Schema<string, string>;
55
+ }>>;
56
+ /**
57
+ * A2A task state store (task metadata only — conversation history is
58
+ * dsh-storage's ai_messages, not this). 'memory' loses tasks on restart.
59
+ */
60
+ taskStore: Schema<"memory" | "redis" | "gcs", "memory" | "redis" | "gcs">;
61
+ redis: Schema<Schemastery.ObjectS<{
62
+ url: Schema<string, string>;
63
+ keyPrefix: Schema<string, string>;
64
+ ttlSeconds: Schema<number, number>;
65
+ }>, Schemastery.ObjectT<{
66
+ url: Schema<string, string>;
67
+ keyPrefix: Schema<string, string>;
68
+ ttlSeconds: Schema<number, number>;
69
+ }>>;
70
+ gcs: Schema<Schemastery.ObjectS<{
71
+ bucket: Schema<string, string>;
72
+ prefix: Schema<string, string>;
73
+ keyFilename: Schema<string, string>;
74
+ }>, Schemastery.ObjectT<{
75
+ bucket: Schema<string, string>;
76
+ prefix: Schema<string, string>;
77
+ keyFilename: Schema<string, string>;
78
+ }>>;
79
+ }>, Schemastery.ObjectT<{
80
+ enabled: Schema<boolean, boolean>;
81
+ host: Schema<string, string>;
82
+ port: Schema<number, number>;
83
+ basePath: Schema<string, string>;
84
+ /** Working directory for agents spawned by A2A tasks. Must be absolute. */
85
+ cwd: Schema<string, string>;
86
+ agent: Schema<Schemastery.ObjectS<{
87
+ provider: Schema<string, string>;
88
+ model: Schema<string, string>;
89
+ /**
90
+ * Agent preset to mount per task (empty = the deployment default when the
91
+ * profile composes a preset roster). The web profile keeps tools inside
92
+ * presets — without one the agent sees an empty tool catalog.
93
+ */
94
+ preset: Schema<string, string>;
95
+ }>, Schemastery.ObjectT<{
96
+ provider: Schema<string, string>;
97
+ model: Schema<string, string>;
98
+ /**
99
+ * Agent preset to mount per task (empty = the deployment default when the
100
+ * profile composes a preset roster). The web profile keeps tools inside
101
+ * presets — without one the agent sees an empty tool catalog.
102
+ */
103
+ preset: Schema<string, string>;
104
+ }>>;
105
+ card: Schema<Schemastery.ObjectS<{
106
+ name: Schema<string, string>;
107
+ description: Schema<string, string>;
108
+ version: Schema<string, string>;
109
+ publicUrl: Schema<string, string>;
110
+ }>, Schemastery.ObjectT<{
111
+ name: Schema<string, string>;
112
+ description: Schema<string, string>;
113
+ version: Schema<string, string>;
114
+ publicUrl: Schema<string, string>;
115
+ }>>;
116
+ /**
117
+ * A2A task state store (task metadata only — conversation history is
118
+ * dsh-storage's ai_messages, not this). 'memory' loses tasks on restart.
119
+ */
120
+ taskStore: Schema<"memory" | "redis" | "gcs", "memory" | "redis" | "gcs">;
121
+ redis: Schema<Schemastery.ObjectS<{
122
+ url: Schema<string, string>;
123
+ keyPrefix: Schema<string, string>;
124
+ ttlSeconds: Schema<number, number>;
125
+ }>, Schemastery.ObjectT<{
126
+ url: Schema<string, string>;
127
+ keyPrefix: Schema<string, string>;
128
+ ttlSeconds: Schema<number, number>;
129
+ }>>;
130
+ gcs: Schema<Schemastery.ObjectS<{
131
+ bucket: Schema<string, string>;
132
+ prefix: Schema<string, string>;
133
+ keyFilename: Schema<string, string>;
134
+ }>, Schemastery.ObjectT<{
135
+ bucket: Schema<string, string>;
136
+ prefix: Schema<string, string>;
137
+ keyFilename: Schema<string, string>;
138
+ }>>;
139
+ }>>;
140
+ export interface A2aPluginConfig {
141
+ enabled: boolean;
142
+ host: string;
143
+ port: number;
144
+ basePath: string;
145
+ cwd: string;
146
+ agent: {
147
+ provider: string;
148
+ model: string;
149
+ preset: string;
150
+ };
151
+ card: {
152
+ name: string;
153
+ description: string;
154
+ version: string;
155
+ publicUrl: string;
156
+ };
157
+ taskStore: 'memory' | 'redis' | 'gcs';
158
+ redis: {
159
+ url: string;
160
+ keyPrefix: string;
161
+ ttlSeconds: number;
162
+ };
163
+ gcs: {
164
+ bucket: string;
165
+ prefix: string;
166
+ keyFilename: string;
167
+ };
168
+ }
169
+ export declare function apply(ctx: Context, config: A2aPluginConfig): Promise<{
170
+ port: number;
171
+ }> | undefined;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * HTTP layer: the @a2a-js/sdk Express middlewares on a plain Express app.
3
+ *
4
+ * - `GET /.well-known/agent-card.json` (and the legacy `agent.json` alias) —
5
+ * agent card, via the SDK's `agentCardHandler`
6
+ * - `POST <basePath>/` — JSON-RPC: A2A 1.0 methods (`SendMessage`,
7
+ * `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`,
8
+ * `SubscribeToTask`, ...) plus the v0.3 spellings (`message/send`, ...)
9
+ * through the SDK's opt-in legacyCompat layer
10
+ *
11
+ * The SSE framing and error envelopes are the SDK's; this layer only binds
12
+ * the server and builds the card.
13
+ */
14
+ import { type AgentExecutor, type TaskStore } from '@a2a-js/sdk/server';
15
+ export interface A2aServerOptions {
16
+ host: string;
17
+ port: number;
18
+ basePath: string;
19
+ card: {
20
+ name: string;
21
+ description: string;
22
+ version: string;
23
+ /** Public base URL advertised in the card, e.g. https://agent.example.com */
24
+ publicUrl?: string;
25
+ };
26
+ executor: AgentExecutor;
27
+ taskStore: TaskStore;
28
+ }
29
+ export interface A2aServer {
30
+ /** The bound port (differs from options.port when 0). */
31
+ port: number;
32
+ close(): Promise<void>;
33
+ }
34
+ export declare function startA2aServer(options: A2aServerOptions): Promise<A2aServer>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * GCS TaskStore for @a2a-js/sdk. `@google-cloud/storage` is an optional peer.
3
+ *
4
+ * Ported from the source project's packages/a2a-server/src/persistence/gcs.ts:
5
+ * tasks/{taskId}/metadata.json.gz — gzipped task state (pre-sanitized by
6
+ * SanitizedTaskStore: no history)
7
+ * tasks/{taskId}/workspace.tar.gz — tar of the task's workspace directory
8
+ *
9
+ * The workspace archive is written explicitly via `archiveWorkspace()` (not
10
+ * wired to any lifecycle event yet — A2A tasks share one configured cwd, so a
11
+ * per-task snapshot needs a per-task workspace story first).
12
+ */
13
+ import type { ListTasksRequest, ListTasksResponse, Task } from '@a2a-js/sdk';
14
+ import type { TaskStore } from '@a2a-js/sdk/server';
15
+ export interface GcsTaskStoreConfig {
16
+ bucket: string;
17
+ /** Object key prefix, default 'tasks' (matches the source layout). */
18
+ prefix?: string;
19
+ /** Path to a service-account key file; omit to use ADC. */
20
+ keyFilename?: string;
21
+ }
22
+ export declare class GcsTaskStore implements TaskStore {
23
+ private config;
24
+ private bucket;
25
+ private prefix;
26
+ constructor(config: GcsTaskStoreConfig);
27
+ init(): Promise<void>;
28
+ save(task: Task): Promise<void>;
29
+ load(taskId: string): Promise<Task | undefined>;
30
+ list(params: ListTasksRequest): Promise<ListTasksResponse>;
31
+ /**
32
+ * Tar + gzip the workspace directory to `tasks/{taskId}/workspace.tar.gz`.
33
+ * TODO: replace the tar subprocess with a streaming tar library (e.g. `tar`)
34
+ * to control inclusion and avoid shelling out.
35
+ */
36
+ archiveWorkspace(taskId: string, cwd: string): Promise<void>;
37
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Redis TaskStore for @a2a-js/sdk. `ioredis` is an optional peer dependency.
3
+ *
4
+ * Ported from the source project's packages/a2a-server/src/persistence/redis.ts:
5
+ * task state JSON under `a2a:tasks:{taskId}` with a TTL. Tasks arrive
6
+ * pre-sanitized (metadata shell, no history) from SanitizedTaskStore —
7
+ * conversation history is dsh-storage's job (ai_messages), not this store's.
8
+ */
9
+ import type { ListTasksRequest, ListTasksResponse, Task } from '@a2a-js/sdk';
10
+ import type { TaskStore } from '@a2a-js/sdk/server';
11
+ export interface RedisTaskStoreConfig {
12
+ url: string;
13
+ keyPrefix?: string;
14
+ ttlSeconds?: number;
15
+ }
16
+ export declare class RedisTaskStore implements TaskStore {
17
+ private readonly config;
18
+ private redis;
19
+ private readonly prefix;
20
+ private readonly ttl;
21
+ constructor(config: RedisTaskStoreConfig);
22
+ init(): Promise<void>;
23
+ private taskKey;
24
+ save(task: Task): Promise<void>;
25
+ load(taskId: string): Promise<Task | undefined>;
26
+ list(params: ListTasksRequest): Promise<ListTasksResponse>;
27
+ close(): Promise<void>;
28
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * TaskStore boundary for the SDK's DefaultRequestHandler.
3
+ *
4
+ * The SDK's ResultManager calls `taskStore.save()` on EVERY task/statusUpdate
5
+ * event — including each streamed text delta — and folds status messages into
6
+ * `task.history`. Two constraints follow from this plugin's design:
7
+ *
8
+ * 1. A2A stores carry task STATE only (task metadata ≠ conversation history —
9
+ * history is dsh-storage's `ai_messages`). {@link sanitizeTask} strips
10
+ * `history`/`artifacts` before anything reaches a backend.
11
+ * 2. Token-rate saves would hammer Redis and put a GCS upload in the hot path
12
+ * of the SSE stream (ResultManager awaits every save). {@link
13
+ * SanitizedTaskStore} collapses saves to task-state CHANGES, which are the
14
+ * only transitions a `tasks/get` reader can act on anyway.
15
+ *
16
+ * A2A 1.0 added `list()` to the TaskStore contract; {@link listShells} holds
17
+ * the shared filter/sort/paginate logic so the backends only enumerate shells.
18
+ */
19
+ import type { ListTasksRequest, ListTasksResponse, Task } from '@a2a-js/sdk';
20
+ import type { ServerCallContext, TaskStore } from '@a2a-js/sdk/server';
21
+ /** The persistent shape: a metadata shell with empty history/artifacts. */
22
+ export declare function sanitizeTask(task: Task): Task;
23
+ /** TaskStore with the optional lifecycle our backends add. */
24
+ export interface ManagedTaskStore extends TaskStore {
25
+ init?(): Promise<void>;
26
+ close?(): Promise<void>;
27
+ }
28
+ /** Strips history/artifacts and saves only on task-state transitions. */
29
+ export declare class SanitizedTaskStore implements ManagedTaskStore {
30
+ private readonly inner;
31
+ private readonly lastState;
32
+ constructor(inner: ManagedTaskStore);
33
+ init(): Promise<void>;
34
+ close(): Promise<void>;
35
+ save(task: Task, context: ServerCallContext): Promise<void>;
36
+ load(taskId: string, context: ServerCallContext): Promise<Task | undefined>;
37
+ list(params: ListTasksRequest, context: ServerCallContext): Promise<ListTasksResponse>;
38
+ }
39
+ /**
40
+ * Shared `listTasks` semantics for the backends: filter by contextId / status /
41
+ * statusTimestampAfter (strictly greater), newest status first, cursor-paginated
42
+ * (base64 "timestamp|id"; a well-formed but unknown cursor yields an empty page,
43
+ * a malformed token throws). Follows the SDK's InMemoryTaskStore.
44
+ */
45
+ export declare function listShells(shells: Task[], params: ListTasksRequest): ListTasksResponse;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Pure translation layer: dsh session events → A2A protocol events.
3
+ *
4
+ * One {@link SessionTranslator} serves one A2A task (one dsh session). It is
5
+ * deliberately free of cordis/HTTP concerns so the mapping is unit-testable
6
+ * without a harness. The mapping ports the source project's
7
+ * `packages/a2a-server/src/agent/task.ts` event switch onto dsh's
8
+ * `SessionEventMap` (verified against @deepseek-ai/dsh-session@0.1.0-rc.7),
9
+ * emitting the A2A 1.0 data model (@a2a-js/sdk 1.1.0):
10
+ *
11
+ * turn/start → statusUpdate(WORKING), ids rotate
12
+ * assistant/chunk (text-delta) → statusUpdate(WORKING, text part),
13
+ * reusing the turn's messageId so
14
+ * clients aggregate deltas into one message
15
+ * assistant/chunk (reasoning-delta) → statusUpdate(WORKING, thought metadata)
16
+ * assistant/message → no event; text + usage captured for the
17
+ * turn-final message (blocking `SendMessage`
18
+ * clients read the answer from the final
19
+ * task status, not from streamed deltas)
20
+ * tool/call → statusUpdate(WORKING, data part)
21
+ * tool/result → statusUpdate(WORKING, data part)
22
+ * turn/end completed | max-tokens → statusUpdate(INPUT_REQUIRED) — the task
23
+ * is a conversation and stays continuable
24
+ * (COMPLETED is terminal in A2A and the SDK
25
+ * refuses follow-ups to terminal tasks)
26
+ * turn/end aborted → statusUpdate(CANCELED)
27
+ * turn/end blocked | error → statusUpdate(FAILED)
28
+ * everything else → no event
29
+ *
30
+ * A2A 1.0 removed the `final` flag: the SDK's event queue stops on terminal
31
+ * and interrupted (input-required) states, which is exactly the set above.
32
+ *
33
+ * dsh rc.7 has no approval/`input-required` mid-turn seam (the unpublished
34
+ * dsh-user-approval package is not installed anywhere; `tools/pre-execute`'s
35
+ * `ask` degrades to deny without an approval service), so no approval bridge
36
+ * exists here. TODO(verify) when dsh ships one.
37
+ */
38
+ import { type Message } from '@a2a-js/sdk';
39
+ import { type AgentExecutionEvent } from '@a2a-js/sdk/server';
40
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
41
+ /** One agent-role text message — the single construction site for the plugin's message payloads. */
42
+ export declare function agentTextMessage(taskId: string, contextId: string, text: string, messageId?: string): Message;
43
+ /** A terminal (stream-closing) statusUpdate — shared by the bridge's settle path and the executor's failure/cancel paths. */
44
+ export declare function terminalStatusUpdate(taskId: string, contextId: string, state: 'canceled' | 'failed', text: string): AgentExecutionEvent;
45
+ export declare class SessionTranslator {
46
+ private readonly taskId;
47
+ private readonly contextId;
48
+ private readonly model?;
49
+ /** messageId shared by this turn's streamed text deltas (client-side aggregation key). */
50
+ private textMessageId;
51
+ private thoughtMessageId;
52
+ /** Authoritative turn text, assembled from `assistant/message` events. */
53
+ private turnText;
54
+ /** Fallback turn text, accumulated from `text-delta` chunks. */
55
+ private deltaText;
56
+ private sawAssistantMessage;
57
+ private usage;
58
+ /** callId → tool name, for naming `tool/result` payloads. */
59
+ private readonly toolNames;
60
+ constructor(taskId: string, contextId: string, model?: string | undefined);
61
+ /** Translate one session event into zero or more A2A events. Never throws on unknown types. */
62
+ handle(event: SessionEvent): AgentExecutionEvent[];
63
+ private turnEnd;
64
+ private status;
65
+ private dataMessage;
66
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@amaster.ai/dsh-a2a",
3
+ "version": "0.1.0",
4
+ "description": "A2A protocol (JSON-RPC + SSE) server plugin for DeepSeek Harness: expose dsh agents as A2A agents.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "lib",
17
+ "cordis.patch.yml",
18
+ "preview.png"
19
+ ],
20
+ "license": "MIT",
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/TGYD-helige/dsh-plugins.git",
27
+ "directory": "packages/dsh-a2a"
28
+ },
29
+ "dsh": {
30
+ "bundle": {
31
+ "patch": "./cordis.patch.yml"
32
+ }
33
+ },
34
+ "dependencies": {
35
+ "@a2a-js/sdk": "^1.1.0",
36
+ "@deepseek-ai/schemastery": "^3.18.1",
37
+ "express": "^4.21.0"
38
+ },
39
+ "peerDependencies": {
40
+ "@deepseek-ai/cordis": "^4.0.1",
41
+ "@deepseek-ai/dsh-agent": "0.1.0-rc.7",
42
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.7",
43
+ "@deepseek-ai/dsh-session": "0.1.0-rc.7",
44
+ "@google-cloud/storage": "^7.0.0",
45
+ "ioredis": "^5.0.0"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "@google-cloud/storage": {
49
+ "optional": true
50
+ },
51
+ "ioredis": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "devDependencies": {
56
+ "@deepseek-ai/cordis": "^4.0.1",
57
+ "@deepseek-ai/dsh-agent": "0.1.0-rc.7",
58
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.7",
59
+ "@deepseek-ai/dsh-session": "0.1.0-rc.7",
60
+ "@google-cloud/storage": "^7.0.0",
61
+ "@types/express": "^4.17.21",
62
+ "@types/node": "^22.0.0",
63
+ "ioredis": "^5.0.0",
64
+ "typescript": "^5.6.3",
65
+ "vitest": "^4.1.11"
66
+ },
67
+ "engines": {
68
+ "node": "^20.19 || ^22.12 || >=24.0"
69
+ },
70
+ "scripts": {
71
+ "build": "tsc -p tsconfig.build.json",
72
+ "typecheck": "tsc -p tsconfig.json --noEmit",
73
+ "test": "vitest run"
74
+ }
75
+ }
package/preview.png ADDED
Binary file