opc-agent 2.0.0 → 2.0.1
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/dist/channels/email.d.ts +32 -26
- package/dist/channels/email.js +239 -62
- package/dist/channels/feishu.d.ts +21 -6
- package/dist/channels/feishu.js +225 -126
- package/dist/channels/websocket.d.ts +46 -3
- package/dist/channels/websocket.js +306 -37
- package/dist/channels/wechat.d.ts +33 -13
- package/dist/channels/wechat.js +229 -42
- package/dist/cli.js +712 -11
- package/dist/core/a2a.d.ts +17 -0
- package/dist/core/a2a.js +43 -1
- package/dist/core/agent.d.ts +16 -0
- package/dist/core/agent.js +108 -0
- package/dist/core/runtime.d.ts +6 -0
- package/dist/core/runtime.js +161 -2
- package/dist/core/sandbox.d.ts +26 -0
- package/dist/core/sandbox.js +117 -0
- package/dist/core/workflow-graph.d.ts +93 -0
- package/dist/core/workflow-graph.js +247 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +183 -0
- package/dist/eval/index.d.ts +65 -0
- package/dist/eval/index.js +191 -0
- package/dist/index.d.ts +30 -6
- package/dist/index.js +60 -4
- package/dist/plugins/content-filter.d.ts +7 -0
- package/dist/plugins/content-filter.js +25 -0
- package/dist/plugins/index.d.ts +42 -0
- package/dist/plugins/index.js +108 -2
- package/dist/plugins/logger.d.ts +6 -0
- package/dist/plugins/logger.js +20 -0
- package/dist/plugins/rate-limiter.d.ts +7 -0
- package/dist/plugins/rate-limiter.js +35 -0
- package/dist/protocols/a2a/client.d.ts +25 -0
- package/dist/protocols/a2a/client.js +115 -0
- package/dist/protocols/a2a/index.d.ts +6 -0
- package/dist/protocols/a2a/index.js +12 -0
- package/dist/protocols/a2a/server.d.ts +41 -0
- package/dist/protocols/a2a/server.js +295 -0
- package/dist/protocols/a2a/types.d.ts +91 -0
- package/dist/protocols/a2a/types.js +15 -0
- package/dist/protocols/a2a/utils.d.ts +6 -0
- package/dist/protocols/a2a/utils.js +47 -0
- package/dist/protocols/agui/client.d.ts +10 -0
- package/dist/protocols/agui/client.js +75 -0
- package/dist/protocols/agui/index.d.ts +4 -0
- package/dist/protocols/agui/index.js +25 -0
- package/dist/protocols/agui/server.d.ts +37 -0
- package/dist/protocols/agui/server.js +191 -0
- package/dist/protocols/agui/types.d.ts +107 -0
- package/dist/protocols/agui/types.js +17 -0
- package/dist/protocols/index.d.ts +2 -0
- package/dist/protocols/index.js +19 -0
- package/dist/protocols/mcp/agent-tools.d.ts +11 -0
- package/dist/protocols/mcp/agent-tools.js +129 -0
- package/dist/protocols/mcp/index.d.ts +5 -0
- package/dist/protocols/mcp/index.js +11 -0
- package/dist/protocols/mcp/server.d.ts +31 -0
- package/dist/protocols/mcp/server.js +248 -0
- package/dist/protocols/mcp/types.d.ts +92 -0
- package/dist/protocols/mcp/types.js +17 -0
- package/dist/publish/index.d.ts +45 -0
- package/dist/publish/index.js +350 -0
- package/dist/schema/oad.d.ts +682 -65
- package/dist/schema/oad.js +36 -3
- package/dist/security/approval.d.ts +36 -0
- package/dist/security/approval.js +113 -0
- package/dist/security/index.d.ts +4 -0
- package/dist/security/index.js +8 -0
- package/dist/security/keys.d.ts +16 -0
- package/dist/security/keys.js +117 -0
- package/dist/studio/server.d.ts +63 -0
- package/dist/studio/server.js +625 -0
- package/dist/studio-ui/index.html +662 -0
- package/dist/telemetry/index.d.ts +93 -0
- package/dist/telemetry/index.js +285 -0
- package/package.json +5 -3
- package/scripts/install.ps1 +31 -0
- package/scripts/install.sh +40 -0
- package/src/channels/email.ts +351 -177
- package/src/channels/feishu.ts +349 -236
- package/src/channels/websocket.ts +399 -87
- package/src/channels/wechat.ts +329 -149
- package/src/cli.ts +783 -12
- package/src/core/a2a.ts +60 -0
- package/src/core/agent.ts +125 -0
- package/src/core/runtime.ts +127 -0
- package/src/core/sandbox.ts +143 -0
- package/src/core/workflow-graph.ts +365 -0
- package/src/doctor.ts +156 -0
- package/src/eval/index.ts +211 -0
- package/src/eval/suites/basic.json +16 -0
- package/src/eval/suites/memory.json +12 -0
- package/src/eval/suites/safety.json +14 -0
- package/src/index.ts +54 -6
- package/src/plugins/content-filter.ts +23 -0
- package/src/plugins/index.ts +133 -2
- package/src/plugins/logger.ts +18 -0
- package/src/plugins/rate-limiter.ts +38 -0
- package/src/protocols/a2a/client.ts +132 -0
- package/src/protocols/a2a/index.ts +8 -0
- package/src/protocols/a2a/server.ts +333 -0
- package/src/protocols/a2a/types.ts +88 -0
- package/src/protocols/a2a/utils.ts +50 -0
- package/src/protocols/agui/client.ts +83 -0
- package/src/protocols/agui/index.ts +4 -0
- package/src/protocols/agui/server.ts +218 -0
- package/src/protocols/agui/types.ts +153 -0
- package/src/protocols/index.ts +2 -0
- package/src/protocols/mcp/agent-tools.ts +134 -0
- package/src/protocols/mcp/index.ts +8 -0
- package/src/protocols/mcp/server.ts +262 -0
- package/src/protocols/mcp/types.ts +69 -0
- package/src/publish/index.ts +376 -0
- package/src/schema/oad.ts +39 -2
- package/src/security/approval.ts +131 -0
- package/src/security/index.ts +3 -0
- package/src/security/keys.ts +87 -0
- package/src/studio/server.ts +629 -0
- package/src/studio-ui/index.html +662 -0
- package/src/telemetry/index.ts +324 -0
- package/src/types/agent-workstation.d.ts +2 -0
- package/tests/a2a-protocol.test.ts +285 -0
- package/tests/agui-protocol.test.ts +246 -0
- package/tests/channels/discord.test.ts +79 -0
- package/tests/channels/email.test.ts +148 -0
- package/tests/channels/feishu.test.ts +123 -0
- package/tests/channels/telegram.test.ts +129 -0
- package/tests/channels/websocket.test.ts +53 -0
- package/tests/channels/wechat.test.ts +170 -0
- package/tests/chat-cli.test.ts +160 -0
- package/tests/daemon.test.ts +135 -0
- package/tests/deepbrain-wire.test.ts +234 -0
- package/tests/doctor.test.ts +38 -0
- package/tests/eval.test.ts +173 -0
- package/tests/init-role.test.ts +124 -0
- package/tests/mcp-client.test.ts +92 -0
- package/tests/mcp-server.test.ts +178 -0
- package/tests/plugin-a2a-enhanced.test.ts +230 -0
- package/tests/publish.test.ts +231 -0
- package/tests/scheduler.test.ts +200 -0
- package/tests/security-enhanced.test.ts +233 -0
- package/tests/skill-learner.test.ts +161 -0
- package/tests/studio.test.ts +229 -0
- package/tests/subagent.test.ts +63 -0
- package/tests/telemetry.test.ts +186 -0
- package/tests/tools/builtin-extended.test.ts +138 -0
- package/tests/workflow-graph.test.ts +279 -0
- package/tutorial/customer-service-agent/README.md +612 -0
- package/tutorial/customer-service-agent/SOUL.md +26 -0
- package/tutorial/customer-service-agent/agent.yaml +63 -0
- package/tutorial/customer-service-agent/package.json +19 -0
- package/tutorial/customer-service-agent/src/index.ts +69 -0
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -0
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -0
- package/tutorial/customer-service-agent/tsconfig.json +14 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { A2AAgentCard, A2ATask, A2AMessage, JsonRpcResponse } from './types';
|
|
2
|
+
|
|
3
|
+
export class A2AClient {
|
|
4
|
+
private agentUrl: string;
|
|
5
|
+
private auth?: { scheme: string; token: string };
|
|
6
|
+
|
|
7
|
+
constructor(agentUrl: string, auth?: { scheme: string; token: string }) {
|
|
8
|
+
this.agentUrl = agentUrl.endsWith('/') ? agentUrl.slice(0, -1) : agentUrl;
|
|
9
|
+
this.auth = auth;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private getHeaders(): Record<string, string> {
|
|
13
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
14
|
+
if (this.auth) {
|
|
15
|
+
if (this.auth.scheme === 'bearer') {
|
|
16
|
+
headers['Authorization'] = `Bearer ${this.auth.token}`;
|
|
17
|
+
} else if (this.auth.scheme === 'apiKey') {
|
|
18
|
+
headers['X-API-Key'] = this.auth.token;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return headers;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private async rpc(method: string, params?: any): Promise<any> {
|
|
25
|
+
const body = JSON.stringify({
|
|
26
|
+
jsonrpc: '2.0',
|
|
27
|
+
id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
28
|
+
method,
|
|
29
|
+
params,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const res = await fetch(this.agentUrl, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: this.getHeaders(),
|
|
35
|
+
body,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const json: JsonRpcResponse = await res.json() as any;
|
|
39
|
+
if (json.error) {
|
|
40
|
+
const err: any = new Error(json.error.message);
|
|
41
|
+
err.code = json.error.code;
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
return json.result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getAgentCard(): Promise<A2AAgentCard> {
|
|
48
|
+
const res = await fetch(`${this.agentUrl}/.well-known/agent.json`, {
|
|
49
|
+
headers: this.getHeaders(),
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok) throw new Error(`Failed to fetch agent card: ${res.status}`);
|
|
52
|
+
return res.json() as any;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async sendTask(message: A2AMessage, options?: { taskId?: string; sessionId?: string }): Promise<A2ATask> {
|
|
56
|
+
return this.rpc('tasks/send', {
|
|
57
|
+
id: options?.taskId || `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
58
|
+
sessionId: options?.sessionId,
|
|
59
|
+
message,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async sendTaskSubscribe(
|
|
64
|
+
message: A2AMessage,
|
|
65
|
+
onEvent: (event: any) => void,
|
|
66
|
+
options?: { taskId?: string },
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
const body = JSON.stringify({
|
|
69
|
+
jsonrpc: '2.0',
|
|
70
|
+
id: `${Date.now()}`,
|
|
71
|
+
method: 'tasks/sendSubscribe',
|
|
72
|
+
params: {
|
|
73
|
+
id: options?.taskId || `task_${Date.now()}`,
|
|
74
|
+
message,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const res = await fetch(this.agentUrl, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: this.getHeaders(),
|
|
81
|
+
body,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!res.ok || !res.body) throw new Error(`SSE failed: ${res.status}`);
|
|
85
|
+
|
|
86
|
+
const reader = res.body.getReader();
|
|
87
|
+
const decoder = new TextDecoder();
|
|
88
|
+
let buffer = '';
|
|
89
|
+
|
|
90
|
+
while (true) {
|
|
91
|
+
const { done, value } = await reader.read();
|
|
92
|
+
if (done) break;
|
|
93
|
+
buffer += decoder.decode(value, { stream: true });
|
|
94
|
+
|
|
95
|
+
const lines = buffer.split('\n');
|
|
96
|
+
buffer = lines.pop() || '';
|
|
97
|
+
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
if (line.startsWith('data: ')) {
|
|
100
|
+
try {
|
|
101
|
+
onEvent(JSON.parse(line.slice(6)));
|
|
102
|
+
} catch { /* skip malformed */ }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async getTask(taskId: string): Promise<A2ATask> {
|
|
109
|
+
return this.rpc('tasks/get', { id: taskId });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async cancelTask(taskId: string): Promise<A2ATask> {
|
|
113
|
+
return this.rpc('tasks/cancel', { id: taskId });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async sendText(text: string, options?: { taskId?: string }): Promise<string> {
|
|
117
|
+
const task = await this.sendTask(
|
|
118
|
+
{ role: 'user', parts: [{ type: 'text', text }] },
|
|
119
|
+
options,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Extract text from last agent message
|
|
123
|
+
const agentMessages = task.history.filter(m => m.role === 'agent');
|
|
124
|
+
const last = agentMessages[agentMessages.length - 1];
|
|
125
|
+
if (!last) return '';
|
|
126
|
+
|
|
127
|
+
return last.parts
|
|
128
|
+
.filter(p => p.type === 'text')
|
|
129
|
+
.map(p => (p as any).text)
|
|
130
|
+
.join('\n');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
A2AAgentCard, A2AAgentSkill, A2ATask, A2ATaskStatus, A2ATaskState,
|
|
3
|
+
A2AMessage, A2AMessagePart, A2AArtifact, JsonRpcRequest, JsonRpcResponse,
|
|
4
|
+
} from './types';
|
|
5
|
+
export { JSON_RPC_ERRORS } from './types';
|
|
6
|
+
export { A2AServer } from './server';
|
|
7
|
+
export { A2AClient } from './client';
|
|
8
|
+
export { oadToAgentCard } from './utils';
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { createServer, IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
import type {
|
|
3
|
+
A2AAgentCard, A2ATask, A2ATaskStatus, A2AMessage, A2AArtifact,
|
|
4
|
+
JsonRpcRequest, JsonRpcResponse,
|
|
5
|
+
} from './types';
|
|
6
|
+
import { JSON_RPC_ERRORS } from './types';
|
|
7
|
+
import { oadToAgentCard } from './utils';
|
|
8
|
+
|
|
9
|
+
export class A2AServer {
|
|
10
|
+
private tasks: Map<string, A2ATask> = new Map();
|
|
11
|
+
private agent: any;
|
|
12
|
+
private card: A2AAgentCard;
|
|
13
|
+
private server: any;
|
|
14
|
+
private taskHandler?: (task: A2ATask) => Promise<A2ATask>;
|
|
15
|
+
|
|
16
|
+
constructor(agent: any, config?: { card?: Partial<A2AAgentCard>; oad?: any; port?: number }) {
|
|
17
|
+
this.agent = agent;
|
|
18
|
+
|
|
19
|
+
// Build card from OAD if available, then overlay explicit config
|
|
20
|
+
const baseCard = config?.oad
|
|
21
|
+
? oadToAgentCard(config.oad, config?.card?.url || `http://localhost:${config?.port || 3001}`)
|
|
22
|
+
: {
|
|
23
|
+
name: agent?.name || 'opc-agent',
|
|
24
|
+
description: agent?.config?.systemPrompt?.slice(0, 200) || 'OPC Agent',
|
|
25
|
+
url: `http://localhost:${config?.port || 3001}`,
|
|
26
|
+
version: '1.0.0',
|
|
27
|
+
capabilities: { streaming: true, pushNotifications: false, stateTransitionHistory: true },
|
|
28
|
+
skills: [],
|
|
29
|
+
defaultInputModes: ['text'],
|
|
30
|
+
defaultOutputModes: ['text'],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
this.card = { ...baseCard, ...config?.card } as A2AAgentCard;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Set custom handler for processing tasks */
|
|
37
|
+
onTask(handler: (task: A2ATask) => Promise<A2ATask>): void {
|
|
38
|
+
this.taskHandler = handler;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getAgentCard(): A2AAgentCard {
|
|
42
|
+
return this.card;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getTasks(): A2ATask[] {
|
|
46
|
+
return Array.from(this.tasks.values());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Mount A2A routes on an existing HTTP server handler */
|
|
50
|
+
mount(handleRequest: (req: IncomingMessage, res: ServerResponse) => void): (req: IncomingMessage, res: ServerResponse) => void {
|
|
51
|
+
return (req: IncomingMessage, res: ServerResponse) => {
|
|
52
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
53
|
+
|
|
54
|
+
// /.well-known/agent.json
|
|
55
|
+
if (url.pathname === '/.well-known/agent.json' && req.method === 'GET') {
|
|
56
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
57
|
+
res.end(JSON.stringify(this.card, null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// JSON-RPC endpoint
|
|
62
|
+
if (url.pathname === '/' && req.method === 'POST') {
|
|
63
|
+
this.handleHTTP(req, res);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fall through to original handler
|
|
68
|
+
handleRequest(req, res);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async start(port: number): Promise<void> {
|
|
73
|
+
this.card.url = `http://localhost:${port}`;
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
this.server = createServer((req, res) => {
|
|
76
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
77
|
+
|
|
78
|
+
if (url.pathname === '/.well-known/agent.json' && req.method === 'GET') {
|
|
79
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
80
|
+
res.end(JSON.stringify(this.card, null, 2));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (url.pathname === '/' && req.method === 'POST') {
|
|
85
|
+
this.handleHTTP(req, res);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
res.writeHead(404);
|
|
90
|
+
res.end('Not Found');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
this.server.listen(port, () => resolve());
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async stop(): Promise<void> {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
if (this.server) {
|
|
100
|
+
this.server.close(() => resolve());
|
|
101
|
+
} else {
|
|
102
|
+
resolve();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async handleHTTP(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
108
|
+
const body = await this.readBody(req);
|
|
109
|
+
let rpcReq: JsonRpcRequest;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
rpcReq = JSON.parse(body);
|
|
113
|
+
} catch {
|
|
114
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
115
|
+
res.end(JSON.stringify(this.rpcError(null, JSON_RPC_ERRORS.PARSE_ERROR, 'Parse error')));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!rpcReq.jsonrpc || rpcReq.jsonrpc !== '2.0' || !rpcReq.method) {
|
|
120
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
121
|
+
res.end(JSON.stringify(this.rpcError(rpcReq?.id ?? null, JSON_RPC_ERRORS.INVALID_REQUEST, 'Invalid Request')));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// SSE for streaming
|
|
126
|
+
if (rpcReq.method === 'tasks/sendSubscribe') {
|
|
127
|
+
res.writeHead(200, {
|
|
128
|
+
'Content-Type': 'text/event-stream',
|
|
129
|
+
'Cache-Control': 'no-cache',
|
|
130
|
+
'Connection': 'keep-alive',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await this.tasksSendSubscribe(rpcReq.params, (event: any) => {
|
|
135
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
136
|
+
});
|
|
137
|
+
} catch (err: any) {
|
|
138
|
+
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
|
|
139
|
+
}
|
|
140
|
+
res.end();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const result = await this.handleRPC(rpcReq.method, rpcReq.params, rpcReq.id);
|
|
145
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
146
|
+
res.end(JSON.stringify(result));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async handleRPC(method: string, params: any, id: string | number | null): Promise<JsonRpcResponse> {
|
|
150
|
+
try {
|
|
151
|
+
let result: any;
|
|
152
|
+
switch (method) {
|
|
153
|
+
case 'tasks/send':
|
|
154
|
+
result = await this.tasksSend(params);
|
|
155
|
+
break;
|
|
156
|
+
case 'tasks/get':
|
|
157
|
+
result = await this.tasksGet(params);
|
|
158
|
+
break;
|
|
159
|
+
case 'tasks/cancel':
|
|
160
|
+
result = await this.tasksCancel(params);
|
|
161
|
+
break;
|
|
162
|
+
default:
|
|
163
|
+
return this.rpcError(id, JSON_RPC_ERRORS.METHOD_NOT_FOUND, `Method not found: ${method}`);
|
|
164
|
+
}
|
|
165
|
+
return { jsonrpc: '2.0', id: id!, result };
|
|
166
|
+
} catch (err: any) {
|
|
167
|
+
if (err.code) {
|
|
168
|
+
return this.rpcError(id, err.code, err.message);
|
|
169
|
+
}
|
|
170
|
+
return this.rpcError(id, JSON_RPC_ERRORS.INTERNAL_ERROR, err.message);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async tasksSend(params: { id: string; sessionId?: string; message: A2AMessage; metadata?: any }): Promise<A2ATask> {
|
|
175
|
+
const taskId = params.id;
|
|
176
|
+
const sessionId = params.sessionId || `session_${Date.now()}`;
|
|
177
|
+
|
|
178
|
+
let task = this.tasks.get(taskId);
|
|
179
|
+
if (!task) {
|
|
180
|
+
task = {
|
|
181
|
+
id: taskId,
|
|
182
|
+
sessionId,
|
|
183
|
+
status: { state: 'submitted', timestamp: new Date().toISOString() },
|
|
184
|
+
history: [],
|
|
185
|
+
artifacts: [],
|
|
186
|
+
metadata: params.metadata,
|
|
187
|
+
};
|
|
188
|
+
this.tasks.set(taskId, task);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Add user message to history
|
|
192
|
+
task.history.push(params.message);
|
|
193
|
+
task.status = { state: 'working', timestamp: new Date().toISOString() };
|
|
194
|
+
|
|
195
|
+
// Process with custom handler or agent
|
|
196
|
+
if (this.taskHandler) {
|
|
197
|
+
task = await this.taskHandler(task);
|
|
198
|
+
this.tasks.set(taskId, task);
|
|
199
|
+
return task;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Default: use agent.handleMessage if available
|
|
203
|
+
if (this.agent?.handleMessage) {
|
|
204
|
+
try {
|
|
205
|
+
const textContent = params.message.parts
|
|
206
|
+
.filter((p: any) => p.type === 'text')
|
|
207
|
+
.map((p: any) => p.text)
|
|
208
|
+
.join('\n');
|
|
209
|
+
|
|
210
|
+
const response = await this.agent.handleMessage({
|
|
211
|
+
id: taskId,
|
|
212
|
+
role: 'user',
|
|
213
|
+
content: textContent,
|
|
214
|
+
timestamp: Date.now(),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const agentMessage: A2AMessage = {
|
|
218
|
+
role: 'agent',
|
|
219
|
+
parts: [{ type: 'text', text: response.content }],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
task.history.push(agentMessage);
|
|
223
|
+
task.status = { state: 'completed', message: agentMessage, timestamp: new Date().toISOString() };
|
|
224
|
+
} catch (err: any) {
|
|
225
|
+
task.status = {
|
|
226
|
+
state: 'failed',
|
|
227
|
+
message: { role: 'agent', parts: [{ type: 'text', text: err.message }] },
|
|
228
|
+
timestamp: new Date().toISOString(),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// No agent — just mark completed with echo
|
|
233
|
+
const agentMessage: A2AMessage = {
|
|
234
|
+
role: 'agent',
|
|
235
|
+
parts: [{ type: 'text', text: 'No agent handler configured' }],
|
|
236
|
+
};
|
|
237
|
+
task.history.push(agentMessage);
|
|
238
|
+
task.status = { state: 'completed', message: agentMessage, timestamp: new Date().toISOString() };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.tasks.set(taskId, task);
|
|
242
|
+
return task;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async tasksSendSubscribe(params: any, onEvent: (event: any) => void): Promise<void> {
|
|
246
|
+
const taskId = params.id;
|
|
247
|
+
const sessionId = params.sessionId || `session_${Date.now()}`;
|
|
248
|
+
|
|
249
|
+
let task: A2ATask = {
|
|
250
|
+
id: taskId,
|
|
251
|
+
sessionId,
|
|
252
|
+
status: { state: 'submitted', timestamp: new Date().toISOString() },
|
|
253
|
+
history: [],
|
|
254
|
+
artifacts: [],
|
|
255
|
+
metadata: params.metadata,
|
|
256
|
+
};
|
|
257
|
+
this.tasks.set(taskId, task);
|
|
258
|
+
task.history.push(params.message);
|
|
259
|
+
|
|
260
|
+
// Emit submitted
|
|
261
|
+
onEvent({ jsonrpc: '2.0', result: { id: taskId, status: task.status } });
|
|
262
|
+
|
|
263
|
+
task.status = { state: 'working', timestamp: new Date().toISOString() };
|
|
264
|
+
onEvent({ jsonrpc: '2.0', result: { id: taskId, status: task.status } });
|
|
265
|
+
|
|
266
|
+
// Process
|
|
267
|
+
if (this.agent?.handleMessage) {
|
|
268
|
+
const textContent = params.message.parts
|
|
269
|
+
.filter((p: any) => p.type === 'text')
|
|
270
|
+
.map((p: any) => p.text)
|
|
271
|
+
.join('\n');
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const response = await this.agent.handleMessage({
|
|
275
|
+
id: taskId, role: 'user', content: textContent, timestamp: Date.now(),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const agentMessage: A2AMessage = { role: 'agent', parts: [{ type: 'text', text: response.content }] };
|
|
279
|
+
task.history.push(agentMessage);
|
|
280
|
+
task.status = { state: 'completed', message: agentMessage, timestamp: new Date().toISOString() };
|
|
281
|
+
} catch (err: any) {
|
|
282
|
+
task.status = { state: 'failed', message: { role: 'agent', parts: [{ type: 'text', text: err.message }] }, timestamp: new Date().toISOString() };
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
const msg: A2AMessage = { role: 'agent', parts: [{ type: 'text', text: 'No agent handler' }] };
|
|
286
|
+
task.history.push(msg);
|
|
287
|
+
task.status = { state: 'completed', message: msg, timestamp: new Date().toISOString() };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this.tasks.set(taskId, task);
|
|
291
|
+
onEvent({ jsonrpc: '2.0', result: { id: taskId, status: task.status, history: task.history } });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async tasksGet(params: { id: string; historyLength?: number }): Promise<A2ATask> {
|
|
295
|
+
const task = this.tasks.get(params.id);
|
|
296
|
+
if (!task) {
|
|
297
|
+
const err: any = new Error(`Task not found: ${params.id}`);
|
|
298
|
+
err.code = JSON_RPC_ERRORS.TASK_NOT_FOUND;
|
|
299
|
+
throw err;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (params.historyLength !== undefined) {
|
|
303
|
+
return { ...task, history: task.history.slice(-params.historyLength) };
|
|
304
|
+
}
|
|
305
|
+
return task;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async tasksCancel(params: { id: string }): Promise<A2ATask> {
|
|
309
|
+
const task = this.tasks.get(params.id);
|
|
310
|
+
if (!task) {
|
|
311
|
+
const err: any = new Error(`Task not found: ${params.id}`);
|
|
312
|
+
err.code = JSON_RPC_ERRORS.TASK_NOT_FOUND;
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
task.status = { state: 'canceled', timestamp: new Date().toISOString() };
|
|
317
|
+
this.tasks.set(params.id, task);
|
|
318
|
+
return task;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private rpcError(id: any, code: number, message: string): JsonRpcResponse {
|
|
322
|
+
return { jsonrpc: '2.0', id, error: { code, message } };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private readBody(req: IncomingMessage): Promise<string> {
|
|
326
|
+
return new Promise((resolve, reject) => {
|
|
327
|
+
const chunks: Buffer[] = [];
|
|
328
|
+
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
329
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
|
330
|
+
req.on('error', reject);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Google A2A Protocol Types — https://google.github.io/A2A/
|
|
2
|
+
|
|
3
|
+
export interface A2AAgentCard {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
url: string;
|
|
7
|
+
version: string;
|
|
8
|
+
capabilities: {
|
|
9
|
+
streaming: boolean;
|
|
10
|
+
pushNotifications: boolean;
|
|
11
|
+
stateTransitionHistory: boolean;
|
|
12
|
+
};
|
|
13
|
+
skills: A2AAgentSkill[];
|
|
14
|
+
defaultInputModes: string[];
|
|
15
|
+
defaultOutputModes: string[];
|
|
16
|
+
authentication?: {
|
|
17
|
+
schemes: string[];
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface A2AAgentSkill {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
tags: string[];
|
|
26
|
+
examples?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface A2ATask {
|
|
30
|
+
id: string;
|
|
31
|
+
sessionId: string;
|
|
32
|
+
status: A2ATaskStatus;
|
|
33
|
+
history: A2AMessage[];
|
|
34
|
+
artifacts: A2AArtifact[];
|
|
35
|
+
metadata?: Record<string, any>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type A2ATaskState = 'submitted' | 'working' | 'input-required' | 'completed' | 'canceled' | 'failed';
|
|
39
|
+
|
|
40
|
+
export interface A2ATaskStatus {
|
|
41
|
+
state: A2ATaskState;
|
|
42
|
+
message?: A2AMessage;
|
|
43
|
+
timestamp: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface A2AMessage {
|
|
47
|
+
role: 'user' | 'agent';
|
|
48
|
+
parts: A2AMessagePart[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type A2AMessagePart =
|
|
52
|
+
| { type: 'text'; text: string }
|
|
53
|
+
| { type: 'file'; file: { name: string; mimeType: string; bytes?: string; uri?: string } }
|
|
54
|
+
| { type: 'data'; data: Record<string, any> };
|
|
55
|
+
|
|
56
|
+
export interface A2AArtifact {
|
|
57
|
+
name: string;
|
|
58
|
+
description?: string;
|
|
59
|
+
parts: A2AMessagePart[];
|
|
60
|
+
index: number;
|
|
61
|
+
append?: boolean;
|
|
62
|
+
lastChunk?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface JsonRpcRequest {
|
|
66
|
+
jsonrpc: '2.0';
|
|
67
|
+
id: string | number;
|
|
68
|
+
method: string;
|
|
69
|
+
params?: any;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface JsonRpcResponse {
|
|
73
|
+
jsonrpc: '2.0';
|
|
74
|
+
id: string | number | null;
|
|
75
|
+
result?: any;
|
|
76
|
+
error?: { code: number; message: string; data?: any };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Standard JSON-RPC error codes
|
|
80
|
+
export const JSON_RPC_ERRORS = {
|
|
81
|
+
PARSE_ERROR: -32700,
|
|
82
|
+
INVALID_REQUEST: -32600,
|
|
83
|
+
METHOD_NOT_FOUND: -32601,
|
|
84
|
+
INVALID_PARAMS: -32602,
|
|
85
|
+
INTERNAL_ERROR: -32603,
|
|
86
|
+
TASK_NOT_FOUND: -32001,
|
|
87
|
+
TASK_CANCELED: -32002,
|
|
88
|
+
} as const;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { A2AAgentCard, A2AAgentSkill } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert an OAD (Open Agent Definition) document to an A2A AgentCard.
|
|
5
|
+
*/
|
|
6
|
+
export function oadToAgentCard(oad: any, baseUrl: string): A2AAgentCard {
|
|
7
|
+
const meta = oad?.metadata || {};
|
|
8
|
+
const spec = oad?.spec || {};
|
|
9
|
+
|
|
10
|
+
// Extract skills from OAD
|
|
11
|
+
const skills: A2AAgentSkill[] = (spec.skills || []).map((s: any, i: number) => ({
|
|
12
|
+
id: s.id || s.name || `skill-${i}`,
|
|
13
|
+
name: s.name || `Skill ${i}`,
|
|
14
|
+
description: s.description || '',
|
|
15
|
+
tags: s.tags || [],
|
|
16
|
+
examples: s.examples || [],
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// If no skills defined, create one from the agent description
|
|
20
|
+
if (skills.length === 0 && (spec.systemPrompt || meta.description)) {
|
|
21
|
+
skills.push({
|
|
22
|
+
id: 'default',
|
|
23
|
+
name: meta.name || 'default',
|
|
24
|
+
description: meta.description || spec.systemPrompt?.slice(0, 200) || 'General agent capability',
|
|
25
|
+
tags: ['general'],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Detect capabilities from OAD
|
|
30
|
+
const channels = spec.channels || [];
|
|
31
|
+
const hasStreaming = channels.some((c: any) => c.type === 'websocket' || c.type === 'web');
|
|
32
|
+
|
|
33
|
+
const url = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
name: meta.name || 'opc-agent',
|
|
37
|
+
description: meta.description || '',
|
|
38
|
+
url,
|
|
39
|
+
version: meta.version || '1.0.0',
|
|
40
|
+
capabilities: {
|
|
41
|
+
streaming: hasStreaming,
|
|
42
|
+
pushNotifications: false,
|
|
43
|
+
stateTransitionHistory: true,
|
|
44
|
+
},
|
|
45
|
+
skills,
|
|
46
|
+
defaultInputModes: ['text'],
|
|
47
|
+
defaultOutputModes: ['text'],
|
|
48
|
+
authentication: spec.auth ? { schemes: [spec.auth.type || 'bearer'] } : undefined,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// AG-UI Client — Connects to AG-UI SSE endpoint
|
|
2
|
+
import type { AGUIEvent, AGUIRunRequest, AGUIMessage } from './types';
|
|
3
|
+
import { isValidEventType } from './types';
|
|
4
|
+
|
|
5
|
+
export class AGUIClient {
|
|
6
|
+
private endpoint: string;
|
|
7
|
+
private controller?: AbortController;
|
|
8
|
+
|
|
9
|
+
constructor(endpoint: string) {
|
|
10
|
+
this.endpoint = endpoint;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async run(request: AGUIRunRequest, onEvent: (event: AGUIEvent) => void): Promise<void> {
|
|
14
|
+
this.controller = new AbortController();
|
|
15
|
+
|
|
16
|
+
const res = await fetch(this.endpoint, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
body: JSON.stringify(request),
|
|
20
|
+
signal: this.controller.signal,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
throw new Error(`AG-UI request failed: ${res.status} ${res.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!res.body) {
|
|
28
|
+
throw new Error('No response body');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const reader = res.body.getReader();
|
|
32
|
+
const decoder = new TextDecoder();
|
|
33
|
+
let buffer = '';
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
while (true) {
|
|
37
|
+
const { done, value } = await reader.read();
|
|
38
|
+
if (done) break;
|
|
39
|
+
|
|
40
|
+
buffer += decoder.decode(value, { stream: true });
|
|
41
|
+
const lines = buffer.split('\n');
|
|
42
|
+
buffer = lines.pop() || '';
|
|
43
|
+
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
if (line.startsWith('data: ')) {
|
|
46
|
+
try {
|
|
47
|
+
const event: AGUIEvent = JSON.parse(line.slice(6));
|
|
48
|
+
if (event.type && isValidEventType(event.type)) {
|
|
49
|
+
onEvent(event);
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// skip malformed events
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
reader.releaseLock();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async sendText(text: string, onChunk: (text: string) => void): Promise<string> {
|
|
63
|
+
let fullText = '';
|
|
64
|
+
const request: AGUIRunRequest = {
|
|
65
|
+
messages: [{ id: `msg_${Date.now()}`, role: 'user', content: text }],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
await this.run(request, (event) => {
|
|
69
|
+
if (event.type === 'TEXT_MESSAGE_CONTENT') {
|
|
70
|
+
const delta = (event as any).delta as string;
|
|
71
|
+
fullText += delta;
|
|
72
|
+
onChunk(delta);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return fullText;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
abort(): void {
|
|
80
|
+
this.controller?.abort();
|
|
81
|
+
this.controller = undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|