@amaster.ai/dsh-a2a 0.1.19 → 0.1.20
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 +1 -1
- package/lib/legacy-stream.js +100 -0
- package/lib/server.js +2 -1
- package/lib/types/legacy-stream.d.ts +4 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Disabled by default. Configure via the profile's `cordis.patch.yml`:
|
|
|
53
53
|
## Endpoints
|
|
54
54
|
|
|
55
55
|
- `GET /.well-known/agent-card.json` — agent card: A2A 1.0 shape for `A2A-Version: 1.0` clients, the 0.3 shape for headerless clients (the legacy `/.well-known/agent.json` path is served as an alias)
|
|
56
|
-
- `POST <basePath>/` — JSON-RPC. v1 methods: `SendMessage` (blocking by default, `configuration.returnImmediately: true` returns after the first event), `SendStreamingMessage` (SSE), `GetTask`, `ListTasks` (filter + cursor pagination), `CancelTask`, `SubscribeToTask` (the current task is the first event; the bus stays alive while the task is interrupted, so interrupted tasks can be re-followed live). v0.3 spellings (`message/send`, `message/stream`, `tasks/get`, `tasks/cancel`, `tasks/resubscribe`) keep working through the compat layer. Legacy `tasks/get` also accepts `contextId` without `id` on this path or root `/`, returning the latest stored task or `result: null` when absent.
|
|
56
|
+
- `POST <basePath>/` — JSON-RPC. v1 methods: `SendMessage` (blocking by default, `configuration.returnImmediately: true` returns after the first event), `SendStreamingMessage` (SSE), `GetTask`, `ListTasks` (filter + cursor pagination), `CancelTask`, `SubscribeToTask` (the current task is the first event; the bus stays alive while the task is interrupted, so interrupted tasks can be re-followed live). v0.3 spellings (`message/send`, `message/stream`, `tasks/get`, `tasks/cancel`, `tasks/resubscribe`) keep working through the compat layer. Legacy SSE status updates that end an interaction carry `final: true`, including `input-required` after a completed turn or an approval wait; this ends the stream without making the task terminal. Resubscribe snapshots keep waiting for future events while a live task bus exists. Legacy `tasks/get` also accepts `contextId` without `id` on this path or root `/`, returning the latest stored task or `result: null` when absent.
|
|
57
57
|
|
|
58
58
|
## Behavior notes
|
|
59
59
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Extensions } from '@a2a-js/sdk';
|
|
2
|
+
import { LegacyJsonRpcTransportHandler } from '@a2a-js/sdk/compat/v0_3/server';
|
|
3
|
+
import { defaultServerCallContextBuilder, validateVersion, } from '@a2a-js/sdk/server';
|
|
4
|
+
import { UserBuilder } from '@a2a-js/sdk/server/express';
|
|
5
|
+
/** SDK 1.1.0 derives legacy final from task terminality, losing interaction endings. */
|
|
6
|
+
export function legacyStreamHandler(requestHandler) {
|
|
7
|
+
const transport = new LegacyJsonRpcTransportHandler(requestHandler);
|
|
8
|
+
return async (req, res, next) => {
|
|
9
|
+
if (req.method !== 'POST' ||
|
|
10
|
+
req.path !== '/' ||
|
|
11
|
+
!req.is('application/json') ||
|
|
12
|
+
!['message/stream', 'tasks/resubscribe'].includes(req.body?.method)) {
|
|
13
|
+
next();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const errorResponse = (error) => ({
|
|
17
|
+
jsonrpc: '2.0',
|
|
18
|
+
id: req.body?.id ?? null,
|
|
19
|
+
error: LegacyJsonRpcTransportHandler.mapToLegacyJSONRPCError(error),
|
|
20
|
+
});
|
|
21
|
+
try {
|
|
22
|
+
const context = defaultServerCallContextBuilder({
|
|
23
|
+
extensions: Extensions.parseServiceParameter(req.header('X-A2A-Extensions') ?? req.header('A2A-Extensions')),
|
|
24
|
+
user: await UserBuilder.noAuthentication(),
|
|
25
|
+
headers: req.headers,
|
|
26
|
+
requestedVersion: req.header('A2A-Version'),
|
|
27
|
+
});
|
|
28
|
+
validateVersion(context.requestedVersion, await requestHandler.getAgentCard(), 'JSONRPC');
|
|
29
|
+
const response = await transport.handle(req.body, context);
|
|
30
|
+
if (!(Symbol.asyncIterator in response)) {
|
|
31
|
+
res.json(response);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const stream = response;
|
|
35
|
+
try {
|
|
36
|
+
const startStream = () => {
|
|
37
|
+
if (context.activatedExtensions)
|
|
38
|
+
res.setHeader('X-A2A-Extensions', Array.from(context.activatedExtensions));
|
|
39
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
40
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
41
|
+
res.setHeader('Connection', 'keep-alive');
|
|
42
|
+
res.flushHeaders();
|
|
43
|
+
};
|
|
44
|
+
// The SDK always streams resubscribe errors; sends validate before SSE headers.
|
|
45
|
+
if (req.body.method === 'tasks/resubscribe')
|
|
46
|
+
startStream();
|
|
47
|
+
const first = await stream.next();
|
|
48
|
+
if (!res.headersSent)
|
|
49
|
+
startStream();
|
|
50
|
+
let last;
|
|
51
|
+
const write = (event) => {
|
|
52
|
+
last = event;
|
|
53
|
+
const result = event.result;
|
|
54
|
+
// INPUT_REQUIRED closes the SDK queue, including real approval waits.
|
|
55
|
+
// A task snapshot on resubscribe does not close it and is left intact.
|
|
56
|
+
if (result?.kind === 'status-update' && result.status?.state === 'input-required')
|
|
57
|
+
result.final = true;
|
|
58
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
59
|
+
return result?.kind === 'status-update' && result.final;
|
|
60
|
+
};
|
|
61
|
+
if (!first.done && write(first.value))
|
|
62
|
+
return;
|
|
63
|
+
for await (const event of stream) {
|
|
64
|
+
if (res.destroyed)
|
|
65
|
+
break;
|
|
66
|
+
if (write(event))
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// A cold subscription has no live bus: the SDK ends after the snapshot.
|
|
70
|
+
const snapshot = last?.result;
|
|
71
|
+
if (!res.destroyed && snapshot?.kind === 'task')
|
|
72
|
+
write({
|
|
73
|
+
...last,
|
|
74
|
+
result: {
|
|
75
|
+
kind: 'status-update',
|
|
76
|
+
taskId: snapshot.id,
|
|
77
|
+
contextId: snapshot.contextId,
|
|
78
|
+
status: snapshot.status,
|
|
79
|
+
metadata: snapshot.metadata,
|
|
80
|
+
final: true,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await stream.return(undefined);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error('[dsh-a2a] legacy stream failed:', error);
|
|
90
|
+
if (!res.headersSent)
|
|
91
|
+
res.json(errorResponse(error));
|
|
92
|
+
else if (!res.destroyed)
|
|
93
|
+
res.write(`event: error\ndata: ${JSON.stringify(errorResponse(error))}\n\n`);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
if (!res.writableEnded)
|
|
97
|
+
res.end();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
package/lib/server.js
CHANGED
|
@@ -19,6 +19,7 @@ import { RequestMalformedError } from '@a2a-js/sdk/errors';
|
|
|
19
19
|
import { DefaultExecutionEventBus, DefaultRequestHandler, ServerCallContext, STATE_HEADERS_KEY, } from '@a2a-js/sdk/server';
|
|
20
20
|
import { agentCardHandler, jsonRpcHandler, UserBuilder } from '@a2a-js/sdk/server/express';
|
|
21
21
|
import express from 'express';
|
|
22
|
+
import { legacyStreamHandler } from './legacy-stream.js';
|
|
22
23
|
export async function startA2aServer(options) {
|
|
23
24
|
const intervalMs = options.heartbeat?.intervalMs ?? 15_000;
|
|
24
25
|
if (!Number.isInteger(intervalMs) || intervalMs < 1 || intervalMs > 2_147_483_647) {
|
|
@@ -437,7 +438,7 @@ export async function startA2aServer(options) {
|
|
|
437
438
|
res.sendStatus(404);
|
|
438
439
|
}, contextLookup, rpcHandler);
|
|
439
440
|
}
|
|
440
|
-
app.use(base, contextLookup, rpcHandler);
|
|
441
|
+
app.use(base, contextLookup, legacyStreamHandler(requestHandler), rpcHandler);
|
|
441
442
|
const server = createServer(app);
|
|
442
443
|
await new Promise((resolve, reject) => {
|
|
443
444
|
server.once('error', reject);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type A2ARequestHandler } from '@a2a-js/sdk/server';
|
|
2
|
+
import type { RequestHandler } from 'express';
|
|
3
|
+
/** SDK 1.1.0 derives legacy final from task terminality, losing interaction endings. */
|
|
4
|
+
export declare function legacyStreamHandler(requestHandler: A2ARequestHandler): RequestHandler;
|
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.20",
|
|
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",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
+
"@a2a-js/sdk-v03": "npm:@a2a-js/sdk@0.3.14",
|
|
64
65
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
65
66
|
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
66
67
|
"@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
|