opc-agent 4.1.0 → 4.1.2
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/.github/ISSUE_TEMPLATE/bug_report.md +20 -20
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -14
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -13
- package/CHANGELOG.md +48 -48
- package/CONTRIBUTING.md +36 -36
- package/README.zh-CN.md +497 -497
- package/USABILITY-ISSUES.md +73 -0
- package/dist/channels/web.js +8 -2
- package/dist/channels/wechat.js +6 -6
- package/dist/cli.js +200 -85
- package/dist/core/runtime.js +37 -15
- package/dist/deploy/index.js +56 -56
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +105 -10
- package/dist/memory/deepbrain.d.ts +1 -1
- package/dist/memory/deepbrain.js +95 -4
- package/dist/scheduler/cron-engine.js +3 -36
- package/dist/studio/server.js +30 -1
- package/dist/studio-ui/index.html +230 -10
- package/dist/ui/components.js +105 -105
- package/examples/README.md +22 -22
- package/examples/basic-agent.ts +90 -90
- package/examples/brain-integration.ts +71 -71
- package/examples/multi-channel.ts +74 -74
- package/fix-sidebar.mjs +188 -188
- package/install.ps1 +154 -154
- package/install.sh +164 -164
- package/package.json +1 -1
- package/scripts/install.ps1 +31 -31
- package/scripts/install.sh +40 -40
- package/serve-studio.js +13 -13
- package/serve-test.js +25 -25
- package/src/channels/dingtalk.ts +46 -46
- package/src/channels/email.ts +351 -351
- package/src/channels/feishu.ts +349 -349
- package/src/channels/googlechat.ts +42 -42
- package/src/channels/imessage.ts +31 -31
- package/src/channels/irc.ts +82 -82
- package/src/channels/line.ts +32 -32
- package/src/channels/matrix.ts +33 -33
- package/src/channels/mattermost.ts +57 -57
- package/src/channels/msteams.ts +32 -32
- package/src/channels/nostr.ts +32 -32
- package/src/channels/qq.ts +33 -33
- package/src/channels/signal.ts +32 -32
- package/src/channels/sms.ts +33 -33
- package/src/channels/telegram.ts +616 -616
- package/src/channels/twitch.ts +65 -65
- package/src/channels/voice-call.ts +100 -100
- package/src/channels/web.ts +8 -2
- package/src/channels/websocket.ts +399 -399
- package/src/channels/wechat.ts +329 -329
- package/src/channels/whatsapp.ts +32 -32
- package/src/cli/chat.ts +99 -99
- package/src/cli/setup.ts +314 -314
- package/src/cli.ts +195 -92
- package/src/core/agent.ts +476 -476
- package/src/core/api-server.ts +277 -277
- package/src/core/audio.ts +98 -98
- package/src/core/collaboration.ts +275 -275
- package/src/core/context-discovery.ts +85 -85
- package/src/core/context-refs.ts +140 -140
- package/src/core/gateway.ts +106 -106
- package/src/core/heartbeat.ts +51 -51
- package/src/core/hooks.ts +105 -105
- package/src/core/ide-bridge.ts +133 -133
- package/src/core/node-network.ts +86 -86
- package/src/core/profiles.ts +122 -122
- package/src/core/runtime.ts +25 -0
- package/src/core/scheduler.ts +187 -187
- package/src/core/session-manager.ts +137 -137
- package/src/core/subagent.ts +98 -98
- package/src/core/vision.ts +180 -180
- package/src/core/workflow-graph.ts +365 -365
- package/src/daemon.ts +96 -96
- package/src/deploy/index.ts +255 -255
- package/src/doctor.ts +98 -11
- package/src/eval/index.ts +211 -211
- package/src/eval/suites/basic.json +16 -16
- package/src/eval/suites/memory.json +12 -12
- package/src/eval/suites/safety.json +14 -14
- package/src/hub/brain-seed.ts +54 -54
- package/src/hub/client.ts +60 -60
- package/src/mcp/servers/calculator-mcp.ts +65 -65
- package/src/mcp/servers/crypto-mcp.ts +73 -73
- package/src/mcp/servers/database-mcp.ts +72 -72
- package/src/mcp/servers/datetime-mcp.ts +69 -69
- package/src/mcp/servers/filesystem.ts +66 -66
- package/src/mcp/servers/github-mcp.ts +58 -58
- package/src/mcp/servers/index.ts +63 -63
- package/src/mcp/servers/json-mcp.ts +102 -102
- package/src/mcp/servers/memory-mcp.ts +56 -56
- package/src/mcp/servers/regex-mcp.ts +53 -53
- package/src/mcp/servers/web-mcp.ts +49 -49
- package/src/memory/context-compressor.ts +189 -189
- package/src/memory/deepbrain.ts +99 -5
- package/src/memory/seed-loader.ts +212 -212
- package/src/memory/user-profiler.ts +215 -215
- package/src/plugins/content-filter.ts +23 -23
- package/src/plugins/logger.ts +18 -18
- package/src/plugins/rate-limiter.ts +38 -38
- package/src/protocols/a2a/client.ts +132 -132
- package/src/protocols/a2a/index.ts +8 -8
- package/src/protocols/a2a/server.ts +333 -333
- package/src/protocols/a2a/types.ts +88 -88
- package/src/protocols/a2a/utils.ts +50 -50
- package/src/protocols/agui/client.ts +83 -83
- package/src/protocols/agui/index.ts +4 -4
- package/src/protocols/agui/server.ts +218 -218
- package/src/protocols/agui/types.ts +153 -153
- package/src/protocols/index.ts +2 -2
- package/src/protocols/mcp/agent-tools.ts +134 -134
- package/src/protocols/mcp/index.ts +8 -8
- package/src/protocols/mcp/server.ts +262 -262
- package/src/protocols/mcp/types.ts +69 -69
- package/src/providers/index.ts +632 -632
- package/src/publish/index.ts +376 -376
- package/src/scheduler/cron-engine.ts +191 -191
- package/src/scheduler/index.ts +2 -2
- package/src/schema/oad.ts +217 -217
- package/src/security/approval.ts +131 -131
- package/src/security/approvals.ts +143 -143
- package/src/security/elevated.ts +105 -105
- package/src/security/guardrails.ts +248 -248
- package/src/security/index.ts +9 -9
- package/src/security/keys.ts +87 -87
- package/src/security/secrets.ts +129 -129
- package/src/skills/builtin/index.ts +408 -408
- package/src/skills/marketplace.ts +113 -113
- package/src/skills/types.ts +42 -42
- package/src/studio/server.ts +31 -1
- package/src/studio/templates-data.ts +178 -178
- package/src/studio-ui/index.html +230 -10
- package/src/telemetry/index.ts +324 -324
- package/src/tools/builtin/browser.ts +299 -299
- package/src/tools/builtin/datetime.ts +41 -41
- package/src/tools/builtin/file.ts +107 -107
- package/src/tools/builtin/home-assistant.ts +116 -116
- package/src/tools/builtin/rl-tools.ts +243 -243
- package/src/tools/builtin/shell.ts +43 -43
- package/src/tools/builtin/vision.ts +64 -64
- package/src/tools/builtin/web-search.ts +126 -126
- package/src/tools/builtin/web.ts +35 -35
- package/src/tools/document-processor.ts +213 -213
- package/src/tools/image-generator.ts +150 -150
- package/src/tools/integrations/calendar.ts +73 -73
- package/src/tools/integrations/code-exec.ts +39 -39
- package/src/tools/integrations/csv-analyzer.ts +92 -92
- package/src/tools/integrations/database.ts +44 -44
- package/src/tools/integrations/email-send.ts +76 -76
- package/src/tools/integrations/git-tool.ts +42 -42
- package/src/tools/integrations/github-tool.ts +76 -76
- package/src/tools/integrations/image-gen.ts +56 -56
- package/src/tools/integrations/index.ts +92 -92
- package/src/tools/integrations/jira.ts +83 -83
- package/src/tools/integrations/notion.ts +71 -71
- package/src/tools/integrations/npm-tool.ts +48 -48
- package/src/tools/integrations/pdf-reader.ts +58 -58
- package/src/tools/integrations/slack.ts +65 -65
- package/src/tools/integrations/summarizer.ts +49 -49
- package/src/tools/integrations/translator.ts +48 -48
- package/src/tools/integrations/trello.ts +60 -60
- package/src/tools/integrations/vector-search.ts +42 -42
- package/src/tools/integrations/web-scraper.ts +47 -47
- package/src/tools/integrations/web-search.ts +58 -58
- package/src/tools/integrations/webhook.ts +38 -38
- package/src/tools/mcp-client.ts +131 -131
- package/src/tools/web-scraper.ts +179 -179
- package/src/tools/web-search.ts +180 -180
- package/src/ui/components.ts +127 -127
- package/srv-out.txt +1 -1
- package/templates/ecommerce-assistant/README.md +45 -45
- package/templates/ecommerce-assistant/oad.yaml +47 -47
- package/templates/tech-support/README.md +43 -43
- package/templates/tech-support/oad.yaml +45 -45
- package/test-agent/Dockerfile +9 -9
- package/test-agent/README.md +50 -50
- package/test-agent/agent.yaml +23 -23
- package/test-agent/docker-compose.yml +11 -11
- package/test-agent/oad.yaml +31 -31
- package/test-agent/package-lock.json +1492 -1492
- package/test-agent/package.json +17 -17
- package/test-agent/src/index.ts +24 -24
- package/test-agent/src/skills/echo.ts +15 -15
- package/test-agent/tsconfig.json +24 -24
- package/test-full.js +43 -43
- package/test-sidebar.js +22 -22
- package/test-studio3.js +75 -75
- package/test-studio4.js +41 -41
- package/tests/a2a-protocol.test.ts +285 -285
- package/tests/agui-protocol.test.ts +246 -246
- package/tests/api-server.test.ts +148 -148
- package/tests/approvals.test.ts +89 -89
- package/tests/audio.test.ts +40 -40
- package/tests/brain-seed-extended.test.ts +490 -490
- package/tests/brain-seed.test.ts +239 -239
- package/tests/browser.test.ts +179 -179
- package/tests/channels/discord.test.ts +79 -79
- package/tests/channels/email.test.ts +148 -148
- package/tests/channels/feishu.test.ts +123 -123
- package/tests/channels/telegram.test.ts +129 -129
- package/tests/channels/websocket.test.ts +53 -53
- package/tests/channels/wechat.test.ts +170 -170
- package/tests/channels-extra.test.ts +45 -45
- package/tests/chat-cli.test.ts +160 -160
- package/tests/cli.test.ts +46 -46
- package/tests/context-compressor.test.ts +172 -172
- package/tests/context-refs.test.ts +121 -121
- package/tests/cron-engine.test.ts +101 -101
- package/tests/daemon.test.ts +135 -135
- package/tests/deepbrain-wire.test.ts +234 -234
- package/tests/deploy-and-dag.test.ts +196 -196
- package/tests/doctor.test.ts +38 -38
- package/tests/document-processor.test.ts +69 -69
- package/tests/e2e-nocode.test.ts +442 -442
- package/tests/elevated.test.ts +69 -69
- package/tests/eval.test.ts +173 -173
- package/tests/gateway.test.ts +63 -63
- package/tests/guardrails.test.ts +177 -177
- package/tests/home-assistant.test.ts +40 -40
- package/tests/hooks.test.ts +79 -79
- package/tests/ide-bridge.test.ts +38 -38
- package/tests/image-generator.test.ts +84 -84
- package/tests/init-role.test.ts +124 -124
- package/tests/integrations.test.ts +249 -249
- package/tests/mcp-client.test.ts +92 -92
- package/tests/mcp-server.test.ts +178 -178
- package/tests/mcp-servers.test.ts +260 -260
- package/tests/node-network.test.ts +74 -74
- package/tests/plugin-a2a-enhanced.test.ts +230 -230
- package/tests/profiles.test.ts +61 -61
- package/tests/publish.test.ts +231 -231
- package/tests/rl-tools.test.ts +93 -93
- package/tests/sandbox-manager.test.ts +46 -46
- package/tests/scheduler.test.ts +200 -200
- package/tests/secrets.test.ts +107 -107
- package/tests/security-enhanced.test.ts +233 -233
- package/tests/settings-api.test.ts +148 -148
- package/tests/setup.test.ts +73 -73
- package/tests/subagent.test.ts +193 -193
- package/tests/telegram-discord.test.ts +60 -60
- package/tests/telemetry.test.ts +186 -186
- package/tests/user-profiler.test.ts +169 -169
- package/tests/v090-features.test.ts +254 -254
- package/tests/vision.test.ts +61 -61
- package/tests/voice-call.test.ts +47 -47
- package/tests/voice-enhanced.test.ts +169 -169
- package/tests/voice-interaction.test.ts +38 -38
- package/tests/web-search.test.ts +155 -155
- package/tests/workflow-graph.test.ts +279 -279
- package/tutorial/customer-service-agent/README.md +612 -612
- package/tutorial/customer-service-agent/SOUL.md +26 -26
- package/tutorial/customer-service-agent/agent.yaml +63 -63
- package/tutorial/customer-service-agent/package.json +19 -19
- package/tutorial/customer-service-agent/src/index.ts +69 -69
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -27
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -22
- package/tutorial/customer-service-agent/tsconfig.json +14 -14
|
@@ -1,246 +1,246 @@
|
|
|
1
|
-
// AG-UI Protocol Tests
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
-
import {
|
|
4
|
-
AGUIEventEmitter,
|
|
5
|
-
AGUIServer,
|
|
6
|
-
AGUIClient,
|
|
7
|
-
AGUI_EVENT_TYPES,
|
|
8
|
-
isValidEventType,
|
|
9
|
-
} from '../src/protocols/agui';
|
|
10
|
-
import type { AGUIEvent, AGUIRunRequest } from '../src/protocols/agui';
|
|
11
|
-
|
|
12
|
-
// ─── Mock ServerResponse ─────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
function createMockRes() {
|
|
15
|
-
const chunks: string[] = [];
|
|
16
|
-
return {
|
|
17
|
-
chunks,
|
|
18
|
-
writeHead: vi.fn(),
|
|
19
|
-
write: vi.fn((data: string) => { chunks.push(data); return true; }),
|
|
20
|
-
end: vi.fn(),
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function parseSSEChunks(chunks: string[]): AGUIEvent[] {
|
|
25
|
-
return chunks
|
|
26
|
-
.filter(c => c.startsWith('data: '))
|
|
27
|
-
.map(c => JSON.parse(c.slice(6).trim()));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// ─── Tests ───────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
describe('AG-UI Protocol', () => {
|
|
33
|
-
describe('isValidEventType', () => {
|
|
34
|
-
it('should validate known event types', () => {
|
|
35
|
-
expect(isValidEventType('TEXT_MESSAGE_START')).toBe(true);
|
|
36
|
-
expect(isValidEventType('RUN_FINISHED')).toBe(true);
|
|
37
|
-
expect(isValidEventType('CUSTOM')).toBe(true);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should reject unknown event types', () => {
|
|
41
|
-
expect(isValidEventType('UNKNOWN')).toBe(false);
|
|
42
|
-
expect(isValidEventType('')).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should have all 15 event types', () => {
|
|
46
|
-
expect(AGUI_EVENT_TYPES.length).toBe(15);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('AGUIEventEmitter', () => {
|
|
51
|
-
let res: ReturnType<typeof createMockRes>;
|
|
52
|
-
let emitter: AGUIEventEmitter;
|
|
53
|
-
|
|
54
|
-
beforeEach(() => {
|
|
55
|
-
res = createMockRes();
|
|
56
|
-
emitter = new AGUIEventEmitter(res as any);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should emit SSE-formatted events', () => {
|
|
60
|
-
emitter.emit({ type: 'RUN_STARTED', runId: 'r1', timestamp: '2025-01-01T00:00:00Z' });
|
|
61
|
-
expect(res.chunks.length).toBe(1);
|
|
62
|
-
expect(res.chunks[0]).toMatch(/^data: \{.*\}\n\n$/);
|
|
63
|
-
const parsed = JSON.parse(res.chunks[0].slice(6));
|
|
64
|
-
expect(parsed.type).toBe('RUN_STARTED');
|
|
65
|
-
expect(parsed.runId).toBe('r1');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('should emit textStart/textContent/textEnd flow', () => {
|
|
69
|
-
emitter.textStart('msg1');
|
|
70
|
-
emitter.textContent('msg1', 'Hello');
|
|
71
|
-
emitter.textContent('msg1', ' world');
|
|
72
|
-
emitter.textEnd('msg1');
|
|
73
|
-
const events = parseSSEChunks(res.chunks);
|
|
74
|
-
expect(events.length).toBe(4);
|
|
75
|
-
expect(events[0].type).toBe('TEXT_MESSAGE_START');
|
|
76
|
-
expect(events[0].messageId).toBe('msg1');
|
|
77
|
-
expect((events[0] as any).role).toBe('assistant');
|
|
78
|
-
expect(events[1].type).toBe('TEXT_MESSAGE_CONTENT');
|
|
79
|
-
expect((events[1] as any).delta).toBe('Hello');
|
|
80
|
-
expect(events[2].type).toBe('TEXT_MESSAGE_CONTENT');
|
|
81
|
-
expect((events[2] as any).delta).toBe(' world');
|
|
82
|
-
expect(events[3].type).toBe('TEXT_MESSAGE_END');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should emit tool call flow', () => {
|
|
86
|
-
emitter.toolCallStart('tc1', 'search');
|
|
87
|
-
emitter.toolCallArgs('tc1', '{"q":"test"}');
|
|
88
|
-
emitter.toolCallEnd('tc1');
|
|
89
|
-
const events = parseSSEChunks(res.chunks);
|
|
90
|
-
expect(events.length).toBe(3);
|
|
91
|
-
expect(events[0].type).toBe('TOOL_CALL_START');
|
|
92
|
-
expect((events[0] as any).toolCallName).toBe('search');
|
|
93
|
-
expect(events[1].type).toBe('TOOL_CALL_ARGS');
|
|
94
|
-
expect(events[2].type).toBe('TOOL_CALL_END');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should emit runStarted/runFinished', () => {
|
|
98
|
-
emitter.runStarted('r1', 'thread1');
|
|
99
|
-
emitter.runFinished('r1');
|
|
100
|
-
const events = parseSSEChunks(res.chunks);
|
|
101
|
-
expect(events[0].type).toBe('RUN_STARTED');
|
|
102
|
-
expect((events[0] as any).threadId).toBe('thread1');
|
|
103
|
-
expect(events[1].type).toBe('RUN_FINISHED');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should emit runError', () => {
|
|
107
|
-
emitter.runError('r1', 'Something went wrong', 'INTERNAL');
|
|
108
|
-
const events = parseSSEChunks(res.chunks);
|
|
109
|
-
expect(events[0].type).toBe('RUN_ERROR');
|
|
110
|
-
expect((events[0] as any).message).toBe('Something went wrong');
|
|
111
|
-
expect((events[0] as any).code).toBe('INTERNAL');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should emit stateSnapshot', () => {
|
|
115
|
-
emitter.stateSnapshot({ count: 42 });
|
|
116
|
-
const events = parseSSEChunks(res.chunks);
|
|
117
|
-
expect(events[0].type).toBe('STATE_SNAPSHOT');
|
|
118
|
-
expect((events[0] as any).snapshot.count).toBe(42);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('should emit stateDelta', () => {
|
|
122
|
-
emitter.stateDelta([{ op: 'replace', path: '/count', value: 43 }]);
|
|
123
|
-
const events = parseSSEChunks(res.chunks);
|
|
124
|
-
expect(events[0].type).toBe('STATE_DELTA');
|
|
125
|
-
expect((events[0] as any).delta[0].op).toBe('replace');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should not emit after close', () => {
|
|
129
|
-
emitter.close();
|
|
130
|
-
emitter.textStart('msg1');
|
|
131
|
-
expect(res.chunks.length).toBe(0);
|
|
132
|
-
expect(res.end).toHaveBeenCalled();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('should include timestamp on convenience methods', () => {
|
|
136
|
-
emitter.textStart('msg1');
|
|
137
|
-
const events = parseSSEChunks(res.chunks);
|
|
138
|
-
expect(events[0].timestamp).toBeDefined();
|
|
139
|
-
expect(typeof events[0].timestamp).toBe('string');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('should emit step events', () => {
|
|
143
|
-
emitter.stepStarted('s1', 'process');
|
|
144
|
-
emitter.stepFinished('s1');
|
|
145
|
-
const events = parseSSEChunks(res.chunks);
|
|
146
|
-
expect(events[0].type).toBe('STEP_STARTED');
|
|
147
|
-
expect((events[0] as any).stepName).toBe('process');
|
|
148
|
-
expect(events[1].type).toBe('STEP_FINISHED');
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('should emit custom events', () => {
|
|
152
|
-
emitter.custom('my_event', { foo: 'bar' });
|
|
153
|
-
const events = parseSSEChunks(res.chunks);
|
|
154
|
-
expect(events[0].type).toBe('CUSTOM');
|
|
155
|
-
expect((events[0] as any).name).toBe('my_event');
|
|
156
|
-
expect((events[0] as any).value.foo).toBe('bar');
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('should emit messagesSnapshot', () => {
|
|
160
|
-
emitter.messagesSnapshot([{ id: 'm1', role: 'user', content: 'hi' }]);
|
|
161
|
-
const events = parseSSEChunks(res.chunks);
|
|
162
|
-
expect(events[0].type).toBe('MESSAGES_SNAPSHOT');
|
|
163
|
-
expect((events[0] as any).messages[0].content).toBe('hi');
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe('AGUIServer', () => {
|
|
168
|
-
it('should construct with default path', () => {
|
|
169
|
-
const mockAgent = { handleMessage: vi.fn(), handleMessageStream: vi.fn() };
|
|
170
|
-
const server = new AGUIServer(mockAgent as any);
|
|
171
|
-
expect(server).toBeDefined();
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('should handle run request with mock agent', async () => {
|
|
175
|
-
const mockAgent = {
|
|
176
|
-
handleMessage: vi.fn().mockResolvedValue({ content: 'Hello!' }),
|
|
177
|
-
handleMessageStream: async function* () { yield 'Hel'; yield 'lo!'; },
|
|
178
|
-
};
|
|
179
|
-
const server = new AGUIServer(mockAgent as any);
|
|
180
|
-
const res = createMockRes();
|
|
181
|
-
|
|
182
|
-
// Simulate request
|
|
183
|
-
const body = JSON.stringify({
|
|
184
|
-
messages: [{ id: 'msg1', role: 'user', content: 'Hi' }],
|
|
185
|
-
});
|
|
186
|
-
const req = {
|
|
187
|
-
[Symbol.asyncIterator]: async function* () { yield body; },
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
await server.handleRun(req as any, res as any);
|
|
191
|
-
|
|
192
|
-
expect(res.writeHead).toHaveBeenCalledWith(200, expect.objectContaining({
|
|
193
|
-
'Content-Type': 'text/event-stream',
|
|
194
|
-
}));
|
|
195
|
-
|
|
196
|
-
const events = parseSSEChunks(res.chunks);
|
|
197
|
-
const types = events.map(e => e.type);
|
|
198
|
-
expect(types).toContain('RUN_STARTED');
|
|
199
|
-
expect(types).toContain('TEXT_MESSAGE_START');
|
|
200
|
-
expect(types).toContain('TEXT_MESSAGE_CONTENT');
|
|
201
|
-
expect(types).toContain('TEXT_MESSAGE_END');
|
|
202
|
-
expect(types).toContain('RUN_FINISHED');
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('should return 400 for invalid JSON', async () => {
|
|
206
|
-
const mockAgent = { handleMessage: vi.fn() };
|
|
207
|
-
const server = new AGUIServer(mockAgent as any);
|
|
208
|
-
const res = createMockRes();
|
|
209
|
-
const req = { [Symbol.asyncIterator]: async function* () { yield 'not json'; } };
|
|
210
|
-
|
|
211
|
-
await server.handleRun(req as any, res as any);
|
|
212
|
-
expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('should return 400 for missing messages', async () => {
|
|
216
|
-
const mockAgent = { handleMessage: vi.fn() };
|
|
217
|
-
const server = new AGUIServer(mockAgent as any);
|
|
218
|
-
const res = createMockRes();
|
|
219
|
-
const req = { [Symbol.asyncIterator]: async function* () { yield '{}'; } };
|
|
220
|
-
|
|
221
|
-
await server.handleRun(req as any, res as any);
|
|
222
|
-
expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
describe('AGUIClient', () => {
|
|
227
|
-
it('should construct with endpoint', () => {
|
|
228
|
-
const client = new AGUIClient('http://localhost:3000/agui');
|
|
229
|
-
expect(client).toBeDefined();
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('should abort cleanly', () => {
|
|
233
|
-
const client = new AGUIClient('http://localhost:3000/agui');
|
|
234
|
-
// Should not throw when no active request
|
|
235
|
-
client.abort();
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
describe('Protocol list includes agui', () => {
|
|
240
|
-
it('should have agui in AGUI_EVENT_TYPES constants', () => {
|
|
241
|
-
expect(AGUI_EVENT_TYPES).toContain('TEXT_MESSAGE_START');
|
|
242
|
-
expect(AGUI_EVENT_TYPES).toContain('RUN_STARTED');
|
|
243
|
-
expect(AGUI_EVENT_TYPES).toContain('TOOL_CALL_START');
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
});
|
|
1
|
+
// AG-UI Protocol Tests
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
AGUIEventEmitter,
|
|
5
|
+
AGUIServer,
|
|
6
|
+
AGUIClient,
|
|
7
|
+
AGUI_EVENT_TYPES,
|
|
8
|
+
isValidEventType,
|
|
9
|
+
} from '../src/protocols/agui';
|
|
10
|
+
import type { AGUIEvent, AGUIRunRequest } from '../src/protocols/agui';
|
|
11
|
+
|
|
12
|
+
// ─── Mock ServerResponse ─────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function createMockRes() {
|
|
15
|
+
const chunks: string[] = [];
|
|
16
|
+
return {
|
|
17
|
+
chunks,
|
|
18
|
+
writeHead: vi.fn(),
|
|
19
|
+
write: vi.fn((data: string) => { chunks.push(data); return true; }),
|
|
20
|
+
end: vi.fn(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseSSEChunks(chunks: string[]): AGUIEvent[] {
|
|
25
|
+
return chunks
|
|
26
|
+
.filter(c => c.startsWith('data: '))
|
|
27
|
+
.map(c => JSON.parse(c.slice(6).trim()));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Tests ───────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
describe('AG-UI Protocol', () => {
|
|
33
|
+
describe('isValidEventType', () => {
|
|
34
|
+
it('should validate known event types', () => {
|
|
35
|
+
expect(isValidEventType('TEXT_MESSAGE_START')).toBe(true);
|
|
36
|
+
expect(isValidEventType('RUN_FINISHED')).toBe(true);
|
|
37
|
+
expect(isValidEventType('CUSTOM')).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should reject unknown event types', () => {
|
|
41
|
+
expect(isValidEventType('UNKNOWN')).toBe(false);
|
|
42
|
+
expect(isValidEventType('')).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should have all 15 event types', () => {
|
|
46
|
+
expect(AGUI_EVENT_TYPES.length).toBe(15);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('AGUIEventEmitter', () => {
|
|
51
|
+
let res: ReturnType<typeof createMockRes>;
|
|
52
|
+
let emitter: AGUIEventEmitter;
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
res = createMockRes();
|
|
56
|
+
emitter = new AGUIEventEmitter(res as any);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should emit SSE-formatted events', () => {
|
|
60
|
+
emitter.emit({ type: 'RUN_STARTED', runId: 'r1', timestamp: '2025-01-01T00:00:00Z' });
|
|
61
|
+
expect(res.chunks.length).toBe(1);
|
|
62
|
+
expect(res.chunks[0]).toMatch(/^data: \{.*\}\n\n$/);
|
|
63
|
+
const parsed = JSON.parse(res.chunks[0].slice(6));
|
|
64
|
+
expect(parsed.type).toBe('RUN_STARTED');
|
|
65
|
+
expect(parsed.runId).toBe('r1');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should emit textStart/textContent/textEnd flow', () => {
|
|
69
|
+
emitter.textStart('msg1');
|
|
70
|
+
emitter.textContent('msg1', 'Hello');
|
|
71
|
+
emitter.textContent('msg1', ' world');
|
|
72
|
+
emitter.textEnd('msg1');
|
|
73
|
+
const events = parseSSEChunks(res.chunks);
|
|
74
|
+
expect(events.length).toBe(4);
|
|
75
|
+
expect(events[0].type).toBe('TEXT_MESSAGE_START');
|
|
76
|
+
expect(events[0].messageId).toBe('msg1');
|
|
77
|
+
expect((events[0] as any).role).toBe('assistant');
|
|
78
|
+
expect(events[1].type).toBe('TEXT_MESSAGE_CONTENT');
|
|
79
|
+
expect((events[1] as any).delta).toBe('Hello');
|
|
80
|
+
expect(events[2].type).toBe('TEXT_MESSAGE_CONTENT');
|
|
81
|
+
expect((events[2] as any).delta).toBe(' world');
|
|
82
|
+
expect(events[3].type).toBe('TEXT_MESSAGE_END');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should emit tool call flow', () => {
|
|
86
|
+
emitter.toolCallStart('tc1', 'search');
|
|
87
|
+
emitter.toolCallArgs('tc1', '{"q":"test"}');
|
|
88
|
+
emitter.toolCallEnd('tc1');
|
|
89
|
+
const events = parseSSEChunks(res.chunks);
|
|
90
|
+
expect(events.length).toBe(3);
|
|
91
|
+
expect(events[0].type).toBe('TOOL_CALL_START');
|
|
92
|
+
expect((events[0] as any).toolCallName).toBe('search');
|
|
93
|
+
expect(events[1].type).toBe('TOOL_CALL_ARGS');
|
|
94
|
+
expect(events[2].type).toBe('TOOL_CALL_END');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should emit runStarted/runFinished', () => {
|
|
98
|
+
emitter.runStarted('r1', 'thread1');
|
|
99
|
+
emitter.runFinished('r1');
|
|
100
|
+
const events = parseSSEChunks(res.chunks);
|
|
101
|
+
expect(events[0].type).toBe('RUN_STARTED');
|
|
102
|
+
expect((events[0] as any).threadId).toBe('thread1');
|
|
103
|
+
expect(events[1].type).toBe('RUN_FINISHED');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should emit runError', () => {
|
|
107
|
+
emitter.runError('r1', 'Something went wrong', 'INTERNAL');
|
|
108
|
+
const events = parseSSEChunks(res.chunks);
|
|
109
|
+
expect(events[0].type).toBe('RUN_ERROR');
|
|
110
|
+
expect((events[0] as any).message).toBe('Something went wrong');
|
|
111
|
+
expect((events[0] as any).code).toBe('INTERNAL');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should emit stateSnapshot', () => {
|
|
115
|
+
emitter.stateSnapshot({ count: 42 });
|
|
116
|
+
const events = parseSSEChunks(res.chunks);
|
|
117
|
+
expect(events[0].type).toBe('STATE_SNAPSHOT');
|
|
118
|
+
expect((events[0] as any).snapshot.count).toBe(42);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should emit stateDelta', () => {
|
|
122
|
+
emitter.stateDelta([{ op: 'replace', path: '/count', value: 43 }]);
|
|
123
|
+
const events = parseSSEChunks(res.chunks);
|
|
124
|
+
expect(events[0].type).toBe('STATE_DELTA');
|
|
125
|
+
expect((events[0] as any).delta[0].op).toBe('replace');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should not emit after close', () => {
|
|
129
|
+
emitter.close();
|
|
130
|
+
emitter.textStart('msg1');
|
|
131
|
+
expect(res.chunks.length).toBe(0);
|
|
132
|
+
expect(res.end).toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should include timestamp on convenience methods', () => {
|
|
136
|
+
emitter.textStart('msg1');
|
|
137
|
+
const events = parseSSEChunks(res.chunks);
|
|
138
|
+
expect(events[0].timestamp).toBeDefined();
|
|
139
|
+
expect(typeof events[0].timestamp).toBe('string');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should emit step events', () => {
|
|
143
|
+
emitter.stepStarted('s1', 'process');
|
|
144
|
+
emitter.stepFinished('s1');
|
|
145
|
+
const events = parseSSEChunks(res.chunks);
|
|
146
|
+
expect(events[0].type).toBe('STEP_STARTED');
|
|
147
|
+
expect((events[0] as any).stepName).toBe('process');
|
|
148
|
+
expect(events[1].type).toBe('STEP_FINISHED');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should emit custom events', () => {
|
|
152
|
+
emitter.custom('my_event', { foo: 'bar' });
|
|
153
|
+
const events = parseSSEChunks(res.chunks);
|
|
154
|
+
expect(events[0].type).toBe('CUSTOM');
|
|
155
|
+
expect((events[0] as any).name).toBe('my_event');
|
|
156
|
+
expect((events[0] as any).value.foo).toBe('bar');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should emit messagesSnapshot', () => {
|
|
160
|
+
emitter.messagesSnapshot([{ id: 'm1', role: 'user', content: 'hi' }]);
|
|
161
|
+
const events = parseSSEChunks(res.chunks);
|
|
162
|
+
expect(events[0].type).toBe('MESSAGES_SNAPSHOT');
|
|
163
|
+
expect((events[0] as any).messages[0].content).toBe('hi');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('AGUIServer', () => {
|
|
168
|
+
it('should construct with default path', () => {
|
|
169
|
+
const mockAgent = { handleMessage: vi.fn(), handleMessageStream: vi.fn() };
|
|
170
|
+
const server = new AGUIServer(mockAgent as any);
|
|
171
|
+
expect(server).toBeDefined();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should handle run request with mock agent', async () => {
|
|
175
|
+
const mockAgent = {
|
|
176
|
+
handleMessage: vi.fn().mockResolvedValue({ content: 'Hello!' }),
|
|
177
|
+
handleMessageStream: async function* () { yield 'Hel'; yield 'lo!'; },
|
|
178
|
+
};
|
|
179
|
+
const server = new AGUIServer(mockAgent as any);
|
|
180
|
+
const res = createMockRes();
|
|
181
|
+
|
|
182
|
+
// Simulate request
|
|
183
|
+
const body = JSON.stringify({
|
|
184
|
+
messages: [{ id: 'msg1', role: 'user', content: 'Hi' }],
|
|
185
|
+
});
|
|
186
|
+
const req = {
|
|
187
|
+
[Symbol.asyncIterator]: async function* () { yield body; },
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
await server.handleRun(req as any, res as any);
|
|
191
|
+
|
|
192
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.objectContaining({
|
|
193
|
+
'Content-Type': 'text/event-stream',
|
|
194
|
+
}));
|
|
195
|
+
|
|
196
|
+
const events = parseSSEChunks(res.chunks);
|
|
197
|
+
const types = events.map(e => e.type);
|
|
198
|
+
expect(types).toContain('RUN_STARTED');
|
|
199
|
+
expect(types).toContain('TEXT_MESSAGE_START');
|
|
200
|
+
expect(types).toContain('TEXT_MESSAGE_CONTENT');
|
|
201
|
+
expect(types).toContain('TEXT_MESSAGE_END');
|
|
202
|
+
expect(types).toContain('RUN_FINISHED');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should return 400 for invalid JSON', async () => {
|
|
206
|
+
const mockAgent = { handleMessage: vi.fn() };
|
|
207
|
+
const server = new AGUIServer(mockAgent as any);
|
|
208
|
+
const res = createMockRes();
|
|
209
|
+
const req = { [Symbol.asyncIterator]: async function* () { yield 'not json'; } };
|
|
210
|
+
|
|
211
|
+
await server.handleRun(req as any, res as any);
|
|
212
|
+
expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should return 400 for missing messages', async () => {
|
|
216
|
+
const mockAgent = { handleMessage: vi.fn() };
|
|
217
|
+
const server = new AGUIServer(mockAgent as any);
|
|
218
|
+
const res = createMockRes();
|
|
219
|
+
const req = { [Symbol.asyncIterator]: async function* () { yield '{}'; } };
|
|
220
|
+
|
|
221
|
+
await server.handleRun(req as any, res as any);
|
|
222
|
+
expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('AGUIClient', () => {
|
|
227
|
+
it('should construct with endpoint', () => {
|
|
228
|
+
const client = new AGUIClient('http://localhost:3000/agui');
|
|
229
|
+
expect(client).toBeDefined();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should abort cleanly', () => {
|
|
233
|
+
const client = new AGUIClient('http://localhost:3000/agui');
|
|
234
|
+
// Should not throw when no active request
|
|
235
|
+
client.abort();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('Protocol list includes agui', () => {
|
|
240
|
+
it('should have agui in AGUI_EVENT_TYPES constants', () => {
|
|
241
|
+
expect(AGUI_EVENT_TYPES).toContain('TEXT_MESSAGE_START');
|
|
242
|
+
expect(AGUI_EVENT_TYPES).toContain('RUN_STARTED');
|
|
243
|
+
expect(AGUI_EVENT_TYPES).toContain('TOOL_CALL_START');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|