@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 ADDED
@@ -0,0 +1,276 @@
1
+ # @loomcycle/client
2
+
3
+ TypeScript client for the [loomcycle](https://github.com/denn-gubsky/loomcycle) sidecar — the agentic-OS substrate for production agents.
4
+
5
+ `@loomcycle/client` speaks HTTP+SSE to the loomcycle server's `/v1/*` surface. The same operation surface is exposed via gRPC (`adapters/python/loomcycle`) and stdio MCP (`loomcycle mcp`); this client is the HTTP-side adapter, suitable for Node.js orchestrators, automation scripts, and operator tooling.
6
+
7
+ ## Status
8
+
9
+ **v0.8.18** — full Python-adapter parity + hook management. 27 methods covering run streaming, agent metadata, transcript, pause/resume/state, snapshot lifecycle, memory admin, interruption resolve, hook registration, and health.
10
+
11
+ > Migrating from raw `fetch` against `/v1/*`? See **[docs/MIGRATING-FROM-HTTP.md](./docs/MIGRATING-FROM-HTTP.md)** for a side-by-side walkthrough.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @loomcycle/client
17
+ ```
18
+
19
+ Requires Node ≥ 18. Bun and Deno likely work but are untested. Browser support is not a target — for browser-side operator control, use loomcycle's built-in Web UI at `/ui`.
20
+
21
+ ## Quick start
22
+
23
+ ```ts
24
+ import { LoomcycleClient } from "@loomcycle/client";
25
+
26
+ const client = new LoomcycleClient({
27
+ baseUrl: process.env.LOOMCYCLE_BASE_URL ?? "http://127.0.0.1:8787",
28
+ authToken: process.env.LOOMCYCLE_AUTH_TOKEN,
29
+ });
30
+
31
+ // Run an agent, stream events
32
+ for await (const ev of client.runStreaming({
33
+ agent: "qa-agent",
34
+ segments: [
35
+ { role: "user", content: [{ type: "trusted-text", text: "Hello, world." }] },
36
+ ],
37
+ })) {
38
+ if (ev.type === "text") process.stdout.write(ev.text ?? "");
39
+ }
40
+ ```
41
+
42
+ ## Cancellation
43
+
44
+ Every method accepts an optional `signal?: AbortSignal`. The streaming methods (`runStreaming`, `continueSession`) also break out of the iterator when the abort fires.
45
+
46
+ ```ts
47
+ const ac = new AbortController();
48
+ setTimeout(() => ac.abort(), 30_000); // 30s budget
49
+
50
+ try {
51
+ for await (const ev of client.runStreaming({ agent: "...", segments: [...], signal: ac.signal })) {
52
+ // ...
53
+ }
54
+ } catch (e) {
55
+ if (e instanceof DOMException && e.name === "AbortError") {
56
+ // timed out
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## API
62
+
63
+ All methods are async / return `Promise<T>` unless noted; streaming methods return `AsyncIterable<AgentEvent>`.
64
+
65
+ ### Run lifecycle
66
+
67
+ | Method | Returns | Notes |
68
+ |---|---|---|
69
+ | `runStreaming(opts: RunOptions)` | `AsyncIterable<AgentEvent>` | Server-streams provider events for a fresh run. |
70
+ | `continueSession(opts: ContinueOptions)` | `AsyncIterable<AgentEvent>` | Continues an existing session. |
71
+
72
+ ### Agent metadata
73
+
74
+ | Method | Returns | Notes |
75
+ |---|---|---|
76
+ | `getAgent(agentId)` | `Promise<Agent>` | One agent's status + usage. Raises `AgentNotFoundError` if unknown. |
77
+ | `cancelAgent(agentId, opts?)` | `Promise<{ cancelledCount: number }>` | Cascades to children via `parent_agent_id`. Idempotent. |
78
+ | `listUserAgents(userId, opts?)` | `Promise<Agent[]>` | Optional filter by status (`running` / `completed` / `failed` / `cancelled`). |
79
+ | `getTranscript(sessionId)` | `Promise<TranscriptResponse>` | Persisted event log; one row per event with seq/run_id/ts_ns/type/event. |
80
+ | `health()` | `Promise<HealthResponse>` | Liveness probe. Hits `/healthz` (no `/v1` prefix). Unauthenticated. |
81
+ | `listUsers()` | `Promise<ListUsersResponse>` | Admin: known users with running-count summary. |
82
+
83
+ ### Pause / Resume / State (v0.8.17 / v0.8.18)
84
+
85
+ | Method | Returns | Notes |
86
+ |---|---|---|
87
+ | `pauseRuntime(opts?: { timeoutMs? })` | `Promise<PauseResult>` | Quiesce the runtime. Raises `AlreadyPausingError` on 409, `PauseNotConfiguredError` on 503. |
88
+ | `resumeRuntime()` | `Promise<ResumeResult>` | Release the quiesce. Raises `NotPausedError` on 409. |
89
+ | `getRuntimeState()` | `Promise<RuntimeStateResponse>` | Current state + paused-runs count. |
90
+
91
+ ### Snapshot lifecycle (v0.8.17 / v0.8.18)
92
+
93
+ | Method | Returns | Notes |
94
+ |---|---|---|
95
+ | `createSnapshot(opts?: CreateSnapshotOptions)` | `Promise<SnapshotCreateResponse>` | Capture envelope. Raises `SnapshotTooLargeError` on 413. |
96
+ | `listSnapshots(opts?: { limit?, labelContains? })` | `Promise<SnapshotDescriptor[]>` | Metadata only. |
97
+ | `getSnapshot(id)` | `Promise<SnapshotEnvelope>` | Full envelope including `json_content`. Raises `SnapshotNotFoundError` on 404. |
98
+ | `exportSnapshotURL(id)` | `string` | **Synchronous** — returns the download URL. Suitable for `<a href>` or piping to a HTTP download tool. |
99
+ | `restoreSnapshot(opts: { snapshotId? \| json?, includeHistory? })` | `Promise<SnapshotRestoreResponse>` | Restore from same-instance id OR inline envelope. Raises `SnapshotVersionError` on 422. |
100
+ | `deleteSnapshot(id)` | `Promise<void>` | Idempotent — 204 on both new and missing rows. |
101
+
102
+ Round-trip example:
103
+
104
+ ```ts
105
+ const created = await client.createSnapshot({ label: "before-deploy" });
106
+ const env = await client.getSnapshot(created.id);
107
+ // ... move bytes to another loomcycle instance ...
108
+ const result = await otherClient.restoreSnapshot({ json: env.json_content });
109
+ console.log(`restored memory rows: ${result.memory_restored}`);
110
+ ```
111
+
112
+ ### Memory admin
113
+
114
+ | Method | Returns | Notes |
115
+ |---|---|---|
116
+ | `listMemoryScopes()` | `Promise<MemoryScopesResponse>` | Scope kinds (agent, user, etc.). |
117
+ | `listMemoryScopeIDs(scope)` | `Promise<MemoryScopeIDsResponse>` | scope_ids with row counts. |
118
+ | `listMemoryEntries(scope, scopeID, opts?)` | `Promise<MemoryEntriesResponse>` | Optional `prefix` + `limit`. |
119
+ | `getMemoryEntry(scope, scopeID, key)` | `Promise<MemoryEntryResponse>` | Single row read. |
120
+
121
+ ### Interruption (v0.8.16)
122
+
123
+ | Method | Returns | Notes |
124
+ |---|---|---|
125
+ | `listUserInterrupts(userId, opts?)` | `Promise<InterruptListResponse>` | Default filter: `status=pending`. |
126
+ | `listRunInterrupts(runId, opts?)` | `Promise<InterruptListResponse>` | Per-run interrupts. |
127
+ | `resolveInterrupt(runId, interruptId, opts: ResolveInterruptOptions)` | `Promise<unknown>` | Answer a pending interrupt. |
128
+
129
+ ### Hook management (v0.8.18)
130
+
131
+ | Method | Returns | Notes |
132
+ |---|---|---|
133
+ | `registerHook(opts: RegisterHookOptions)` | `Promise<RegisterHookResponse>` | Register a pre- or post-tool webhook. Re-registering the same `(owner, name)` replaces in-place with a fresh id. Raises `InvalidArgumentError` on 400 (bad URL / phase / missing field). |
134
+ | `listHooks()` | `Promise<Hook[]>` | Every registered hook. **In-memory only — empty after a loomcycle restart.** |
135
+ | `deleteHook(id)` | `Promise<void>` | Raises `HookNotFoundError` on 404. |
136
+
137
+ Hook registration is one side; the other side is the **callback receiver** — a small HTTP endpoint your app runs at the URL you registered. The adapter exports the wire shapes (`PreHookCall` / `PostHookCall` / `PreHookResult` / `PostHookResult`) so you can type the handler against the same JSON loomcycle posts.
138
+
139
+ **Register from your app's startup:**
140
+
141
+ ```ts
142
+ import { LoomcycleClient } from "@loomcycle/client";
143
+
144
+ const client = new LoomcycleClient({
145
+ baseUrl: process.env.LOOMCYCLE_BASE_URL!,
146
+ authToken: process.env.LOOMCYCLE_AUTH_TOKEN,
147
+ });
148
+
149
+ await client.registerHook({
150
+ owner: "jobember-web", // (owner, name) is the identity tuple
151
+ name: "scan-webfetch", // re-registering same pair replaces in place
152
+ phase: "post", // "pre" or "post"
153
+ tools: ["WebFetch"], // empty/omitted = all tools
154
+ callbackUrl: "https://jobember.example/hooks/scan",
155
+ failMode: "open", // "open" = errors pass through; "closed" = errors fail the tool call
156
+ timeoutMs: 3000, // 0 = registry default (5s)
157
+ });
158
+ ```
159
+
160
+ **Run the callback receiver** (Next.js App Router example — adapt to your framework):
161
+
162
+ ```ts
163
+ // app/hooks/scan/route.ts
164
+ import { NextResponse } from "next/server";
165
+ import type { PostHookCall, PostHookResult } from "@loomcycle/client";
166
+
167
+ export async function POST(req: Request) {
168
+ const body = (await req.json()) as PostHookCall;
169
+ // body.phase === "post", body.agent, body.tool_call.{id,name,input}, body.tool_result.{text,is_error}
170
+
171
+ // Telemetry-shaped: log + pass through.
172
+ console.log(`[hook] ${body.agent}.${body.tool_call.name} -> ${body.tool_result.is_error ? "error" : "ok"}`);
173
+
174
+ // Empty body = pass through unchanged. Return a PostHookResult to rewrite:
175
+ const reply: PostHookResult = {}; // or { result: { text: "redacted", is_error: false } }
176
+ return NextResponse.json(reply);
177
+ }
178
+ ```
179
+
180
+ **Pre-hook example** (short-circuit a tool call):
181
+
182
+ ```ts
183
+ // app/hooks/guard/route.ts
184
+ import { NextResponse } from "next/server";
185
+ import type { PreHookCall, PreHookResult } from "@loomcycle/client";
186
+
187
+ export async function POST(req: Request) {
188
+ const body = (await req.json()) as PreHookCall;
189
+
190
+ // Deny outbound fetches to disallowed hosts
191
+ const input = body.tool_call.input as { url?: string };
192
+ if (input.url && new URL(input.url).hostname.endsWith(".internal")) {
193
+ const reply: PreHookResult = {
194
+ deny: { text: "internal hosts are not reachable from agents", is_error: true },
195
+ };
196
+ return NextResponse.json(reply);
197
+ }
198
+
199
+ return NextResponse.json({}); // pass through
200
+ }
201
+ ```
202
+
203
+ **Important constraints**:
204
+
205
+ - Hook registrations are **in-memory** on the loomcycle server. Re-register on every app startup; the `(owner, name)` idempotency contract makes this safe (replaces in place).
206
+ - Auth flows one-way: loomcycle → your callback URL. Loomcycle does NOT attach a bearer token to callback POSTs by default. If you need to authenticate the caller, validate by source IP or include a shared secret in the `callback_url` path/query (`https://jobember.example/hooks/scan?secret=...`).
207
+ - `fail_mode: "open"` (default) is right for telemetry hooks where a down receiver shouldn't break tool dispatch. `"closed"` is right for security hooks where a down receiver should fail the tool call (don't let bypassed payloads through).
208
+ - `allow_hosts` in `PreHookResult` is a **trust-sensitive surface** — it widens the agent's outbound network policy for one tool call. Server enforces an operator-yaml allowlist (`hooks.permit_host_widen.owners`); your owner has to be on that list for `allow_hosts` to take effect. See the SECURITY note in `internal/hooks/types.go` before using.
209
+
210
+ ## Errors
211
+
212
+ Non-2xx responses throw typed subclasses of `LoomcycleError`. The original HTTP status is on `e.status`; the truncated response body is on `e.bodyText` (≤1 KiB).
213
+
214
+ | HTTP status / body | Exception class |
215
+ |---|---|
216
+ | 400 | `InvalidArgumentError` |
217
+ | 401 | `AuthError` |
218
+ | 404 + "snapshot" | `SnapshotNotFoundError` ⎫ |
219
+ | 404 + "session" | `SessionNotFoundError` ⎬ all extend `NotFoundError` |
220
+ | 404 + "hook" | `HookNotFoundError` ⎬ |
221
+ | 404 + "agent" | `AgentNotFoundError` ⎬ |
222
+ | 404 (other) | `NotFoundError` (base) ⎭ catch any 404 with one `instanceof` |
223
+ | 409 + "already_pausing" / "already paused" | `AlreadyPausingError` |
224
+ | 409 + "not_paused" / "not paused" | `NotPausedError` |
225
+ | 409 + "session" | `SessionBusyError` |
226
+ | 409 + "agent_id" | `AgentIDInUseError` |
227
+ | 409 (other) | `LoomcycleError` (base) |
228
+ | 413 | `SnapshotTooLargeError` |
229
+ | 422 | `SnapshotVersionError` |
230
+ | 429 | `BackpressureError` |
231
+ | 503 + "pause manager not configured" | `PauseNotConfiguredError` (subclass of `UnavailableError` — back-compat) |
232
+ | 503 (other) | `UnavailableError` |
233
+ | 500 / other | `LoomcycleError` (base) |
234
+
235
+ Priority within `404`: most-specific keyword wins (`snapshot` → `session` → `hook` → `agent` → base). The dispatch is keyword-matched on the response body lowercase; a hook with id `hook_agent_scan` still routes to `HookNotFoundError`, not `AgentNotFoundError`.
236
+
237
+ ```ts
238
+ import {
239
+ BackpressureError,
240
+ SnapshotNotFoundError,
241
+ LoomcycleError,
242
+ } from "@loomcycle/client";
243
+
244
+ try {
245
+ for await (const ev of client.runStreaming({ /* ... */ })) {}
246
+ } catch (e) {
247
+ if (e instanceof BackpressureError) {
248
+ console.warn(`loomcycle backpressure (status=${e.status}): ${e.message}`);
249
+ } else if (e instanceof LoomcycleError) {
250
+ console.error(`loomcycle error ${e.status}: ${e.bodyText}`);
251
+ } else {
252
+ throw e;
253
+ }
254
+ }
255
+ ```
256
+
257
+ ## Why HTTP, not gRPC
258
+
259
+ Loomcycle's HTTP+SSE surface is the canonical wire contract — every gRPC RPC has an HTTP equivalent (see `internal/api/http/server.go` for the route registrations). The Python adapter (gRPC) and this TS adapter (HTTP) cover the same surface; the choice between them is about ecosystem fit, not capability. HTTP+SSE works through every reverse proxy without special config; gRPC needs HTTP/2 + protoc round trips. For Node.js orchestrators that already have `fetch` in scope, HTTP is the simpler dependency.
260
+
261
+ ## Development
262
+
263
+ ```bash
264
+ cd adapters/ts
265
+ npm install
266
+ npm run typecheck # tsc --noEmit
267
+ npm run build # tsc → dist/
268
+ npm test # vitest run
269
+ npm run test:watch # vitest --watch
270
+ ```
271
+
272
+ Tests use Vitest with a Node environment. They mock `fetch` via constructor injection (no global monkey-patching). See `tests/helpers.ts` for the request-mock pattern.
273
+
274
+ ## License
275
+
276
+ Apache-2.0.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * LoomcycleClient — the single public class exported by
3
+ * @loomcycle/client. Speaks HTTP+SSE to a running loomcycle sidecar.
4
+ *
5
+ * hooks-connector PR C: full Python-adapter parity + hook management.
6
+ * 27 methods total — 26 async (run streaming, continuation, agent
7
+ * metadata, transcript, health, users, pause/resume/state, snapshot
8
+ * lifecycle capture / list / get / restore / delete, memory admin,
9
+ * interruption listing + resolve, hook registration / list / delete)
10
+ * plus one synchronous helper (exportSnapshotURL builds a URL string
11
+ * without issuing a request).
12
+ *
13
+ * Construction:
14
+ *
15
+ * const client = new LoomcycleClient({
16
+ * baseUrl: "http://127.0.0.1:8787", // or process.env.LOOMCYCLE_BASE_URL
17
+ * authToken: "...", // or process.env.LOOMCYCLE_AUTH_TOKEN
18
+ * });
19
+ *
20
+ * Streaming methods (`runStreaming`, `continueSession`) return
21
+ * AsyncIterable<AgentEvent>; non-streaming methods return
22
+ * Promise<T>. Non-2xx responses throw typed errors from errors.ts
23
+ * via fetch-helpers.ts:raiseFromResponse — see README.md for the
24
+ * full mapping table.
25
+ */
26
+ import type { Agent, AgentEvent, AgentStatus, CancelAgentResult, ClientOptions, ContinueOptions, CreateSnapshotOptions, HealthResponse, Hook, InterruptListResponse, InterruptStatus, ListUsersResponse, MemoryEntriesResponse, MemoryEntryResponse, MemoryScopeIDsResponse, MemoryScopesResponse, PauseResult, RegisterHookOptions, RegisterHookResponse, ResolveInterruptOptions, ResumeResult, RunOptions, RuntimeStateResponse, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotRestoreResponse, TranscriptResponse } from "./types.js";
27
+ export declare class LoomcycleClient {
28
+ private ctx;
29
+ constructor(opts?: ClientOptions);
30
+ /**
31
+ * Run an agent and stream events. Returns AsyncIterable<AgentEvent>;
32
+ * the iterator completes when the server closes the SSE stream.
33
+ *
34
+ * Errors during the run surface as `{ type: "error", error }` events;
35
+ * only transport / HTTP-level failures throw — and those throw typed
36
+ * errors (e.g. AuthError for 401, BackpressureError for 429).
37
+ */
38
+ runStreaming(opts: RunOptions): AsyncIterable<AgentEvent>;
39
+ /**
40
+ * Continue an existing session with a new run. The session's prior
41
+ * transcript is replayed into the model's context server-side;
42
+ * this iterator yields only the NEW events from the continuation.
43
+ *
44
+ * Raises SessionNotFoundError when sessionId is unknown,
45
+ * SessionBusyError when another request is in flight on the same
46
+ * session.
47
+ */
48
+ continueSession(opts: ContinueOptions): AsyncIterable<AgentEvent>;
49
+ /** Read one agent's status + usage stats. Raises AgentNotFoundError
50
+ * when the agent_id is unknown. */
51
+ getAgent(agentId: string, opts?: {
52
+ signal?: AbortSignal;
53
+ }): Promise<Agent>;
54
+ /** Cancel a live agent (cascades to children via parent_agent_id).
55
+ * Returns count of agents cancelled. Idempotent — already-terminated
56
+ * agents return 0. */
57
+ cancelAgent(agentId: string, opts?: {
58
+ reason?: string;
59
+ signal?: AbortSignal;
60
+ }): Promise<CancelAgentResult>;
61
+ /** List a user's recent agent runs, optionally filtered by status. */
62
+ listUserAgents(userId: string, opts?: {
63
+ status?: AgentStatus;
64
+ signal?: AbortSignal;
65
+ }): Promise<Agent[]>;
66
+ /** Read the full event log for a session. Each entry has seq,
67
+ * run_id, ts_ns, type, event (the providers.Event payload). */
68
+ getTranscript(sessionId: string, opts?: {
69
+ signal?: AbortSignal;
70
+ }): Promise<TranscriptResponse>;
71
+ /** Liveness probe. Unauthenticated. Returns build info + uptime.
72
+ * Hits /healthz, not /v1/. */
73
+ health(opts?: {
74
+ signal?: AbortSignal;
75
+ }): Promise<HealthResponse>;
76
+ /** Admin: list known users with running-count summary. Drives the
77
+ * Web UI's user picker; operators with bearer auth can call too. */
78
+ listUsers(opts?: {
79
+ signal?: AbortSignal;
80
+ }): Promise<ListUsersResponse>;
81
+ /** Quiesce the runtime. Idempotent tools cancel immediately;
82
+ * non-idempotent + external tools get a grace window then
83
+ * force-cancel. Raises AlreadyPausingError on 409,
84
+ * PauseNotConfiguredError on 503. */
85
+ pauseRuntime(opts?: {
86
+ timeoutMs?: number;
87
+ signal?: AbortSignal;
88
+ }): Promise<PauseResult>;
89
+ /** Release the runtime quiesce. Raises NotPausedError on 409. */
90
+ resumeRuntime(opts?: {
91
+ signal?: AbortSignal;
92
+ }): Promise<ResumeResult>;
93
+ /** Current runtime state. Cheap query — atomic state + a
94
+ * bounded snapshots count. */
95
+ getRuntimeState(opts?: {
96
+ signal?: AbortSignal;
97
+ }): Promise<RuntimeStateResponse>;
98
+ /** Capture running-state into a per-section-semver JSON envelope.
99
+ * Raises SnapshotTooLargeError on 413 when the envelope exceeds
100
+ * LOOMCYCLE_SNAPSHOT_MAX_BYTES (default 512 MiB). */
101
+ createSnapshot(opts?: CreateSnapshotOptions & {
102
+ signal?: AbortSignal;
103
+ }): Promise<SnapshotCreateResponse>;
104
+ /** List captured snapshots (most-recent first). Capped at 200
105
+ * server-side; the limit param defaults to 200 too. */
106
+ listSnapshots(opts?: {
107
+ limit?: number;
108
+ labelContains?: string;
109
+ signal?: AbortSignal;
110
+ }): Promise<SnapshotDescriptor[]>;
111
+ /** Fetch the full snapshot envelope including JSON content.
112
+ * Distinct from exportSnapshot (which is operator-facing
113
+ * "where did this land on the host" semantics with a download
114
+ * URL). Raises SnapshotNotFoundError on 404. */
115
+ getSnapshot(snapshotId: string, opts?: {
116
+ signal?: AbortSignal;
117
+ }): Promise<SnapshotEnvelope>;
118
+ /** Returns the URL of the snapshot's canonical envelope —
119
+ * synchronous and side-effect-free; does NOT issue an HTTP
120
+ * request. The endpoint is bearer-authed like every other
121
+ * `/v1/_snapshots/*` route, so callers must attach the same
122
+ * `Authorization: Bearer <token>` header when fetching this
123
+ * URL (e.g. `curl -H "Authorization: Bearer $TOKEN" ...`).
124
+ * There is no token query-param fallback. */
125
+ exportSnapshotURL(snapshotId: string): string;
126
+ /** Restore from a same-instance snapshot id OR an inline
127
+ * envelope JSON. Idempotent: ON CONFLICT DO NOTHING per row;
128
+ * the returned counters reflect rows actually written.
129
+ * Raises SnapshotVersionError on 422 when a section's
130
+ * declared version is newer than the reader supports. */
131
+ restoreSnapshot(opts: {
132
+ snapshotId?: string;
133
+ /** Pass a parsed JSON object (e.g. `getSnapshot.json_content` or
134
+ * the result of JSON.parse on an exported envelope). */
135
+ json?: unknown;
136
+ includeHistory?: boolean;
137
+ signal?: AbortSignal;
138
+ }): Promise<SnapshotRestoreResponse>;
139
+ /** Delete a snapshot. Idempotent — succeeds whether or not the
140
+ * row existed (server returns 204 in both cases). */
141
+ deleteSnapshot(snapshotId: string, opts?: {
142
+ signal?: AbortSignal;
143
+ }): Promise<void>;
144
+ /** List the kinds of memory scopes the server knows about
145
+ * (agent, user — or whatever the operator yaml declares). */
146
+ listMemoryScopes(opts?: {
147
+ signal?: AbortSignal;
148
+ }): Promise<MemoryScopesResponse>;
149
+ /** List the scope_ids that have at least one memory row under
150
+ * a given scope. */
151
+ listMemoryScopeIDs(scope: string, opts?: {
152
+ signal?: AbortSignal;
153
+ }): Promise<MemoryScopeIDsResponse>;
154
+ /** List memory entries under a (scope, scope_id) tuple.
155
+ * Optional prefix narrows by key prefix. */
156
+ listMemoryEntries(scope: string, scopeID: string, opts?: {
157
+ prefix?: string;
158
+ limit?: number;
159
+ signal?: AbortSignal;
160
+ }): Promise<MemoryEntriesResponse>;
161
+ /** Read a single memory entry by (scope, scope_id, key). */
162
+ getMemoryEntry(scope: string, scopeID: string, key: string, opts?: {
163
+ signal?: AbortSignal;
164
+ }): Promise<MemoryEntryResponse>;
165
+ /** List interrupts addressable to a user_id. Default filter is
166
+ * status=pending. */
167
+ listUserInterrupts(userId: string, opts?: {
168
+ status?: InterruptStatus;
169
+ signal?: AbortSignal;
170
+ }): Promise<InterruptListResponse>;
171
+ /** List interrupts emitted by a specific run. */
172
+ listRunInterrupts(runId: string, opts?: {
173
+ status?: InterruptStatus;
174
+ signal?: AbortSignal;
175
+ }): Promise<InterruptListResponse>;
176
+ /** Resolve a pending Interruption.ask from outside the agent
177
+ * loop. Lets a TS-side dashboard or service act as the human
178
+ * answerer when operator yaml configures the consumer-MCP
179
+ * backend. */
180
+ resolveInterrupt(runId: string, interruptId: string, opts: ResolveInterruptOptions & {
181
+ signal?: AbortSignal;
182
+ }): Promise<unknown>;
183
+ /** Register a pre- or post-tool webhook. The callback_url must be
184
+ * an http:// or https:// endpoint the CONSUMER runs — loomcycle
185
+ * POSTs PreHookCall / PostHookCall payloads to it. This method
186
+ * manages registration only; the receiver is the consumer's own
187
+ * HTTP framework (Express, Next.js, etc.).
188
+ *
189
+ * Re-registering the same (owner, name) replaces the prior entry
190
+ * with a fresh id (idempotent app-restart contract).
191
+ *
192
+ * Raises InvalidArgumentError on 400 (bad URL / phase / missing
193
+ * required fields). */
194
+ registerHook(opts: RegisterHookOptions & {
195
+ signal?: AbortSignal;
196
+ }): Promise<RegisterHookResponse>;
197
+ /** List every currently-registered hook. Returns the array
198
+ * unwrapped (the wire envelope is `{hooks: [...]}` — we strip
199
+ * the envelope to match listUserAgents). In-memory only — empty
200
+ * after a loomcycle restart. */
201
+ listHooks(opts?: {
202
+ signal?: AbortSignal;
203
+ }): Promise<Hook[]>;
204
+ /** Delete a registered hook by id. Raises HookNotFoundError on
205
+ * 404. Returns void on success (the HTTP 200 body `{deleted: id}`
206
+ * is dropped — callers already know the id they passed). */
207
+ deleteHook(id: string, opts?: {
208
+ signal?: AbortSignal;
209
+ }): Promise<void>;
210
+ /** Shared SSE POST → stream-of-AgentEvent path. Used by
211
+ * runStreaming + continueSession. */
212
+ private streamSSE;
213
+ }