@machinemetrics/agent-event-protocol 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.
- package/README.md +274 -0
- package/dist/chunk-RSG7CUUT.js +220 -0
- package/dist/chunk-RSG7CUUT.js.map +1 -0
- package/dist/chunk-WZD2VBMM.js +180 -0
- package/dist/chunk-WZD2VBMM.js.map +1 -0
- package/dist/chunk-ZINGYSGU.js +19 -0
- package/dist/chunk-ZINGYSGU.js.map +1 -0
- package/dist/core.cjs +241 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +463 -0
- package/dist/core.d.ts +463 -0
- package/dist/core.js +47 -0
- package/dist/core.js.map +1 -0
- package/dist/index.cjs +458 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/sse.cjs +404 -0
- package/dist/sse.cjs.map +1 -0
- package/dist/sse.d.cts +56 -0
- package/dist/sse.d.ts +56 -0
- package/dist/sse.js +10 -0
- package/dist/sse.js.map +1 -0
- package/dist/types-CttB5bnC.d.cts +323 -0
- package/dist/types-CttB5bnC.d.ts +323 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# @machinemetrics/agent-event-protocol
|
|
2
|
+
|
|
3
|
+
A transport-agnostic event protocol for streaming agent responses. Defines the schema and utilities for real-time communication between AI agents and clients.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @machinemetrics/agent-event-protocol
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Design Principles
|
|
12
|
+
|
|
13
|
+
1. **Simple** - Easy to understand and implement
|
|
14
|
+
2. **Flexible** - Supports future features without breaking changes
|
|
15
|
+
3. **Type-safe** - Strong contracts between client and server
|
|
16
|
+
4. **Observable** - Easy to monitor and debug
|
|
17
|
+
5. **Transport-agnostic** - Same events work over SSE, NDJSON, WebSocket, NATS, etc.
|
|
18
|
+
|
|
19
|
+
## Protocol Overview
|
|
20
|
+
|
|
21
|
+
The protocol defines a streaming event schema for agent responses. All events are self-identifying JSON payloads with common fields:
|
|
22
|
+
|
|
23
|
+
- `event` - Event type identifier
|
|
24
|
+
- `requestId` - Correlates all events to the originating request
|
|
25
|
+
- `timestamp` - ISO 8601 timestamp for observability
|
|
26
|
+
|
|
27
|
+
### Message Flow
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
┌─────────┐ ┌─────────┐
|
|
31
|
+
│ Client │ │ Server │
|
|
32
|
+
└────┬────┘ └────┬────┘
|
|
33
|
+
│ │
|
|
34
|
+
│ POST /v1/agents/{agentName}/stream │
|
|
35
|
+
│ { messages, threadId?, context? } │
|
|
36
|
+
├─────────────────────────────────────────────>│
|
|
37
|
+
│ │
|
|
38
|
+
│ metadata: {threadId, requestId, agentId} │
|
|
39
|
+
│<─────────────────────────────────────────────┤
|
|
40
|
+
│ │
|
|
41
|
+
│ text: {blockId, stage: "start"} │
|
|
42
|
+
│<─────────────────────────────────────────────┤
|
|
43
|
+
│ │
|
|
44
|
+
│ text: {blockId, stage: "delta", delta} │
|
|
45
|
+
│<─────────────────────────────────────────────┤
|
|
46
|
+
│ │
|
|
47
|
+
│ text: {blockId, stage: "stop"} │
|
|
48
|
+
│<─────────────────────────────────────────────┤
|
|
49
|
+
│ │
|
|
50
|
+
│ done: {content, usage, finishReason} │
|
|
51
|
+
│<─────────────────────────────────────────────┤
|
|
52
|
+
│ │
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Client Action Flow
|
|
56
|
+
|
|
57
|
+
When the agent needs client-side action, the stream pauses and resumes via a new request:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
┌─────────┐ ┌─────────┐
|
|
61
|
+
│ Client │ │ Server │
|
|
62
|
+
└────┬────┘ └────┬────┘
|
|
63
|
+
│ │
|
|
64
|
+
│ POST /stream { messages, threadId? } │
|
|
65
|
+
├─────────────────────────────────────────────>│
|
|
66
|
+
│ │
|
|
67
|
+
│ continuation: {actionId, kind, payload} │
|
|
68
|
+
│<─────────────────────────────────────────────┤
|
|
69
|
+
│ │
|
|
70
|
+
│ done: {finishReason: "action_required"} │
|
|
71
|
+
│<─────────────────────────────────────────────┤
|
|
72
|
+
│ │
|
|
73
|
+
│ (client handles action based on kind) │
|
|
74
|
+
│ │
|
|
75
|
+
│ POST /stream { messages, threadId, │
|
|
76
|
+
│ context: { actionResults: [...] }} │
|
|
77
|
+
├─────────────────────────────────────────────>│
|
|
78
|
+
│ │
|
|
79
|
+
│ (agent continues with result) │
|
|
80
|
+
│ │
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Event Types
|
|
84
|
+
|
|
85
|
+
The protocol defines 9 event types covering the complete lifecycle of an agent response.
|
|
86
|
+
|
|
87
|
+
| Event | Description |
|
|
88
|
+
|-------|-------------|
|
|
89
|
+
| `metadata` | Stream initialization with threadId, requestId, agentId |
|
|
90
|
+
| `text` | Streaming text output with stage lifecycle |
|
|
91
|
+
| `thinking` | Model reasoning/thinking content (extended thinking) |
|
|
92
|
+
| `tool_call` | Tool invocation with streaming arguments |
|
|
93
|
+
| `tool_result` | Tool execution result (status and summary only) |
|
|
94
|
+
| `done` | Stream complete with full content array |
|
|
95
|
+
| `error` | Error condition with code and retry guidance |
|
|
96
|
+
| `aborted` | Client cancellation acknowledgment |
|
|
97
|
+
| `continuation` | Pause for client action (tools, approval, input) |
|
|
98
|
+
|
|
99
|
+
### Terminal Events
|
|
100
|
+
|
|
101
|
+
Each request must end with exactly one terminal event: `done`, `error`, or `aborted`.
|
|
102
|
+
|
|
103
|
+
## Event Lifecycle (Stage Pattern)
|
|
104
|
+
|
|
105
|
+
Content events (`text`, `thinking`, `tool_call`, `tool_result`) use a consistent stage pattern:
|
|
106
|
+
|
|
107
|
+
| Stage | Description |
|
|
108
|
+
|-------|-------------|
|
|
109
|
+
| `start` | Block begins, includes `blockIndex` for ordering |
|
|
110
|
+
| `delta` | Content chunk, includes `deltaIndex` for ordering within block |
|
|
111
|
+
| `stop` | Block complete, includes final data for that block type |
|
|
112
|
+
|
|
113
|
+
## Request Schema
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
type AgentRequest = {
|
|
117
|
+
messages: InputMessage[]; // New user input
|
|
118
|
+
threadId?: string; // Resume existing thread
|
|
119
|
+
context?: {
|
|
120
|
+
actionResults?: ActionResult[]; // Results from continuation events
|
|
121
|
+
[key: string]: unknown; // Additional passthrough data
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Only `messages` is required. The server loads thread history from storage via `threadId`.
|
|
127
|
+
|
|
128
|
+
## Content Blocks
|
|
129
|
+
|
|
130
|
+
**User Content Blocks** (in requests):
|
|
131
|
+
- `text` - Plain text input
|
|
132
|
+
|
|
133
|
+
**Assistant Content Blocks** (in responses):
|
|
134
|
+
- `text` - Text output
|
|
135
|
+
- `thinking` - Model reasoning
|
|
136
|
+
- `tool_use` - Tool invocation with args
|
|
137
|
+
- `tool_result` - Tool result status and summary (no full payloads)
|
|
138
|
+
|
|
139
|
+
## Error Handling
|
|
140
|
+
|
|
141
|
+
### Error Codes
|
|
142
|
+
|
|
143
|
+
| Code | Description |
|
|
144
|
+
|------|-------------|
|
|
145
|
+
| `auth_failed` | Authentication failed |
|
|
146
|
+
| `thread_not_found` | Thread does not exist |
|
|
147
|
+
| `thread_access_denied` | Thread ownership violation |
|
|
148
|
+
| `invalid_message_format` | Invalid request format |
|
|
149
|
+
| `message_too_large` | Message exceeds size limit |
|
|
150
|
+
| `rate_limit_exceeded` | Rate limit hit |
|
|
151
|
+
| `stream_timeout` | Stream timed out |
|
|
152
|
+
| `agent_error` | Agent execution error |
|
|
153
|
+
| `tool_error` | Tool execution error |
|
|
154
|
+
| `internal_error` | Internal server error |
|
|
155
|
+
| `invalid_action_id` | Action ID not found or already resolved |
|
|
156
|
+
|
|
157
|
+
### HTTP Status Mapping
|
|
158
|
+
|
|
159
|
+
| Status | Error Codes |
|
|
160
|
+
|--------|-------------|
|
|
161
|
+
| 400 | `invalid_message_format`, `invalid_action_id` |
|
|
162
|
+
| 401 | `auth_failed` |
|
|
163
|
+
| 403 | `thread_access_denied` |
|
|
164
|
+
| 404 | `thread_not_found` |
|
|
165
|
+
| 413 | `message_too_large` |
|
|
166
|
+
| 429 | `rate_limit_exceeded` |
|
|
167
|
+
| 500 | `internal_error` |
|
|
168
|
+
| 504 | `stream_timeout` |
|
|
169
|
+
|
|
170
|
+
### Retry Guidance
|
|
171
|
+
|
|
172
|
+
- `retryable: true` - Client can retry with backoff
|
|
173
|
+
- `retryable: false` - Don't retry, user action required
|
|
174
|
+
|
|
175
|
+
## SSE Transport
|
|
176
|
+
|
|
177
|
+
Server-Sent Events is the default transport implementation.
|
|
178
|
+
|
|
179
|
+
### Wire Format
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
Content-Type: text/event-stream
|
|
183
|
+
Connection: keep-alive
|
|
184
|
+
Cache-Control: no-cache
|
|
185
|
+
|
|
186
|
+
event: metadata
|
|
187
|
+
data: {"event":"metadata","requestId":"req-1","threadId":"th-1","agentId":"agent","timestamp":"..."}
|
|
188
|
+
|
|
189
|
+
event: text
|
|
190
|
+
data: {"event":"text","requestId":"req-1","messageId":"msg-1","blockId":"b0","stage":"start","blockIndex":0,"timestamp":"..."}
|
|
191
|
+
|
|
192
|
+
event: text
|
|
193
|
+
data: {"event":"text","requestId":"req-1","messageId":"msg-1","blockId":"b0","stage":"delta","delta":"Hello","deltaIndex":0,"timestamp":"..."}
|
|
194
|
+
|
|
195
|
+
event: done
|
|
196
|
+
data: {"event":"done","requestId":"req-1","messageId":"msg-1","threadId":"th-1","content":[...],"finishReason":"stop","timestamp":"..."}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Keepalive
|
|
200
|
+
|
|
201
|
+
Servers should send SSE comment lines (`: heartbeat\n\n`) every 15 seconds to prevent proxy timeouts.
|
|
202
|
+
|
|
203
|
+
### Utilities
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { encodeSSE, parseSSEChunk } from '@machinemetrics/agent-event-protocol';
|
|
207
|
+
|
|
208
|
+
// Server: encode events for streaming
|
|
209
|
+
const sseData = encodeSSE('text', { event: 'text', ... });
|
|
210
|
+
|
|
211
|
+
// Client: parse incoming chunks
|
|
212
|
+
const { events, remainder } = parseSSEChunk(chunk);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Extensibility
|
|
216
|
+
|
|
217
|
+
**Adding new events is backward compatible:**
|
|
218
|
+
- Clients should ignore unknown event types
|
|
219
|
+
- New optional fields can be added to existing events
|
|
220
|
+
- No version bump required for additive changes
|
|
221
|
+
|
|
222
|
+
**Breaking changes require new endpoint version:**
|
|
223
|
+
```
|
|
224
|
+
/v1/agents/{agentName}/stream → Current
|
|
225
|
+
/v2/agents/{agentName}/stream → Breaking changes
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Exports
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import {
|
|
232
|
+
// Core types
|
|
233
|
+
InputMessage,
|
|
234
|
+
AgentRequest,
|
|
235
|
+
AgentEvent,
|
|
236
|
+
ActionResult,
|
|
237
|
+
UserContentBlock,
|
|
238
|
+
AssistantContentBlock,
|
|
239
|
+
EventStage,
|
|
240
|
+
MetadataEvent,
|
|
241
|
+
TextEvent,
|
|
242
|
+
ThinkingEvent,
|
|
243
|
+
ToolCallEvent,
|
|
244
|
+
ToolResultEvent,
|
|
245
|
+
DoneEvent,
|
|
246
|
+
ErrorEvent,
|
|
247
|
+
AbortedEvent,
|
|
248
|
+
ContinuationEvent,
|
|
249
|
+
|
|
250
|
+
// Schemas (Zod validators)
|
|
251
|
+
AgentRequestSchema,
|
|
252
|
+
AgentEventSchema,
|
|
253
|
+
// ... all event schemas
|
|
254
|
+
|
|
255
|
+
// SSE utilities
|
|
256
|
+
encodeSSE,
|
|
257
|
+
parseSSEChunk,
|
|
258
|
+
ParsedEvent,
|
|
259
|
+
ParseResult,
|
|
260
|
+
|
|
261
|
+
// Constants
|
|
262
|
+
PROTOCOL_VERSION,
|
|
263
|
+
EVENT_TYPES,
|
|
264
|
+
} from '@machinemetrics/agent-event-protocol';
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Development
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
pnpm install
|
|
271
|
+
pnpm build # Build the package (ESM + CJS)
|
|
272
|
+
pnpm test # Run tests
|
|
273
|
+
pnpm typecheck # Type check
|
|
274
|
+
```
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AgentEventSchema
|
|
3
|
+
} from "./chunk-WZD2VBMM.js";
|
|
4
|
+
|
|
5
|
+
// src/sse/encode.ts
|
|
6
|
+
function encodeSSE(event, data) {
|
|
7
|
+
return `event: ${event}
|
|
8
|
+
data: ${JSON.stringify(data)}
|
|
9
|
+
|
|
10
|
+
`;
|
|
11
|
+
}
|
|
12
|
+
if (import.meta.vitest) {
|
|
13
|
+
const { describe, it, expect } = import.meta.vitest;
|
|
14
|
+
describe("encodeSSE", () => {
|
|
15
|
+
it("formats SSE event correctly", () => {
|
|
16
|
+
const result = encodeSSE("metadata", {
|
|
17
|
+
event: "metadata",
|
|
18
|
+
requestId: "req-1",
|
|
19
|
+
threadId: "th-1",
|
|
20
|
+
agentId: "test-agent",
|
|
21
|
+
timestamp: "2025-01-01T00:00:00.000Z"
|
|
22
|
+
});
|
|
23
|
+
expect(result).toBe(
|
|
24
|
+
'event: metadata\ndata: {"event":"metadata","requestId":"req-1","threadId":"th-1","agentId":"test-agent","timestamp":"2025-01-01T00:00:00.000Z"}\n\n'
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
it("handles special characters in data", () => {
|
|
28
|
+
const result = encodeSSE("text", {
|
|
29
|
+
event: "text",
|
|
30
|
+
requestId: "req-1",
|
|
31
|
+
messageId: "msg-1",
|
|
32
|
+
blockId: "b1",
|
|
33
|
+
stage: "delta",
|
|
34
|
+
delta: "Hello\nWorld",
|
|
35
|
+
deltaIndex: 0,
|
|
36
|
+
timestamp: "2025-01-01T00:00:00.000Z"
|
|
37
|
+
});
|
|
38
|
+
expect(result).toContain("Hello\\nWorld");
|
|
39
|
+
expect(result).toMatch(/^event: text\ndata: .+\n\n$/);
|
|
40
|
+
});
|
|
41
|
+
it("handles unicode characters", () => {
|
|
42
|
+
const result = encodeSSE("text", {
|
|
43
|
+
event: "text",
|
|
44
|
+
requestId: "req-1",
|
|
45
|
+
messageId: "msg-1",
|
|
46
|
+
blockId: "b1",
|
|
47
|
+
stage: "delta",
|
|
48
|
+
delta: "\u4F60\u597D\u4E16\u754C \u{1F30D}",
|
|
49
|
+
deltaIndex: 0,
|
|
50
|
+
timestamp: "2025-01-01T00:00:00.000Z"
|
|
51
|
+
});
|
|
52
|
+
expect(result).toContain("\u4F60\u597D\u4E16\u754C \u{1F30D}");
|
|
53
|
+
});
|
|
54
|
+
it("handles empty object data", () => {
|
|
55
|
+
const result = encodeSSE("custom", {});
|
|
56
|
+
expect(result).toBe("event: custom\ndata: {}\n\n");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/sse/parse.ts
|
|
62
|
+
function parseSSEChunk(chunk) {
|
|
63
|
+
const events = [];
|
|
64
|
+
const blocks = chunk.split("\n\n");
|
|
65
|
+
for (let i = 0; i < blocks.length - 1; i++) {
|
|
66
|
+
const block = blocks[i];
|
|
67
|
+
const parsed = parseBlock(block);
|
|
68
|
+
if (parsed !== null) {
|
|
69
|
+
events.push(parsed);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const lastBlock = blocks[blocks.length - 1] ?? "";
|
|
73
|
+
const remainder = lastBlock;
|
|
74
|
+
return { events, remainder };
|
|
75
|
+
}
|
|
76
|
+
function parseBlock(block) {
|
|
77
|
+
const lines = block.split("\n");
|
|
78
|
+
let eventType = null;
|
|
79
|
+
let data = null;
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
if (line.startsWith(":")) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (line.startsWith("event: ")) {
|
|
85
|
+
eventType = line.slice(7);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (line.startsWith("data: ")) {
|
|
89
|
+
data = line.slice(6);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (data === null) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return parseAndValidate(data, eventType);
|
|
97
|
+
}
|
|
98
|
+
function parseAndValidate(data, eventType) {
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(data);
|
|
102
|
+
} catch {
|
|
103
|
+
return { success: false, error: "Invalid JSON", raw: data };
|
|
104
|
+
}
|
|
105
|
+
const result = AgentEventSchema.safeParse(parsed);
|
|
106
|
+
if (result.success) {
|
|
107
|
+
if (eventType !== null && result.data.event !== eventType) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
error: `Event type mismatch: header says '${eventType}', data says '${result.data.event}'`,
|
|
111
|
+
raw: parsed
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { success: true, event: result.data };
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
error: result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; "),
|
|
119
|
+
raw: parsed
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (import.meta.vitest) {
|
|
123
|
+
const { describe, it, expect } = import.meta.vitest;
|
|
124
|
+
describe("parseSSEChunk", () => {
|
|
125
|
+
it("parses a single complete event", () => {
|
|
126
|
+
const chunk = 'event: metadata\ndata: {"event":"metadata","requestId":"req-1","threadId":"th-1","agentId":"test-agent","timestamp":"2025-01-01T00:00:00.000Z"}\n\n';
|
|
127
|
+
const result = parseSSEChunk(chunk);
|
|
128
|
+
expect(result.events).toHaveLength(1);
|
|
129
|
+
expect(result.events[0]?.success).toBe(true);
|
|
130
|
+
if (result.events[0]?.success) {
|
|
131
|
+
expect(result.events[0].event.event).toBe("metadata");
|
|
132
|
+
expect(result.events[0].event.requestId).toBe("req-1");
|
|
133
|
+
}
|
|
134
|
+
expect(result.remainder).toBe("");
|
|
135
|
+
});
|
|
136
|
+
it("parses multiple events in one chunk", () => {
|
|
137
|
+
const chunk = 'event: text\ndata: {"event":"text","requestId":"req-1","messageId":"msg-1","blockId":"b1","stage":"start","blockIndex":0,"timestamp":"2025-01-01T00:00:00.000Z"}\n\nevent: text\ndata: {"event":"text","requestId":"req-1","messageId":"msg-1","blockId":"b1","stage":"delta","delta":"Hello","deltaIndex":0,"timestamp":"2025-01-01T00:00:00.001Z"}\n\n';
|
|
138
|
+
const result = parseSSEChunk(chunk);
|
|
139
|
+
expect(result.events).toHaveLength(2);
|
|
140
|
+
expect(result.events[0]?.success).toBe(true);
|
|
141
|
+
expect(result.events[1]?.success).toBe(true);
|
|
142
|
+
if (result.events[0]?.success && result.events[1]?.success) {
|
|
143
|
+
expect(result.events[0].event.event).toBe("text");
|
|
144
|
+
expect(result.events[1].event.event).toBe("text");
|
|
145
|
+
}
|
|
146
|
+
expect(result.remainder).toBe("");
|
|
147
|
+
});
|
|
148
|
+
it("returns remainder for partial event", () => {
|
|
149
|
+
const chunk = 'event: metadata\ndata: {"event":"metadata","requestId":"req-1","threadId":"th-1","agentId":"test-agent","timestamp":"2025-01-01T00:00:00.000Z"}\n\nevent: text\ndata: {"event":"text","request';
|
|
150
|
+
const result = parseSSEChunk(chunk);
|
|
151
|
+
expect(result.events).toHaveLength(1);
|
|
152
|
+
expect(result.events[0]?.success).toBe(true);
|
|
153
|
+
expect(result.remainder).toContain("event: text");
|
|
154
|
+
expect(result.remainder).toContain('data: {"event":"text","request');
|
|
155
|
+
});
|
|
156
|
+
it("ignores SSE comments", () => {
|
|
157
|
+
const chunk = ': heartbeat\nevent: metadata\ndata: {"event":"metadata","requestId":"req-1","threadId":"th-1","agentId":"test-agent","timestamp":"2025-01-01T00:00:00.000Z"}\n\n';
|
|
158
|
+
const result = parseSSEChunk(chunk);
|
|
159
|
+
expect(result.events).toHaveLength(1);
|
|
160
|
+
expect(result.events[0]?.success).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
it("returns error for invalid JSON", () => {
|
|
163
|
+
const chunk = "event: text\ndata: {invalid json}\n\n";
|
|
164
|
+
const result = parseSSEChunk(chunk);
|
|
165
|
+
expect(result.events).toHaveLength(1);
|
|
166
|
+
const event = result.events[0];
|
|
167
|
+
expect(event.success).toBe(false);
|
|
168
|
+
if (!event.success) {
|
|
169
|
+
expect(event.error).toBe("Invalid JSON");
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
it("returns error for invalid event schema", () => {
|
|
173
|
+
const chunk = 'event: text\ndata: {"event":"text","invalidField":"value"}\n\n';
|
|
174
|
+
const result = parseSSEChunk(chunk);
|
|
175
|
+
expect(result.events).toHaveLength(1);
|
|
176
|
+
const event = result.events[0];
|
|
177
|
+
expect(event.success).toBe(false);
|
|
178
|
+
if (!event.success) {
|
|
179
|
+
expect(event.error).toContain("requestId");
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
it("returns error for event type mismatch", () => {
|
|
183
|
+
const chunk = 'event: metadata\ndata: {"event":"text","requestId":"req-1","messageId":"msg-1","blockId":"b1","stage":"start","blockIndex":0,"timestamp":"2025-01-01T00:00:00.000Z"}\n\n';
|
|
184
|
+
const result = parseSSEChunk(chunk);
|
|
185
|
+
expect(result.events).toHaveLength(1);
|
|
186
|
+
const event = result.events[0];
|
|
187
|
+
expect(event.success).toBe(false);
|
|
188
|
+
if (!event.success) {
|
|
189
|
+
expect(event.error).toContain("mismatch");
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
it("handles data without event line", () => {
|
|
193
|
+
const chunk = 'data: {"event":"metadata","requestId":"req-1","threadId":"th-1","agentId":"test-agent","timestamp":"2025-01-01T00:00:00.000Z"}\n\n';
|
|
194
|
+
const result = parseSSEChunk(chunk);
|
|
195
|
+
expect(result.events).toHaveLength(1);
|
|
196
|
+
expect(result.events[0]?.success).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
it("handles empty chunk", () => {
|
|
199
|
+
const result = parseSSEChunk("");
|
|
200
|
+
expect(result.events).toHaveLength(0);
|
|
201
|
+
expect(result.remainder).toBe("");
|
|
202
|
+
});
|
|
203
|
+
it("buffers partial chunks correctly across calls", () => {
|
|
204
|
+
const part1 = 'event: text\ndata: {"event":"text","requestId":"req-1",';
|
|
205
|
+
const part2 = '"messageId":"msg-1","blockId":"b1","stage":"start","blockIndex":0,"timestamp":"2025-01-01T00:00:00.000Z"}\n\n';
|
|
206
|
+
const result1 = parseSSEChunk(part1);
|
|
207
|
+
expect(result1.events).toHaveLength(0);
|
|
208
|
+
expect(result1.remainder).not.toBe("");
|
|
209
|
+
const result2 = parseSSEChunk(result1.remainder + part2);
|
|
210
|
+
expect(result2.events).toHaveLength(1);
|
|
211
|
+
expect(result2.events[0]?.success).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export {
|
|
217
|
+
encodeSSE,
|
|
218
|
+
parseSSEChunk
|
|
219
|
+
};
|
|
220
|
+
//# sourceMappingURL=chunk-RSG7CUUT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sse/encode.ts","../src/sse/parse.ts"],"sourcesContent":["/**\n * Encodes an event and data payload into SSE wire format.\n *\n * Output format: `event: {event}\\ndata: {json}\\n\\n`\n *\n * The data object is JSON serialized. Newlines in string values are escaped\n * by JSON.stringify, ensuring valid SSE format.\n *\n * @param event - Event type identifier (e.g., 'text', 'done', 'error')\n * @param data - Event payload object to serialize as JSON\n * @returns SSE-formatted string ready for streaming\n */\nexport function encodeSSE(event: string, data: object): string {\n return `event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`;\n}\n\nif (import.meta.vitest) {\n const { describe, it, expect } = import.meta.vitest;\n\n describe('encodeSSE', () => {\n it('formats SSE event correctly', () => {\n const result = encodeSSE('metadata', {\n event: 'metadata',\n requestId: 'req-1',\n threadId: 'th-1',\n agentId: 'test-agent',\n timestamp: '2025-01-01T00:00:00.000Z',\n });\n\n expect(result).toBe(\n 'event: metadata\\n' +\n 'data: {\"event\":\"metadata\",\"requestId\":\"req-1\",\"threadId\":\"th-1\",\"agentId\":\"test-agent\",\"timestamp\":\"2025-01-01T00:00:00.000Z\"}\\n\\n',\n );\n });\n\n it('handles special characters in data', () => {\n const result = encodeSSE('text', {\n event: 'text',\n requestId: 'req-1',\n messageId: 'msg-1',\n blockId: 'b1',\n stage: 'delta',\n delta: 'Hello\\nWorld',\n deltaIndex: 0,\n timestamp: '2025-01-01T00:00:00.000Z',\n });\n\n expect(result).toContain('Hello\\\\nWorld');\n expect(result).toMatch(/^event: text\\ndata: .+\\n\\n$/);\n });\n\n it('handles unicode characters', () => {\n const result = encodeSSE('text', {\n event: 'text',\n requestId: 'req-1',\n messageId: 'msg-1',\n blockId: 'b1',\n stage: 'delta',\n delta: '你好世界 🌍',\n deltaIndex: 0,\n timestamp: '2025-01-01T00:00:00.000Z',\n });\n\n expect(result).toContain('你好世界 🌍');\n });\n\n it('handles empty object data', () => {\n const result = encodeSSE('custom', {});\n expect(result).toBe('event: custom\\ndata: {}\\n\\n');\n });\n });\n}\n","import { AgentEventSchema } from '../core/schemas.js';\nimport type { AgentEvent } from '../core/types.js';\n\n/**\n * Result of parsing a single SSE event.\n * On success, contains a validated AgentEvent.\n * On failure, contains the error message and the raw parsed data.\n */\nexport type ParsedEvent =\n | { success: true; event: AgentEvent }\n | { success: false; error: string; raw: unknown };\n\n/**\n * Result of parsing an SSE chunk.\n * Contains successfully parsed events and any incomplete data to buffer for the next chunk.\n */\nexport type ParseResult = {\n /** Array of parsed events (may include failures with error details). */\n events: ParsedEvent[];\n /** Incomplete data at end of chunk; prepend to next chunk before parsing. */\n remainder: string;\n};\n\n/**\n * Parses an SSE chunk into validated protocol events.\n *\n * Handles partial chunks by returning unparsed data as remainder.\n * The remainder should be prepended to the next chunk before parsing.\n *\n * Each SSE block is validated against AgentEventSchema. Invalid events\n * are returned with success: false and error details.\n *\n * SSE comments (lines starting with ':') are ignored, including heartbeats.\n *\n * @param chunk - Raw SSE data (may contain multiple events or partial data)\n * @returns Object with parsed events array and remainder string for buffering\n */\nexport function parseSSEChunk(chunk: string): ParseResult {\n const events: ParsedEvent[] = [];\n const blocks = chunk.split('\\n\\n');\n\n for (let i = 0; i < blocks.length - 1; i++) {\n const block = blocks[i]!;\n const parsed = parseBlock(block);\n if (parsed !== null) {\n events.push(parsed);\n }\n }\n\n const lastBlock = blocks[blocks.length - 1] ?? '';\n const remainder = lastBlock;\n\n return { events, remainder };\n}\n\n/**\n * Parses a single SSE block into a ParsedEvent.\n * Returns null for blocks without data (e.g., comment-only blocks).\n */\nfunction parseBlock(block: string): ParsedEvent | null {\n const lines = block.split('\\n');\n let eventType: string | null = null;\n let data: string | null = null;\n\n for (const line of lines) {\n if (line.startsWith(':')) {\n continue;\n }\n if (line.startsWith('event: ')) {\n eventType = line.slice(7);\n continue;\n }\n if (line.startsWith('data: ')) {\n data = line.slice(6);\n continue;\n }\n }\n\n if (data === null) {\n return null;\n }\n\n return parseAndValidate(data, eventType);\n}\n\n/**\n * Parses JSON data and validates against AgentEventSchema.\n * Checks that the event type in data matches the SSE event header if present.\n */\nfunction parseAndValidate(data: string, eventType: string | null): ParsedEvent {\n let parsed: unknown;\n try {\n parsed = JSON.parse(data);\n } catch {\n return { success: false, error: 'Invalid JSON', raw: data };\n }\n\n const result = AgentEventSchema.safeParse(parsed);\n if (result.success) {\n if (eventType !== null && result.data.event !== eventType) {\n return {\n success: false,\n error: `Event type mismatch: header says '${eventType}', data says '${result.data.event}'`,\n raw: parsed,\n };\n }\n return { success: true, event: result.data };\n }\n\n return {\n success: false,\n error: result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; '),\n raw: parsed,\n };\n}\n\nif (import.meta.vitest) {\n const { describe, it, expect } = import.meta.vitest;\n\n describe('parseSSEChunk', () => {\n it('parses a single complete event', () => {\n const chunk =\n 'event: metadata\\n' +\n 'data: {\"event\":\"metadata\",\"requestId\":\"req-1\",\"threadId\":\"th-1\",\"agentId\":\"test-agent\",\"timestamp\":\"2025-01-01T00:00:00.000Z\"}\\n\\n';\n\n const result = parseSSEChunk(chunk);\n\n expect(result.events).toHaveLength(1);\n expect(result.events[0]?.success).toBe(true);\n if (result.events[0]?.success) {\n expect(result.events[0].event.event).toBe('metadata');\n expect(result.events[0].event.requestId).toBe('req-1');\n }\n expect(result.remainder).toBe('');\n });\n\n it('parses multiple events in one chunk', () => {\n const chunk =\n 'event: text\\n' +\n 'data: {\"event\":\"text\",\"requestId\":\"req-1\",\"messageId\":\"msg-1\",\"blockId\":\"b1\",\"stage\":\"start\",\"blockIndex\":0,\"timestamp\":\"2025-01-01T00:00:00.000Z\"}\\n\\n' +\n 'event: text\\n' +\n 'data: {\"event\":\"text\",\"requestId\":\"req-1\",\"messageId\":\"msg-1\",\"blockId\":\"b1\",\"stage\":\"delta\",\"delta\":\"Hello\",\"deltaIndex\":0,\"timestamp\":\"2025-01-01T00:00:00.001Z\"}\\n\\n';\n\n const result = parseSSEChunk(chunk);\n\n expect(result.events).toHaveLength(2);\n expect(result.events[0]?.success).toBe(true);\n expect(result.events[1]?.success).toBe(true);\n if (result.events[0]?.success && result.events[1]?.success) {\n expect(result.events[0].event.event).toBe('text');\n expect(result.events[1].event.event).toBe('text');\n }\n expect(result.remainder).toBe('');\n });\n\n it('returns remainder for partial event', () => {\n const chunk =\n 'event: metadata\\n' +\n 'data: {\"event\":\"metadata\",\"requestId\":\"req-1\",\"threadId\":\"th-1\",\"agentId\":\"test-agent\",\"timestamp\":\"2025-01-01T00:00:00.000Z\"}\\n\\n' +\n 'event: text\\n' +\n 'data: {\"event\":\"text\",\"request';\n\n const result = parseSSEChunk(chunk);\n\n expect(result.events).toHaveLength(1);\n expect(result.events[0]?.success).toBe(true);\n expect(result.remainder).toContain('event: text');\n expect(result.remainder).toContain('data: {\"event\":\"text\",\"request');\n });\n\n it('ignores SSE comments', () => {\n const chunk =\n ': heartbeat\\n' +\n 'event: metadata\\n' +\n 'data: {\"event\":\"metadata\",\"requestId\":\"req-1\",\"threadId\":\"th-1\",\"agentId\":\"test-agent\",\"timestamp\":\"2025-01-01T00:00:00.000Z\"}\\n\\n';\n\n const result = parseSSEChunk(chunk);\n\n expect(result.events).toHaveLength(1);\n expect(result.events[0]?.success).toBe(true);\n });\n\n it('returns error for invalid JSON', () => {\n const chunk = 'event: text\\n' + 'data: {invalid json}\\n\\n';\n\n const result = parseSSEChunk(chunk);\n\n expect(result.events).toHaveLength(1);\n const event = result.events[0]!;\n expect(event.success).toBe(false);\n if (!event.success) {\n expect(event.error).toBe('Invalid JSON');\n }\n });\n\n it('returns error for invalid event schema', () => {\n const chunk = 'event: text\\n' + 'data: {\"event\":\"text\",\"invalidField\":\"value\"}\\n\\n';\n\n const result = parseSSEChunk(chunk);\n\n expect(result.events).toHaveLength(1);\n const event = result.events[0]!;\n expect(event.success).toBe(false);\n if (!event.success) {\n expect(event.error).toContain('requestId');\n }\n });\n\n it('returns error for event type mismatch', () => {\n const chunk =\n 'event: metadata\\n' +\n 'data: {\"event\":\"text\",\"requestId\":\"req-1\",\"messageId\":\"msg-1\",\"blockId\":\"b1\",\"stage\":\"start\",\"blockIndex\":0,\"timestamp\":\"2025-01-01T00:00:00.000Z\"}\\n\\n';\n\n const result = parseSSEChunk(chunk);\n\n expect(result.events).toHaveLength(1);\n const event = result.events[0]!;\n expect(event.success).toBe(false);\n if (!event.success) {\n expect(event.error).toContain('mismatch');\n }\n });\n\n it('handles data without event line', () => {\n const chunk =\n 'data: {\"event\":\"metadata\",\"requestId\":\"req-1\",\"threadId\":\"th-1\",\"agentId\":\"test-agent\",\"timestamp\":\"2025-01-01T00:00:00.000Z\"}\\n\\n';\n\n const result = parseSSEChunk(chunk);\n\n expect(result.events).toHaveLength(1);\n expect(result.events[0]?.success).toBe(true);\n });\n\n it('handles empty chunk', () => {\n const result = parseSSEChunk('');\n expect(result.events).toHaveLength(0);\n expect(result.remainder).toBe('');\n });\n\n it('buffers partial chunks correctly across calls', () => {\n const part1 = 'event: text\\ndata: {\"event\":\"text\",\"requestId\":\"req-1\",';\n const part2 =\n '\"messageId\":\"msg-1\",\"blockId\":\"b1\",\"stage\":\"start\",\"blockIndex\":0,\"timestamp\":\"2025-01-01T00:00:00.000Z\"}\\n\\n';\n\n const result1 = parseSSEChunk(part1);\n expect(result1.events).toHaveLength(0);\n expect(result1.remainder).not.toBe('');\n\n const result2 = parseSSEChunk(result1.remainder + part2);\n expect(result2.events).toHaveLength(1);\n expect(result2.events[0]?.success).toBe(true);\n });\n });\n}\n"],"mappings":"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","names":[]}
|