@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.
- package/README.md +276 -0
- package/dist/client.d.ts +213 -0
- package/dist/client.js +366 -0
- package/dist/errors.d.ts +135 -0
- package/dist/errors.js +138 -0
- package/dist/fetch-helpers.d.ts +89 -0
- package/dist/fetch-helpers.js +198 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +62 -0
- package/dist/stream.d.ts +17 -0
- package/dist/stream.js +81 -0
- package/dist/types.d.ts +479 -0
- package/dist/types.js +10 -0
- package/package.json +32 -0
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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";
|
package/dist/stream.d.ts
ADDED
|
@@ -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
|
+
}
|