@amaster.ai/dsh-a2a 0.1.17 → 0.1.18
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 +26 -1
- package/lib/index.js +5 -0
- package/lib/server.js +47 -1
- package/lib/types/index.d.ts +18 -0
- package/lib/types/server.d.ts +5 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -25,6 +25,9 @@ Disabled by default. Configure via the profile's `cordis.patch.yml`:
|
|
|
25
25
|
host: 127.0.0.1 # no auth built in — keep loopback or front with a proxy
|
|
26
26
|
port: 41241
|
|
27
27
|
basePath: /a2a
|
|
28
|
+
heartbeat:
|
|
29
|
+
enabled: false # opt in to transport keepalive comments
|
|
30
|
+
intervalMs: 15000 # positive integer, at most 2147483647
|
|
28
31
|
cwd: /srv/agent-workspaces
|
|
29
32
|
uploadsDir: '' # file-part upload root; empty = <OS temp>/dsh-a2a-uploads/<date>
|
|
30
33
|
agent:
|
|
@@ -68,6 +71,28 @@ Disabled by default. Configure via the profile's `cordis.patch.yml`:
|
|
|
68
71
|
- **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.
|
|
69
72
|
- **Clear**: the same-process gateway can call `ctx.get('a2aTasks').clearContext(contextId)` before reporting `messages/clear` success. It cancels and drains the live turn, removes the binding and every TaskStore shell for that context, and returns the removed task IDs. The gateway owns the separate session-surface replacement and client-visible history-ID update.
|
|
70
73
|
|
|
74
|
+
## SSE heartbeats
|
|
75
|
+
|
|
76
|
+
Set `heartbeat.enabled: true` to send the SSE comment `: heartbeat\n\n` every
|
|
77
|
+
`heartbeat.intervalMs` (default 15000 ms). The default is disabled. Intervals
|
|
78
|
+
must be positive integers no greater than 2147483647 ms (Node's timer limit).
|
|
79
|
+
This covers `SendStreamingMessage` / `SubscribeToTask` and their legacy
|
|
80
|
+
`message/stream` / `tasks/resubscribe` equivalents. The timer starts after the
|
|
81
|
+
SDK commits SSE headers; pre-stream validation and failures retain their JSON
|
|
82
|
+
responses. There is no heartbeat while waiting for the SDK's first stream event.
|
|
83
|
+
|
|
84
|
+
Comments contain no JSON payload and are ignored by SSE event parsers. They
|
|
85
|
+
keep an otherwise idle transport active without changing task state, history,
|
|
86
|
+
results, or event IDs. A client whose idle timeout resets only on parsed JSON
|
|
87
|
+
messages will still time out: reset that transport timer on received bytes, or
|
|
88
|
+
configure a longer timeout. Reverse proxies must forward streaming bytes without
|
|
89
|
+
buffering for keepalives to reach the client.
|
|
90
|
+
|
|
91
|
+
Heartbeats are skipped while the response requires drain, with no heartbeat
|
|
92
|
+
queue or catch-up burst. Timers stop on completion, disconnect, response errors,
|
|
93
|
+
and server disposal. Heartbeats require a responsive event loop; they cannot
|
|
94
|
+
repair event-loop starvation or guarantee delivery during backpressure.
|
|
95
|
+
|
|
71
96
|
## Task stores
|
|
72
97
|
|
|
73
98
|
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.
|
|
@@ -84,7 +109,7 @@ dsh ships **no authentication or authorization**. The server binds `127.0.0.1` b
|
|
|
84
109
|
|
|
85
110
|
## Compatibility
|
|
86
111
|
|
|
87
|
-
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.
|
|
112
|
+
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.
|
|
88
113
|
|
|
89
114
|
## License
|
|
90
115
|
|
package/lib/index.js
CHANGED
|
@@ -31,6 +31,10 @@ export const Config = Schema.object({
|
|
|
31
31
|
host: Schema.string().default('127.0.0.1'),
|
|
32
32
|
port: Schema.natural().default(41241),
|
|
33
33
|
basePath: Schema.string().default('/a2a'),
|
|
34
|
+
heartbeat: Schema.object({
|
|
35
|
+
enabled: Schema.boolean().default(false),
|
|
36
|
+
intervalMs: Schema.natural().min(1).max(2_147_483_647).default(15_000),
|
|
37
|
+
}),
|
|
34
38
|
/** Working directory for agents spawned by A2A tasks. Must be absolute. */
|
|
35
39
|
cwd: Schema.string().default(process.cwd()),
|
|
36
40
|
/**
|
|
@@ -131,6 +135,7 @@ export function apply(ctx, config) {
|
|
|
131
135
|
host: config.host,
|
|
132
136
|
port: config.port,
|
|
133
137
|
basePath: config.basePath,
|
|
138
|
+
heartbeat: config.heartbeat,
|
|
134
139
|
card: {
|
|
135
140
|
name: config.card.name,
|
|
136
141
|
description: config.card.description,
|
package/lib/server.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* through the SDK's opt-in legacyCompat layer
|
|
10
10
|
*
|
|
11
11
|
* The SSE framing and error envelopes are the SDK's; this layer only binds
|
|
12
|
-
* the server
|
|
12
|
+
* the server, builds the card, and optionally adds transport heartbeats.
|
|
13
13
|
*/
|
|
14
14
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
15
15
|
import { createServer } from 'node:http';
|
|
@@ -20,6 +20,11 @@ import { DefaultExecutionEventBus, DefaultRequestHandler, ServerCallContext, STA
|
|
|
20
20
|
import { agentCardHandler, jsonRpcHandler, UserBuilder } from '@a2a-js/sdk/server/express';
|
|
21
21
|
import express from 'express';
|
|
22
22
|
export async function startA2aServer(options) {
|
|
23
|
+
const intervalMs = options.heartbeat?.intervalMs ?? 15_000;
|
|
24
|
+
if (!Number.isInteger(intervalMs) || intervalMs < 1 || intervalMs > 2_147_483_647) {
|
|
25
|
+
throw new Error('[dsh-a2a] heartbeat.intervalMs must be an integer from 1 to 2147483647');
|
|
26
|
+
}
|
|
27
|
+
const stopHeartbeats = new Set();
|
|
23
28
|
const base = options.basePath.replace(/\/$/, '');
|
|
24
29
|
const publicUrl = (options.card.publicUrl ?? `http://${options.host}:${options.port}`).replace(/\/$/, '');
|
|
25
30
|
const interfaces = [
|
|
@@ -303,6 +308,45 @@ export async function startA2aServer(options) {
|
|
|
303
308
|
// The SDK's jsonRpcHandler parses bodies with express.json()'s 100kb default;
|
|
304
309
|
// raise the ceiling here — body-parser skips re-parsing an already-read body.
|
|
305
310
|
app.use(express.json({ limit: '16mb' }));
|
|
311
|
+
if (options.heartbeat?.enabled) {
|
|
312
|
+
app.use((_req, res, next) => {
|
|
313
|
+
const flushHeaders = res.flushHeaders;
|
|
314
|
+
// Wait for the SDK to commit SSE headers: validation errors stay JSON.
|
|
315
|
+
res.flushHeaders = function () {
|
|
316
|
+
flushHeaders.call(this);
|
|
317
|
+
if (!String(res.getHeader('content-type')).startsWith('text/event-stream') || timer)
|
|
318
|
+
return;
|
|
319
|
+
timer = setInterval(() => {
|
|
320
|
+
if (res.destroyed || res.writableEnded)
|
|
321
|
+
return stop();
|
|
322
|
+
// A slow client gets no additional heartbeat backlog; retry next tick.
|
|
323
|
+
if (res.writableNeedDrain)
|
|
324
|
+
return;
|
|
325
|
+
try {
|
|
326
|
+
res.write(': heartbeat\n\n');
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
stop();
|
|
330
|
+
console.error('[dsh-a2a] heartbeat write failed:', error);
|
|
331
|
+
}
|
|
332
|
+
}, intervalMs);
|
|
333
|
+
timer.unref();
|
|
334
|
+
stopHeartbeats.add(stop);
|
|
335
|
+
res.once('finish', stop);
|
|
336
|
+
res.once('close', stop);
|
|
337
|
+
res.once('error', stop);
|
|
338
|
+
};
|
|
339
|
+
let timer;
|
|
340
|
+
const stop = () => {
|
|
341
|
+
clearInterval(timer);
|
|
342
|
+
stopHeartbeats.delete(stop);
|
|
343
|
+
res.off('finish', stop);
|
|
344
|
+
res.off('close', stop);
|
|
345
|
+
res.off('error', stop);
|
|
346
|
+
};
|
|
347
|
+
next();
|
|
348
|
+
});
|
|
349
|
+
}
|
|
306
350
|
const cardHandler = agentCardHandler({
|
|
307
351
|
agentCardProvider: requestHandler,
|
|
308
352
|
legacyCompat: { enabled: true },
|
|
@@ -373,6 +417,8 @@ export async function startA2aServer(options) {
|
|
|
373
417
|
return {
|
|
374
418
|
port: server.address().port,
|
|
375
419
|
close: () => new Promise((resolve) => {
|
|
420
|
+
for (const stop of stopHeartbeats)
|
|
421
|
+
stop();
|
|
376
422
|
server.closeAllConnections?.();
|
|
377
423
|
server.close(() => resolve());
|
|
378
424
|
}),
|
package/lib/types/index.d.ts
CHANGED
|
@@ -70,6 +70,13 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
70
70
|
host: Schema<string, string>;
|
|
71
71
|
port: Schema<number, number>;
|
|
72
72
|
basePath: Schema<string, string>;
|
|
73
|
+
heartbeat: Schema<Schemastery.ObjectS<{
|
|
74
|
+
enabled: Schema<boolean, boolean>;
|
|
75
|
+
intervalMs: Schema<number, number>;
|
|
76
|
+
}>, Schemastery.ObjectT<{
|
|
77
|
+
enabled: Schema<boolean, boolean>;
|
|
78
|
+
intervalMs: Schema<number, number>;
|
|
79
|
+
}>>;
|
|
73
80
|
/** Working directory for agents spawned by A2A tasks. Must be absolute. */
|
|
74
81
|
cwd: Schema<string, string>;
|
|
75
82
|
/**
|
|
@@ -135,6 +142,13 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
135
142
|
host: Schema<string, string>;
|
|
136
143
|
port: Schema<number, number>;
|
|
137
144
|
basePath: Schema<string, string>;
|
|
145
|
+
heartbeat: Schema<Schemastery.ObjectS<{
|
|
146
|
+
enabled: Schema<boolean, boolean>;
|
|
147
|
+
intervalMs: Schema<number, number>;
|
|
148
|
+
}>, Schemastery.ObjectT<{
|
|
149
|
+
enabled: Schema<boolean, boolean>;
|
|
150
|
+
intervalMs: Schema<number, number>;
|
|
151
|
+
}>>;
|
|
138
152
|
/** Working directory for agents spawned by A2A tasks. Must be absolute. */
|
|
139
153
|
cwd: Schema<string, string>;
|
|
140
154
|
/**
|
|
@@ -201,6 +215,10 @@ export interface A2aPluginConfig {
|
|
|
201
215
|
host: string;
|
|
202
216
|
port: number;
|
|
203
217
|
basePath: string;
|
|
218
|
+
heartbeat?: {
|
|
219
|
+
enabled: boolean;
|
|
220
|
+
intervalMs: number;
|
|
221
|
+
};
|
|
204
222
|
cwd: string;
|
|
205
223
|
uploadsDir: string;
|
|
206
224
|
agent: {
|
package/lib/types/server.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* through the SDK's opt-in legacyCompat layer
|
|
10
10
|
*
|
|
11
11
|
* The SSE framing and error envelopes are the SDK's; this layer only binds
|
|
12
|
-
* the server
|
|
12
|
+
* the server, builds the card, and optionally adds transport heartbeats.
|
|
13
13
|
*/
|
|
14
14
|
import { type Message } from '@a2a-js/sdk';
|
|
15
15
|
import { type AgentExecutor, type TaskStore } from '@a2a-js/sdk/server';
|
|
@@ -26,6 +26,10 @@ export interface A2aServerOptions {
|
|
|
26
26
|
};
|
|
27
27
|
executor: AgentExecutor;
|
|
28
28
|
taskStore: TaskStore;
|
|
29
|
+
heartbeat?: {
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
intervalMs: number;
|
|
32
|
+
};
|
|
29
33
|
approvalGuard?: (message: Message) => undefined | (() => void);
|
|
30
34
|
beforeWorkingTaskMessage?: (message: Message, requestHeaders: unknown) => Promise<void>;
|
|
31
35
|
}
|
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.18",
|
|
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",
|
|
@@ -62,12 +62,12 @@
|
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
65
|
-
"@deepseek-ai/dsh-agent": "0.2.0-rc.
|
|
66
|
-
"@deepseek-ai/dsh-attachment": "0.2.0-rc.
|
|
67
|
-
"@deepseek-ai/dsh-llm": "0.2.0-rc.
|
|
68
|
-
"@deepseek-ai/dsh-session": "0.2.0-rc.
|
|
69
|
-
"@deepseek-ai/dsh-session-projection": "0.2.0-rc.
|
|
70
|
-
"@deepseek-ai/dsh-user-approval": "0.2.0-rc.
|
|
65
|
+
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-llm": "0.2.0-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-session": "0.2.0-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-session-projection": "0.2.0-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-user-approval": "0.2.0-rc.2",
|
|
71
71
|
"@google-cloud/storage": "^7.0.0",
|
|
72
72
|
"@types/express": "^4.17.21",
|
|
73
73
|
"@types/node": "^22.0.0",
|