@loomcycle/client 0.8.19

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.
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Shared HTTP plumbing for LoomcycleClient. Three responsibilities:
3
+ *
4
+ * 1. Build the Authorization header from the client's bearer token.
5
+ * 2. JSON encode/decode the request + response.
6
+ * 3. Map non-2xx responses to typed errors from errors.ts (single
7
+ * source of truth — mirrors Python's _raise_from_grpc).
8
+ *
9
+ * Method-level code in client.ts stays focused on URL + body shape;
10
+ * the boring fetch + error-translation machinery lives here.
11
+ */
12
+ /**
13
+ * @internal — not part of @loomcycle/client's public API surface.
14
+ *
15
+ * Snapshot of the LoomcycleClient constructor inputs threaded through
16
+ * the helpers below. The leading underscore + this docstring signal
17
+ * "internal type; consumers MUST NOT depend on it". TypeScript's
18
+ * declaration emit still publishes it under dist/fetch-helpers.d.ts
19
+ * (no --stripInternal in tsconfig today), but the rename + comment
20
+ * make accidental dependence implausible: a consumer doing
21
+ * `import type { _FetchContext } from "@loomcycle/client"` would have
22
+ * to deliberately reach for a name marked internal, which is a
23
+ * "contract violation" gesture rather than a casual import. The
24
+ * fields here (authToken in plaintext, fetchImpl) are implementation
25
+ * details that may change without notice. */
26
+ export interface _FetchContext {
27
+ baseUrl: string;
28
+ authToken: string | undefined;
29
+ fetchImpl: typeof fetch;
30
+ }
31
+ /** authHeaders builds the standard request header set: JSON Accept
32
+ * + Bearer token when the client was constructed with one. The
33
+ * caller adds Content-Type when posting a body. */
34
+ export declare function authHeaders(ctx: _FetchContext): Record<string, string>;
35
+ /** jsonFetch performs a GET and unwraps the JSON body. Non-2xx
36
+ * status maps to a typed error via raiseFromResponse. */
37
+ export declare function jsonFetch<T>(ctx: _FetchContext, path: string, opts?: {
38
+ signal?: AbortSignal;
39
+ }): Promise<T>;
40
+ /** postJSON sends a JSON-encoded body and unwraps the response.
41
+ * When `body` is undefined, no body is sent (Content-Type
42
+ * omitted). */
43
+ export declare function postJSON<T>(ctx: _FetchContext, path: string, body?: unknown, opts?: {
44
+ signal?: AbortSignal;
45
+ }): Promise<T>;
46
+ /** deleteRequest sends a DELETE and tolerates 204/200/404-with-
47
+ * idempotent-semantics per the loomcycle wire contract. */
48
+ export declare function deleteRequest(ctx: _FetchContext, path: string, opts?: {
49
+ signal?: AbortSignal;
50
+ }): Promise<void>;
51
+ /**
52
+ * raiseFromResponse — the single point where HTTP status + body
53
+ * text get mapped to typed errors. Always throws; the function
54
+ * signature returns `never` only because TypeScript needs the
55
+ * return type for control-flow narrowing.
56
+ *
57
+ * Mapping table:
58
+ *
59
+ * 400 → InvalidArgumentError
60
+ * 401 → AuthError
61
+ * 404 + "snapshot" → SnapshotNotFoundError ────────┐
62
+ * 404 + "session" → SessionNotFoundError │ All extend
63
+ * 404 + "hook" → HookNotFoundError │ NotFoundError —
64
+ * 404 + "agent" → AgentNotFoundError │ callers can
65
+ * 404 + (other) → NotFoundError (base) │ catch any 404
66
+ * │ with one
67
+ * │ instanceof.
68
+ * 409 + "already_pausing" / "already paused" → AlreadyPausingError
69
+ * 409 + "not_paused" / "not paused" → NotPausedError
70
+ * 409 + "session" → SessionBusyError
71
+ * 409 + "agent_id" → AgentIDInUseError
72
+ * 409 + (other) → LoomcycleError (base)
73
+ * 413 → SnapshotTooLargeError
74
+ * 422 → SnapshotVersionError (snapshot-version-too-new/unknown)
75
+ * 429 → BackpressureError
76
+ * 503 + "pause manager not configured" → PauseNotConfiguredError
77
+ * (subclass of UnavailableError)
78
+ * 503 + (other) → UnavailableError
79
+ * 500-599 (other) → LoomcycleError (base)
80
+ * default → LoomcycleError (base)
81
+ *
82
+ * Priority within a status group is most-specific-first; an unknown
83
+ * 409 falls through to base LoomcycleError so callers see a
84
+ * meaningful message + status. For 404, the catch-all is NotFoundError
85
+ * (base) so the v0.8.18-added memory + interrupt routes don't
86
+ * misclassify into AgentNotFoundError when the 404 body doesn't
87
+ * mention "agent".
88
+ */
89
+ export declare function raiseFromResponse(resp: Response): Promise<never>;
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Shared HTTP plumbing for LoomcycleClient. Three responsibilities:
3
+ *
4
+ * 1. Build the Authorization header from the client's bearer token.
5
+ * 2. JSON encode/decode the request + response.
6
+ * 3. Map non-2xx responses to typed errors from errors.ts (single
7
+ * source of truth — mirrors Python's _raise_from_grpc).
8
+ *
9
+ * Method-level code in client.ts stays focused on URL + body shape;
10
+ * the boring fetch + error-translation machinery lives here.
11
+ */
12
+ import { AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, HookNotFoundError, InvalidArgumentError, LoomcycleError, NotFoundError, NotPausedError, PauseNotConfiguredError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, UnavailableError, } from "./errors.js";
13
+ /** authHeaders builds the standard request header set: JSON Accept
14
+ * + Bearer token when the client was constructed with one. The
15
+ * caller adds Content-Type when posting a body. */
16
+ export function authHeaders(ctx) {
17
+ const h = { Accept: "application/json" };
18
+ if (ctx.authToken)
19
+ h.Authorization = `Bearer ${ctx.authToken}`;
20
+ return h;
21
+ }
22
+ /** jsonFetch performs a GET and unwraps the JSON body. Non-2xx
23
+ * status maps to a typed error via raiseFromResponse. */
24
+ export async function jsonFetch(ctx, path, opts) {
25
+ const resp = await ctx.fetchImpl(ctx.baseUrl + path, {
26
+ method: "GET",
27
+ headers: authHeaders(ctx),
28
+ signal: opts?.signal,
29
+ });
30
+ if (!resp.ok) {
31
+ await raiseFromResponse(resp);
32
+ }
33
+ return (await resp.json());
34
+ }
35
+ /** postJSON sends a JSON-encoded body and unwraps the response.
36
+ * When `body` is undefined, no body is sent (Content-Type
37
+ * omitted). */
38
+ export async function postJSON(ctx, path, body, opts) {
39
+ const headers = authHeaders(ctx);
40
+ let bodyStr;
41
+ if (body !== undefined) {
42
+ headers["Content-Type"] = "application/json";
43
+ bodyStr = JSON.stringify(body);
44
+ }
45
+ const resp = await ctx.fetchImpl(ctx.baseUrl + path, {
46
+ method: "POST",
47
+ headers,
48
+ body: bodyStr,
49
+ signal: opts?.signal,
50
+ });
51
+ if (!resp.ok) {
52
+ await raiseFromResponse(resp);
53
+ }
54
+ // Some endpoints return 204 No Content; tolerate that with a
55
+ // null cast — typed methods that know they return 204 use a
56
+ // void wrapper instead.
57
+ if (resp.status === 204)
58
+ return null;
59
+ return (await resp.json());
60
+ }
61
+ /** deleteRequest sends a DELETE and tolerates 204/200/404-with-
62
+ * idempotent-semantics per the loomcycle wire contract. */
63
+ export async function deleteRequest(ctx, path, opts) {
64
+ const resp = await ctx.fetchImpl(ctx.baseUrl + path, {
65
+ method: "DELETE",
66
+ headers: authHeaders(ctx),
67
+ signal: opts?.signal,
68
+ });
69
+ if (!resp.ok) {
70
+ await raiseFromResponse(resp);
71
+ }
72
+ }
73
+ /**
74
+ * raiseFromResponse — the single point where HTTP status + body
75
+ * text get mapped to typed errors. Always throws; the function
76
+ * signature returns `never` only because TypeScript needs the
77
+ * return type for control-flow narrowing.
78
+ *
79
+ * Mapping table:
80
+ *
81
+ * 400 → InvalidArgumentError
82
+ * 401 → AuthError
83
+ * 404 + "snapshot" → SnapshotNotFoundError ────────┐
84
+ * 404 + "session" → SessionNotFoundError │ All extend
85
+ * 404 + "hook" → HookNotFoundError │ NotFoundError —
86
+ * 404 + "agent" → AgentNotFoundError │ callers can
87
+ * 404 + (other) → NotFoundError (base) │ catch any 404
88
+ * │ with one
89
+ * │ instanceof.
90
+ * 409 + "already_pausing" / "already paused" → AlreadyPausingError
91
+ * 409 + "not_paused" / "not paused" → NotPausedError
92
+ * 409 + "session" → SessionBusyError
93
+ * 409 + "agent_id" → AgentIDInUseError
94
+ * 409 + (other) → LoomcycleError (base)
95
+ * 413 → SnapshotTooLargeError
96
+ * 422 → SnapshotVersionError (snapshot-version-too-new/unknown)
97
+ * 429 → BackpressureError
98
+ * 503 + "pause manager not configured" → PauseNotConfiguredError
99
+ * (subclass of UnavailableError)
100
+ * 503 + (other) → UnavailableError
101
+ * 500-599 (other) → LoomcycleError (base)
102
+ * default → LoomcycleError (base)
103
+ *
104
+ * Priority within a status group is most-specific-first; an unknown
105
+ * 409 falls through to base LoomcycleError so callers see a
106
+ * meaningful message + status. For 404, the catch-all is NotFoundError
107
+ * (base) so the v0.8.18-added memory + interrupt routes don't
108
+ * misclassify into AgentNotFoundError when the 404 body doesn't
109
+ * mention "agent".
110
+ */
111
+ export async function raiseFromResponse(resp) {
112
+ const status = resp.status;
113
+ // Read body with a cap; many error bodies are JSON {error, message}
114
+ // shape but raw text is fine for matching keywords.
115
+ let bodyText = "";
116
+ try {
117
+ bodyText = await resp.text();
118
+ }
119
+ catch {
120
+ // network-level body read failure — fall through with empty body
121
+ }
122
+ const bodyLower = bodyText.toLowerCase();
123
+ // HTTP/2 strips reason phrases — Node's undici fetch returns "" for
124
+ // resp.statusText on HTTP/2 responses. Fall back to a stock phrase
125
+ // for the common status codes so the error message reads cleanly
126
+ // ("401 Unauthorized" not "401 " with a trailing space).
127
+ const statusPhrase = resp.statusText || stockStatusPhrase(status);
128
+ const msg = bodyText.trim() ? bodyText.slice(0, 1024) : `${status} ${statusPhrase}`;
129
+ const opts = { status, bodyText: bodyText.slice(0, 1024) };
130
+ switch (status) {
131
+ case 400:
132
+ throw new InvalidArgumentError(msg, opts);
133
+ case 401:
134
+ throw new AuthError(msg, opts);
135
+ case 404:
136
+ // Priority: most-specific keyword wins.
137
+ // - "snapshot" → SnapshotNotFoundError
138
+ // - "session" → SessionNotFoundError
139
+ // - "hook" → HookNotFoundError (must precede "agent" — the
140
+ // hooks 404 body is `no hook with id "..."`,
141
+ // doesn't mention "agent")
142
+ // - "agent" or "agent_id" → AgentNotFoundError
143
+ // - otherwise → NotFoundError (base) — e.g. memory rows, interrupts,
144
+ // or any future 404-returning endpoint that doesn't fit the
145
+ // existing keyword set.
146
+ if (bodyLower.includes("snapshot"))
147
+ throw new SnapshotNotFoundError(msg, opts);
148
+ if (bodyLower.includes("session"))
149
+ throw new SessionNotFoundError(msg, opts);
150
+ if (bodyLower.includes("hook"))
151
+ throw new HookNotFoundError(msg, opts);
152
+ if (bodyLower.includes("agent"))
153
+ throw new AgentNotFoundError(msg, opts);
154
+ throw new NotFoundError(msg, opts);
155
+ case 409:
156
+ if (bodyLower.includes("already_pausing") || bodyLower.includes("already paused"))
157
+ throw new AlreadyPausingError(msg, opts);
158
+ if (bodyLower.includes("not_paused") || bodyLower.includes("not paused"))
159
+ throw new NotPausedError(msg, opts);
160
+ if (bodyLower.includes("session"))
161
+ throw new SessionBusyError(msg, opts);
162
+ if (bodyLower.includes("agent_id"))
163
+ throw new AgentIDInUseError(msg, opts);
164
+ throw new LoomcycleError(msg, opts);
165
+ case 413:
166
+ throw new SnapshotTooLargeError(msg, opts);
167
+ case 422:
168
+ throw new SnapshotVersionError(msg, opts);
169
+ case 429:
170
+ throw new BackpressureError(msg, opts);
171
+ case 503:
172
+ if (bodyLower.includes("pause") && bodyLower.includes("not configured"))
173
+ throw new PauseNotConfiguredError(msg, opts);
174
+ throw new UnavailableError(msg, opts);
175
+ default:
176
+ throw new LoomcycleError(msg, opts);
177
+ }
178
+ }
179
+ /** stockStatusPhrase returns a stock reason phrase for the common
180
+ * HTTP statuses raiseFromResponse handles. Used as a fallback when
181
+ * Response.statusText is empty (HTTP/2 strips reason phrases). */
182
+ function stockStatusPhrase(status) {
183
+ switch (status) {
184
+ case 400: return "Bad Request";
185
+ case 401: return "Unauthorized";
186
+ case 403: return "Forbidden";
187
+ case 404: return "Not Found";
188
+ case 409: return "Conflict";
189
+ case 413: return "Payload Too Large";
190
+ case 422: return "Unprocessable Entity";
191
+ case 429: return "Too Many Requests";
192
+ case 500: return "Internal Server Error";
193
+ case 502: return "Bad Gateway";
194
+ case 503: return "Service Unavailable";
195
+ case 504: return "Gateway Timeout";
196
+ default: return "HTTP " + status;
197
+ }
198
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @loomcycle/client — TypeScript client for the loomcycle sidecar.
3
+ *
4
+ * Public surface (v0.8.18 — Python-adapter parity):
5
+ *
6
+ * class LoomcycleClient
7
+ * constructor(opts: ClientOptions)
8
+ *
9
+ * // Run lifecycle (SSE streams)
10
+ * runStreaming(opts: RunOptions): AsyncIterable<AgentEvent>
11
+ * continueSession(opts: ContinueOptions): AsyncIterable<AgentEvent>
12
+ *
13
+ * // Agent metadata
14
+ * getAgent(agentId): Promise<Agent>
15
+ * cancelAgent(agentId, opts?): Promise<CancelAgentResult>
16
+ * listUserAgents(userId, opts?): Promise<Agent[]>
17
+ * getTranscript(sessionId): Promise<TranscriptResponse>
18
+ * health(): Promise<HealthResponse>
19
+ * listUsers(): Promise<ListUsersResponse>
20
+ *
21
+ * // Pause / Resume / State (v0.8.17/8.18)
22
+ * pauseRuntime(opts?): Promise<PauseResult>
23
+ * resumeRuntime(): Promise<ResumeResult>
24
+ * getRuntimeState(): Promise<RuntimeStateResponse>
25
+ *
26
+ * // Snapshot lifecycle (v0.8.17/8.18)
27
+ * createSnapshot(opts?): Promise<SnapshotCreateResponse>
28
+ * listSnapshots(opts?): Promise<SnapshotDescriptor[]>
29
+ * getSnapshot(id): Promise<SnapshotEnvelope>
30
+ * exportSnapshotURL(id): string (synchronous; returns a URL)
31
+ * restoreSnapshot(opts): Promise<SnapshotRestoreResponse>
32
+ * deleteSnapshot(id): Promise<void>
33
+ *
34
+ * // Memory admin
35
+ * listMemoryScopes(): Promise<MemoryScopesResponse>
36
+ * listMemoryScopeIDs(scope): Promise<MemoryScopeIDsResponse>
37
+ * listMemoryEntries(scope, scopeID, opts?): Promise<MemoryEntriesResponse>
38
+ * getMemoryEntry(scope, scopeID, key): Promise<MemoryEntryResponse>
39
+ *
40
+ * // Interruption (v0.8.16)
41
+ * listUserInterrupts(userId, opts?): Promise<InterruptListResponse>
42
+ * listRunInterrupts(runId, opts?): Promise<InterruptListResponse>
43
+ * resolveInterrupt(runId, interruptId, opts): Promise<unknown>
44
+ *
45
+ * Errors (typed subclasses of LoomcycleError; see README for the
46
+ * full HTTP-status → typed-error mapping table):
47
+ * LoomcycleError, AgentNotFoundError, SessionNotFoundError,
48
+ * SessionBusyError, AgentIDInUseError, BackpressureError,
49
+ * AuthError, UnavailableError, InvalidArgumentError,
50
+ * PauseNotConfiguredError (subclass of UnavailableError),
51
+ * AlreadyPausingError, NotPausedError, SnapshotNotFoundError,
52
+ * SnapshotTooLargeError, SnapshotVersionError
53
+ *
54
+ * Transport: HTTP+SSE. Auth: Bearer token via the Authorization
55
+ * header. Designed for Node ≥18 (engines pinned); Bun/Deno likely
56
+ * work but untested. Browser support is not a target (use the
57
+ * Web UI for browser-side operator control).
58
+ *
59
+ * See `adapters/ts/README.md` for usage examples.
60
+ */
61
+ export { LoomcycleClient } from "./client.js";
62
+ export type { AgentEvent, ClientOptions, ContinueOptions, EventType, HostWidening, PromptContent, PromptSegment, RetryInfo, RunOptions, ToolUse, Usage, Agent, AgentStatus, AgentUsage, CancelAgentResult, ListAgentsResponse, TranscriptEvent, TranscriptResponse, HealthResponse, ListUsersResponse, UserSummary, PauseResult, ResumeResult, RuntimeStateResponse, RuntimeStateStatus, CreateSnapshotOptions, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotListResponse, SnapshotRestoreResponse, MemoryEntriesResponse, MemoryEntry, MemoryEntryResponse, MemoryScopeIDsResponse, MemoryScopeIDSummary, MemoryScopeKind, MemoryScopesResponse, InterruptListResponse, InterruptRow, InterruptStatus, ResolveInterruptOptions, Hook, HookFailMode, HookPhase, HookToolCall, HookToolResult, ListHooksResponse, PostHookCall, PostHookResult, PreHookCall, PreHookResult, RegisterHookOptions, RegisterHookResponse, } from "./types.js";
63
+ export { AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, HookNotFoundError, NotFoundError, InvalidArgumentError, LoomcycleError, NotPausedError, PauseNotConfiguredError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, UnavailableError, } from "./errors.js";
package/dist/index.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @loomcycle/client — TypeScript client for the loomcycle sidecar.
3
+ *
4
+ * Public surface (v0.8.18 — Python-adapter parity):
5
+ *
6
+ * class LoomcycleClient
7
+ * constructor(opts: ClientOptions)
8
+ *
9
+ * // Run lifecycle (SSE streams)
10
+ * runStreaming(opts: RunOptions): AsyncIterable<AgentEvent>
11
+ * continueSession(opts: ContinueOptions): AsyncIterable<AgentEvent>
12
+ *
13
+ * // Agent metadata
14
+ * getAgent(agentId): Promise<Agent>
15
+ * cancelAgent(agentId, opts?): Promise<CancelAgentResult>
16
+ * listUserAgents(userId, opts?): Promise<Agent[]>
17
+ * getTranscript(sessionId): Promise<TranscriptResponse>
18
+ * health(): Promise<HealthResponse>
19
+ * listUsers(): Promise<ListUsersResponse>
20
+ *
21
+ * // Pause / Resume / State (v0.8.17/8.18)
22
+ * pauseRuntime(opts?): Promise<PauseResult>
23
+ * resumeRuntime(): Promise<ResumeResult>
24
+ * getRuntimeState(): Promise<RuntimeStateResponse>
25
+ *
26
+ * // Snapshot lifecycle (v0.8.17/8.18)
27
+ * createSnapshot(opts?): Promise<SnapshotCreateResponse>
28
+ * listSnapshots(opts?): Promise<SnapshotDescriptor[]>
29
+ * getSnapshot(id): Promise<SnapshotEnvelope>
30
+ * exportSnapshotURL(id): string (synchronous; returns a URL)
31
+ * restoreSnapshot(opts): Promise<SnapshotRestoreResponse>
32
+ * deleteSnapshot(id): Promise<void>
33
+ *
34
+ * // Memory admin
35
+ * listMemoryScopes(): Promise<MemoryScopesResponse>
36
+ * listMemoryScopeIDs(scope): Promise<MemoryScopeIDsResponse>
37
+ * listMemoryEntries(scope, scopeID, opts?): Promise<MemoryEntriesResponse>
38
+ * getMemoryEntry(scope, scopeID, key): Promise<MemoryEntryResponse>
39
+ *
40
+ * // Interruption (v0.8.16)
41
+ * listUserInterrupts(userId, opts?): Promise<InterruptListResponse>
42
+ * listRunInterrupts(runId, opts?): Promise<InterruptListResponse>
43
+ * resolveInterrupt(runId, interruptId, opts): Promise<unknown>
44
+ *
45
+ * Errors (typed subclasses of LoomcycleError; see README for the
46
+ * full HTTP-status → typed-error mapping table):
47
+ * LoomcycleError, AgentNotFoundError, SessionNotFoundError,
48
+ * SessionBusyError, AgentIDInUseError, BackpressureError,
49
+ * AuthError, UnavailableError, InvalidArgumentError,
50
+ * PauseNotConfiguredError (subclass of UnavailableError),
51
+ * AlreadyPausingError, NotPausedError, SnapshotNotFoundError,
52
+ * SnapshotTooLargeError, SnapshotVersionError
53
+ *
54
+ * Transport: HTTP+SSE. Auth: Bearer token via the Authorization
55
+ * header. Designed for Node ≥18 (engines pinned); Bun/Deno likely
56
+ * work but untested. Browser support is not a target (use the
57
+ * Web UI for browser-side operator control).
58
+ *
59
+ * See `adapters/ts/README.md` for usage examples.
60
+ */
61
+ export { LoomcycleClient } from "./client.js";
62
+ export { AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, HookNotFoundError, NotFoundError, InvalidArgumentError, LoomcycleError, NotPausedError, PauseNotConfiguredError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, UnavailableError, } from "./errors.js";
@@ -0,0 +1,17 @@
1
+ import type { AgentEvent } from "./types.js";
2
+ /**
3
+ * parseSSE turns a chunked byte stream into typed AgentEvents.
4
+ *
5
+ * SSE framing (subset): "event: <name>\ndata: <json>\n\n". We only emit a
6
+ * frame when both event + data have been seen since the last blank line.
7
+ *
8
+ * Used by `runStreaming` and `continueSession` — both POST endpoints
9
+ * return the same SSE wire shape and the parser doesn't differentiate.
10
+ *
11
+ * Side-channel frames: the v0.4 `event: agent` SSE frame (and any future
12
+ * sse.sendRaw user) emits a JSON payload that does NOT carry the `type`
13
+ * field — the SSE event name is the only discriminator. parseSSE backfills
14
+ * `type` from the event name in that case so consumers see a well-formed
15
+ * AgentEvent and switch on `ev.type` uniformly.
16
+ */
17
+ export declare function parseSSE(reader: ReadableStreamDefaultReader<Uint8Array>): AsyncIterable<AgentEvent>;
package/dist/stream.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * parseSSE turns a chunked byte stream into typed AgentEvents.
3
+ *
4
+ * SSE framing (subset): "event: <name>\ndata: <json>\n\n". We only emit a
5
+ * frame when both event + data have been seen since the last blank line.
6
+ *
7
+ * Used by `runStreaming` and `continueSession` — both POST endpoints
8
+ * return the same SSE wire shape and the parser doesn't differentiate.
9
+ *
10
+ * Side-channel frames: the v0.4 `event: agent` SSE frame (and any future
11
+ * sse.sendRaw user) emits a JSON payload that does NOT carry the `type`
12
+ * field — the SSE event name is the only discriminator. parseSSE backfills
13
+ * `type` from the event name in that case so consumers see a well-formed
14
+ * AgentEvent and switch on `ev.type` uniformly.
15
+ */
16
+ export async function* parseSSE(reader) {
17
+ const decoder = new TextDecoder("utf-8");
18
+ let buf = "";
19
+ let event = "";
20
+ let data = "";
21
+ const flush = () => {
22
+ if (!event && !data)
23
+ return null;
24
+ if (!data) {
25
+ event = "";
26
+ return null;
27
+ }
28
+ try {
29
+ const parsed = JSON.parse(data);
30
+ // Side-channel sendRaw frames omit `type` in the JSON payload — the
31
+ // SSE event name is the only discriminator. Backfill it so the
32
+ // consumer's switch on ev.type doesn't miss these.
33
+ if (!parsed.type && event) {
34
+ parsed.type = event;
35
+ }
36
+ event = "";
37
+ data = "";
38
+ return parsed;
39
+ }
40
+ catch {
41
+ event = "";
42
+ data = "";
43
+ return null;
44
+ }
45
+ };
46
+ while (true) {
47
+ const { value, done } = await reader.read();
48
+ if (done)
49
+ break;
50
+ buf += decoder.decode(value, { stream: true });
51
+ let idx;
52
+ while ((idx = buf.indexOf("\n")) !== -1) {
53
+ const line = buf.slice(0, idx).replace(/\r$/, "");
54
+ buf = buf.slice(idx + 1);
55
+ if (line === "") {
56
+ const ev = flush();
57
+ if (ev)
58
+ yield ev;
59
+ continue;
60
+ }
61
+ if (line.startsWith("event:"))
62
+ event = line.slice("event:".length).trim();
63
+ else if (line.startsWith("data:"))
64
+ data = line.slice("data:".length).trim();
65
+ }
66
+ }
67
+ // Stream ended. Drain any unterminated final line still in `buf` — a
68
+ // connection drop can land here mid-frame, and without this step the
69
+ // last frame whose `\n` never arrived would be silently lost. Then
70
+ // flush any pending event + data.
71
+ if (buf.length > 0) {
72
+ const line = buf.replace(/\r$/, "");
73
+ if (line.startsWith("event:"))
74
+ event = line.slice("event:".length).trim();
75
+ else if (line.startsWith("data:"))
76
+ data = line.slice("data:".length).trim();
77
+ }
78
+ const ev = flush();
79
+ if (ev)
80
+ yield ev;
81
+ }