@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,84 @@
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 { execFile } from 'node:child_process';
14
+ import { createReadStream } from 'node:fs';
15
+ import { mkdtemp, rm } from 'node:fs/promises';
16
+ import { tmpdir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ import { pipeline } from 'node:stream/promises';
19
+ import { promisify } from 'node:util';
20
+ import { gunzipSync, gzipSync } from 'node:zlib';
21
+ import { listShells } from '../task-store.js';
22
+ export class GcsTaskStore {
23
+ config;
24
+ bucket = null;
25
+ prefix;
26
+ constructor(config) {
27
+ this.config = config;
28
+ this.prefix = config.prefix ?? 'tasks';
29
+ }
30
+ async init() {
31
+ const mod = await import('@google-cloud/storage');
32
+ const storage = new mod.Storage(this.config.keyFilename ? { keyFilename: this.config.keyFilename } : undefined);
33
+ this.bucket = storage.bucket(this.config.bucket);
34
+ }
35
+ async save(task) {
36
+ if (!this.bucket)
37
+ return;
38
+ await this.bucket
39
+ .file(`${this.prefix}/${task.id}/metadata.json.gz`)
40
+ .save(gzipSync(JSON.stringify(task)), { contentType: 'application/gzip', resumable: false });
41
+ }
42
+ async load(taskId) {
43
+ if (!this.bucket)
44
+ return undefined;
45
+ const file = this.bucket.file(`${this.prefix}/${taskId}/metadata.json.gz`);
46
+ const [exists] = await file.exists();
47
+ if (!exists)
48
+ return undefined;
49
+ const [buf] = await file.download();
50
+ return JSON.parse(gunzipSync(buf).toString('utf8'));
51
+ }
52
+ async list(params) {
53
+ if (!this.bucket)
54
+ return { tasks: [], nextPageToken: '', pageSize: 0, totalSize: 0 };
55
+ const [files] = await this.bucket.getFiles({ prefix: `${this.prefix}/` });
56
+ const shells = [];
57
+ for (const file of files) {
58
+ if (!file.name.endsWith('/metadata.json.gz'))
59
+ continue;
60
+ const [buf] = await file.download();
61
+ shells.push(JSON.parse(gunzipSync(buf).toString('utf8')));
62
+ }
63
+ return listShells(shells, params);
64
+ }
65
+ /**
66
+ * Tar + gzip the workspace directory to `tasks/{taskId}/workspace.tar.gz`.
67
+ * TODO: replace the tar subprocess with a streaming tar library (e.g. `tar`)
68
+ * to control inclusion and avoid shelling out.
69
+ */
70
+ async archiveWorkspace(taskId, cwd) {
71
+ if (!this.bucket)
72
+ return;
73
+ const dir = await mkdtemp(join(tmpdir(), 'dsh-a2a-archive-'));
74
+ const tarball = join(dir, 'workspace.tar.gz');
75
+ try {
76
+ await promisify(execFile)('tar', ['-czf', tarball, '-C', cwd, '.']);
77
+ const dest = this.bucket.file(`${this.prefix}/${taskId}/workspace.tar.gz`);
78
+ await pipeline(createReadStream(tarball), dest.createWriteStream());
79
+ }
80
+ finally {
81
+ await rm(dir, { recursive: true, force: true });
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,58 @@
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 { listShells } from '../task-store.js';
10
+ export class RedisTaskStore {
11
+ config;
12
+ redis = null;
13
+ prefix;
14
+ ttl;
15
+ constructor(config) {
16
+ this.config = config;
17
+ this.prefix = config.keyPrefix ?? 'a2a';
18
+ this.ttl = config.ttlSeconds ?? 86_400;
19
+ }
20
+ async init() {
21
+ const mod = await import('ioredis');
22
+ this.redis = new mod.Redis(this.config.url);
23
+ }
24
+ taskKey(taskId) {
25
+ return `${this.prefix}:tasks:${taskId}`;
26
+ }
27
+ async save(task) {
28
+ if (!this.redis)
29
+ return;
30
+ await this.redis.set(this.taskKey(task.id), JSON.stringify(task), 'EX', this.ttl);
31
+ }
32
+ async load(taskId) {
33
+ if (!this.redis)
34
+ return undefined;
35
+ const raw = await this.redis.get(this.taskKey(taskId));
36
+ return raw ? JSON.parse(raw) : undefined;
37
+ }
38
+ async list(params) {
39
+ if (!this.redis)
40
+ return { tasks: [], nextPageToken: '', pageSize: 0, totalSize: 0 };
41
+ // Task shells are few and TTL-bound — a paged SCAN over the prefix is ample.
42
+ const shells = [];
43
+ let cursor = '0';
44
+ do {
45
+ const [next, keys] = await this.redis.scan(cursor, 'MATCH', `${this.prefix}:tasks:*`, 'COUNT', 100);
46
+ cursor = next;
47
+ for (const key of keys) {
48
+ const raw = await this.redis.get(key);
49
+ if (raw)
50
+ shells.push(JSON.parse(raw));
51
+ }
52
+ } while (cursor !== '0');
53
+ return listShells(shells, params);
54
+ }
55
+ async close() {
56
+ await this.redis?.quit?.();
57
+ }
58
+ }
@@ -0,0 +1,111 @@
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
+ /** The persistent shape: a metadata shell with empty history/artifacts. */
20
+ export function sanitizeTask(task) {
21
+ return {
22
+ id: task.id,
23
+ contextId: task.contextId,
24
+ status: task.status,
25
+ metadata: task.metadata,
26
+ history: [],
27
+ artifacts: [],
28
+ };
29
+ }
30
+ /** Strips history/artifacts and saves only on task-state transitions. */
31
+ export class SanitizedTaskStore {
32
+ inner;
33
+ lastState = new Map();
34
+ constructor(inner) {
35
+ this.inner = inner;
36
+ }
37
+ async init() {
38
+ await this.inner.init?.();
39
+ }
40
+ async close() {
41
+ await this.inner.close?.();
42
+ }
43
+ async save(task, context) {
44
+ const state = task.status?.state;
45
+ if (state !== undefined && this.lastState.get(task.id) === state)
46
+ return;
47
+ if (state !== undefined)
48
+ this.lastState.set(task.id, state);
49
+ await this.inner.save(sanitizeTask(task), context);
50
+ }
51
+ load(taskId, context) {
52
+ return this.inner.load(taskId, context);
53
+ }
54
+ list(params, context) {
55
+ return this.inner.list(params, context);
56
+ }
57
+ }
58
+ /**
59
+ * Shared `listTasks` semantics for the backends: filter by contextId / status /
60
+ * statusTimestampAfter (strictly greater), newest status first, cursor-paginated
61
+ * (base64 "timestamp|id"; a well-formed but unknown cursor yields an empty page,
62
+ * a malformed token throws). Follows the SDK's InMemoryTaskStore.
63
+ */
64
+ export function listShells(shells, params) {
65
+ const filtered = shells.filter((task) => {
66
+ if (params.contextId && task.contextId !== params.contextId)
67
+ return false;
68
+ if (params.status && task.status?.state !== params.status)
69
+ return false;
70
+ if (params.statusTimestampAfter) {
71
+ const timestamp = task.status?.timestamp;
72
+ // Strictly-greater mirrors the SDK's InMemoryTaskStore.
73
+ if (!timestamp || Date.parse(timestamp) <= Date.parse(params.statusTimestampAfter)) {
74
+ return false;
75
+ }
76
+ }
77
+ return true;
78
+ });
79
+ filtered.sort((a, b) => {
80
+ const at = a.status?.timestamp ?? '';
81
+ const bt = b.status?.timestamp ?? '';
82
+ return bt.localeCompare(at) || b.id.localeCompare(a.id);
83
+ });
84
+ const pageSize = Math.min(Math.max(params.pageSize ?? 50, 1), 100);
85
+ let start = 0;
86
+ if (params.pageToken) {
87
+ const decoded = Buffer.from(params.pageToken, 'base64').toString('utf8');
88
+ const sep = decoded.indexOf('|');
89
+ if (sep < 0)
90
+ throw new Error('invalid page token');
91
+ const timestamp = decoded.slice(0, sep);
92
+ const id = decoded.slice(sep + 1);
93
+ // A well-formed cursor pointing at nothing (expired/rotated) is an empty page.
94
+ const index = filtered.findIndex((task) => (task.status?.timestamp ?? '') === timestamp && task.id === id);
95
+ if (index >= 0)
96
+ start = index + 1;
97
+ else
98
+ return { tasks: [], nextPageToken: '', pageSize, totalSize: filtered.length };
99
+ }
100
+ const page = filtered.slice(start, start + pageSize);
101
+ const last = page.at(-1);
102
+ const hasMore = start + pageSize < filtered.length;
103
+ return {
104
+ tasks: page.map((task) => (params.includeArtifacts ? task : { ...task, artifacts: [] })),
105
+ nextPageToken: hasMore && last
106
+ ? Buffer.from(`${last.status?.timestamp ?? ''}|${last.id}`).toString('base64')
107
+ : '',
108
+ pageSize,
109
+ totalSize: filtered.length,
110
+ };
111
+ }
@@ -0,0 +1,244 @@
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 { randomUUID } from 'node:crypto';
39
+ import { Role, TaskState } from '@a2a-js/sdk';
40
+ import { AgentEvent } from '@a2a-js/sdk/server';
41
+ /** One text part in the 1.0 wire model. */
42
+ function textPart(text) {
43
+ return {
44
+ content: { $case: 'text', value: text },
45
+ metadata: undefined,
46
+ filename: '',
47
+ mediaType: 'text/plain',
48
+ };
49
+ }
50
+ /** One structured-data part in the 1.0 wire model. */
51
+ function dataPart(data) {
52
+ return {
53
+ content: { $case: 'data', value: data },
54
+ metadata: undefined,
55
+ filename: '',
56
+ mediaType: 'application/json',
57
+ };
58
+ }
59
+ /** One agent-role text message — the single construction site for the plugin's message payloads. */
60
+ export function agentTextMessage(taskId, contextId, text, messageId = randomUUID()) {
61
+ return {
62
+ messageId,
63
+ contextId,
64
+ taskId,
65
+ role: Role.ROLE_AGENT,
66
+ parts: [textPart(text)],
67
+ metadata: undefined,
68
+ extensions: [],
69
+ referenceTaskIds: [],
70
+ };
71
+ }
72
+ /** A terminal (stream-closing) statusUpdate — shared by the bridge's settle path and the executor's failure/cancel paths. */
73
+ export function terminalStatusUpdate(taskId, contextId, state, text) {
74
+ return AgentEvent.statusUpdate({
75
+ taskId,
76
+ contextId,
77
+ status: {
78
+ state: state === 'canceled' ? TaskState.TASK_STATE_CANCELED : TaskState.TASK_STATE_FAILED,
79
+ message: agentTextMessage(taskId, contextId, text),
80
+ timestamp: new Date().toISOString(),
81
+ },
82
+ metadata: { dshAgent: { kind: 'state-change', reason: state } },
83
+ });
84
+ }
85
+ export class SessionTranslator {
86
+ taskId;
87
+ contextId;
88
+ model;
89
+ /** messageId shared by this turn's streamed text deltas (client-side aggregation key). */
90
+ textMessageId = randomUUID();
91
+ thoughtMessageId = randomUUID();
92
+ /** Authoritative turn text, assembled from `assistant/message` events. */
93
+ turnText = '';
94
+ /** Fallback turn text, accumulated from `text-delta` chunks. */
95
+ deltaText = '';
96
+ sawAssistantMessage = false;
97
+ usage;
98
+ /** callId → tool name, for naming `tool/result` payloads. */
99
+ toolNames = new Map();
100
+ constructor(taskId, contextId, model) {
101
+ this.taskId = taskId;
102
+ this.contextId = contextId;
103
+ this.model = model;
104
+ }
105
+ /** Translate one session event into zero or more A2A events. Never throws on unknown types. */
106
+ handle(event) {
107
+ switch (event.type) {
108
+ case 'turn/start': {
109
+ this.textMessageId = randomUUID();
110
+ this.thoughtMessageId = randomUUID();
111
+ this.turnText = '';
112
+ this.deltaText = '';
113
+ this.sawAssistantMessage = false;
114
+ this.usage = undefined;
115
+ return [this.status(TaskState.TASK_STATE_WORKING, { kind: 'state-change' })];
116
+ }
117
+ case 'assistant/chunk': {
118
+ const chunk = event.data.chunk;
119
+ if (chunk.type === 'text-delta') {
120
+ this.deltaText += chunk.text;
121
+ return [
122
+ this.status(TaskState.TASK_STATE_WORKING, { kind: 'text-content' }, agentTextMessage(this.taskId, this.contextId, chunk.text, this.textMessageId)),
123
+ ];
124
+ }
125
+ if (chunk.type === 'reasoning-delta') {
126
+ return [
127
+ this.status(TaskState.TASK_STATE_WORKING, { kind: 'thought' }, agentTextMessage(this.taskId, this.contextId, chunk.text, this.thoughtMessageId)),
128
+ ];
129
+ }
130
+ return [];
131
+ }
132
+ case 'assistant/message': {
133
+ this.sawAssistantMessage = true;
134
+ for (const block of event.data.message.content) {
135
+ if (block.type === 'text')
136
+ this.turnText += block.text;
137
+ }
138
+ const usage = event.data.usage;
139
+ if (usage)
140
+ this.usage = this.usage ? addUsage(this.usage, usage) : { ...usage };
141
+ return [];
142
+ }
143
+ case 'tool/call': {
144
+ const { callId, name, arguments: args } = event.data;
145
+ this.toolNames.set(callId, name);
146
+ return [
147
+ this.status(TaskState.TASK_STATE_WORKING, { kind: 'tool-call' }, this.dataMessage({ callId, name, arguments: args })),
148
+ ];
149
+ }
150
+ case 'tool/result': {
151
+ const { message, error, meta } = event.data;
152
+ // ToolResultMessage.content is a single tool-result block.
153
+ const block = message.content[0];
154
+ const callId = block?.type === 'tool-result' ? block.toolCallId : undefined;
155
+ return [
156
+ this.status(TaskState.TASK_STATE_WORKING, { kind: 'tool-result' }, this.dataMessage({
157
+ callId,
158
+ name: callId ? this.toolNames.get(callId) : undefined,
159
+ result: block?.type === 'tool-result'
160
+ ? flattenContent(block.content)
161
+ : flattenContent(message.content),
162
+ isError: (block?.type === 'tool-result' && block.isError === true) || error !== undefined,
163
+ ...(error ? { error: `${error.name}: ${error.code}` } : {}),
164
+ ...(meta !== undefined ? { meta } : {}),
165
+ })),
166
+ ];
167
+ }
168
+ case 'turn/end': {
169
+ const final = this.turnEnd(event.data.reason);
170
+ return final ? [final] : [];
171
+ }
172
+ default:
173
+ return [];
174
+ }
175
+ }
176
+ turnEnd(reason) {
177
+ const text = this.sawAssistantMessage ? this.turnText : this.deltaText;
178
+ const finalMessage = text
179
+ ? agentTextMessage(this.taskId, this.contextId, text, this.textMessageId)
180
+ : undefined;
181
+ switch (reason.kind) {
182
+ case 'completed':
183
+ case 'max-tokens':
184
+ // The task stays continuable; the reason records which ceiling hit.
185
+ return this.status(TaskState.TASK_STATE_INPUT_REQUIRED, { kind: 'state-change', reason: reason.kind }, finalMessage);
186
+ case 'aborted':
187
+ return this.status(TaskState.TASK_STATE_CANCELED, { kind: 'state-change', reason: 'aborted' }, undefined);
188
+ case 'blocked':
189
+ return this.status(TaskState.TASK_STATE_FAILED, { kind: 'state-change', reason: 'blocked' }, undefined);
190
+ case 'error': {
191
+ const message = reason.error.message || 'Agent turn failed.';
192
+ return this.status(TaskState.TASK_STATE_FAILED, { kind: 'state-change', reason: 'error' }, agentTextMessage(this.taskId, this.contextId, message), message);
193
+ }
194
+ default:
195
+ // 'interrupted' closes crash-orphaned turns on reload; the live loop
196
+ // never emits it, and unknown merge extensions are ignored too.
197
+ return undefined;
198
+ }
199
+ }
200
+ status(state, dshAgent, message, error) {
201
+ const metadata = { dshAgent };
202
+ if (this.model)
203
+ metadata.model = this.model;
204
+ const terminal = state !== TaskState.TASK_STATE_WORKING;
205
+ if (terminal && this.usage)
206
+ metadata.usage = this.usage;
207
+ if (error)
208
+ metadata.error = error;
209
+ return AgentEvent.statusUpdate({
210
+ taskId: this.taskId,
211
+ contextId: this.contextId,
212
+ status: { state, message, timestamp: new Date().toISOString() },
213
+ metadata,
214
+ });
215
+ }
216
+ dataMessage(data) {
217
+ return {
218
+ messageId: randomUUID(),
219
+ contextId: this.contextId,
220
+ taskId: this.taskId,
221
+ role: Role.ROLE_AGENT,
222
+ parts: [dataPart(data)],
223
+ metadata: undefined,
224
+ extensions: [],
225
+ referenceTaskIds: [],
226
+ };
227
+ }
228
+ }
229
+ function addUsage(a, b) {
230
+ const sum = (x, y) => x === undefined && y === undefined ? undefined : (x ?? 0) + (y ?? 0);
231
+ return {
232
+ inputTokens: a.inputTokens + b.inputTokens,
233
+ outputTokens: a.outputTokens + b.outputTokens,
234
+ cacheReadTokens: sum(a.cacheReadTokens, b.cacheReadTokens),
235
+ cacheWriteTokens: sum(a.cacheWriteTokens, b.cacheWriteTokens),
236
+ reasoningTokens: sum(a.reasoningTokens, b.reasoningTokens),
237
+ };
238
+ }
239
+ /** Flatten tool-result content blocks to display text; non-text blocks serialize as JSON. */
240
+ function flattenContent(content) {
241
+ return content
242
+ .map((block) => (block.type === 'text' ? block.text : JSON.stringify(block)))
243
+ .join('');
244
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Bridges A2A tasks to dsh agents.
3
+ *
4
+ * One A2A task maps to one dsh session/agent; the A2A `contextId` IS the dsh
5
+ * sessionId. The bridge owns every agent it creates (the consumer-handle
6
+ * pattern dsh's own ACP bridge uses) and routes the durable `session/event`
7
+ * stream through a per-task {@link SessionTranslator} onto the executing
8
+ * request's {@link ExecutionEventBus}.
9
+ *
10
+ * Turn settlement: `agent.followup()` is fire-and-forget, so callers await a
11
+ * FIFO waiter resolved by the session's next `turn/end` event (dsh's inbox
12
+ * serializes queued follow-ups into successive turns, so waiter order matches
13
+ * turn order). The final status-update is published before the waiter
14
+ * resolves, so an execute() that returns always landed its events first.
15
+ */
16
+ import type { ExecutionEventBus } from '@a2a-js/sdk/server';
17
+ import type { Context } from '@deepseek-ai/cordis';
18
+ import type { AgentHandle } from '@deepseek-ai/dsh-agent';
19
+ import { SessionId } from '@deepseek-ai/dsh-session/types';
20
+ import { SessionTranslator } from './translator.js';
21
+ export interface BridgeOptions {
22
+ /** Absolute working directory for spawned agents. */
23
+ cwd: string;
24
+ /** Optional provider/model overrides for created agents. */
25
+ agentOptions?: {
26
+ provider?: string;
27
+ model?: string;
28
+ };
29
+ /** Agent preset id; empty/absent = the deployment default (when a roster exists). */
30
+ preset?: string;
31
+ }
32
+ export interface TaskEntry {
33
+ taskId: string;
34
+ sessionId: SessionId;
35
+ handle: AgentHandle;
36
+ translator: SessionTranslator;
37
+ /** The in-flight execute()'s bus; null between turns. */
38
+ bus: ExecutionEventBus | null;
39
+ /** Between turn/start and turn/end. */
40
+ turnActive: boolean;
41
+ /** FIFO turn-end waiters, one per queued/running execute(). */
42
+ settled: Array<() => void>;
43
+ }
44
+ export declare class A2aBridge {
45
+ private readonly ctx;
46
+ private readonly options;
47
+ private readonly tasks;
48
+ private readonly bySession;
49
+ constructor(ctx: Context, options: BridgeOptions);
50
+ /**
51
+ * Resolve the live task entry, creating the session/agent when needed.
52
+ * `freshTask` tells the executor to anchor the SDK's ResultManager with an
53
+ * initial Task event (status-updates for a task id the store never saw are
54
+ * dropped by it). Rebinding by bare contextId counts as fresh: the SDK mints
55
+ * a new task id for `message/send` calls that omit `taskId`.
56
+ */
57
+ ensureTask(taskId: string, contextId: string): Promise<{
58
+ entry: TaskEntry;
59
+ freshTask: boolean;
60
+ }>;
61
+ /** Queue one user-message turn on the task's agent and await its `turn/end`. */
62
+ runTurn(entry: TaskEntry, text: string, bus: ExecutionEventBus): Promise<void>;
63
+ /** Abort the task's active turn; returns the contextId when the task is live. */
64
+ cancel(taskId: string): string | undefined;
65
+ /** Dispose every owned agent (plugin unload / server shutdown). */
66
+ dispose(): Promise<void>;
67
+ private onSessionEvent;
68
+ private onSessionDisposed;
69
+ /** Close an in-flight stream honestly and release every turn waiter. */
70
+ private settleEntry;
71
+ /**
72
+ * Config override wins per field; otherwise the deployment default read from
73
+ * the `agentDefaultModel` service (the entry-point contract — the loop
74
+ * itself does not apply it). Undefined when neither source has a pair
75
+ * (persona-less minimal compositions stay valid).
76
+ */
77
+ private resolveSelection;
78
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The @a2a-js/sdk {@link AgentExecutor} that drives dsh agents through the
3
+ * bridge. One `execute()` = one user-message turn:
4
+ *
5
+ * 1. extract the text prompt (v1 is text-only)
6
+ * 2. ensure the task's session/agent exists (creating it on first contact)
7
+ * 3. publish a `task` event first — A2A 1.0 stream ordering REQUIRES the
8
+ * first event of every execute to be a task or message, including
9
+ * follow-up turns on a known task (the ResultManager merges it with the
10
+ * stored task; fresh tasks get the submitted anchor carrying the user
11
+ * message, continuing tasks get a working snapshot)
12
+ * 4. run the turn; session events stream onto the bus via the bridge
13
+ * 5. on failure publish a failed statusUpdate (plus a failed task anchor if
14
+ * none went out yet) instead of rethrowing — clients get the terminal
15
+ * state in band on both blocking and streaming paths
16
+ *
17
+ * `cancelTask` is an arrow property because the SDK destructures it.
18
+ */
19
+ import type { AgentExecutor, ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
20
+ import type { A2aBridge } from './bridge.js';
21
+ export declare class DshAgentExecutor implements AgentExecutor {
22
+ private readonly bridge;
23
+ constructor(bridge: A2aBridge);
24
+ execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void>;
25
+ cancelTask: (taskId: string, eventBus: ExecutionEventBus) => Promise<void>;
26
+ }