@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 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":[]}