@agent-compose/sdk 0.2.1 → 0.2.2
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/package.json +10 -2
- package/src/agent/agent-loop.ts +131 -0
- package/src/agent/protocol-suffix.md +57 -0
- package/src/agent/protocol.ts +22 -0
- package/src/agent/run-agent.ts +141 -0
- package/src/client.ts +446 -0
- package/src/env.d.ts +5 -0
- package/src/errors.ts +10 -0
- package/src/index.ts +111 -0
- package/src/runtimes/claude.ts +305 -0
- package/src/runtimes/openai-desktop.ts +151 -0
- package/src/sandbox.ts +458 -0
- package/src/sse.ts +56 -0
- package/src/types/events.ts +51 -0
- package/src/types/protocol.ts +74 -0
- package/src/types/runtime.ts +59 -0
- package/src/types/sandbox-environment.ts +64 -0
- package/src/types/sandbox.ts +51 -0
- package/src/types/workflow.ts +128 -0
- package/src/utils/bundler.ts +81 -0
- package/src/utils/discovery.ts +4 -0
- package/src/utils/errors.ts +4 -0
- package/src/utils/schemas.ts +10 -0
- package/src/utils/source-loader.ts +16 -0
- package/src/workflows/engine.ts +110 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentComposeClient — HTTP client for the agent-compose server API.
|
|
3
|
+
*
|
|
4
|
+
* Templates always live inside a factory (project). Methods that touch a
|
|
5
|
+
* specific template (`register`, `invoke`, `setSecret`, `listSecrets`,
|
|
6
|
+
* `deleteSecret`) take an optional `factorySlug`; when omitted, they target
|
|
7
|
+
* the team's auto-created `default` factory. There is no second URL space
|
|
8
|
+
* for "templates without a factory" — every workflow belongs to exactly one.
|
|
9
|
+
*
|
|
10
|
+
* `register()` accepts pre-built sources — use the CLI (`agent-compose
|
|
11
|
+
* register`) or build sources yourself and pass them directly.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { ofetch } from "ofetch";
|
|
15
|
+
import { AgentComposeError } from "./errors.js";
|
|
16
|
+
import { parseSseStream } from "./sse.js";
|
|
17
|
+
import type { RunEvent } from "./types/events.js";
|
|
18
|
+
import type { SandboxNetworkPolicy } from "./sandbox.js";
|
|
19
|
+
|
|
20
|
+
/** UUID-v4-ish — matches the server-side predicate. Used to auto-detect
|
|
21
|
+
* that `process.env.RUN_ID` was injected by the runner sandbox (rather
|
|
22
|
+
* than being set by accident), so we only propagate it when it looks real. */
|
|
23
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
|
+
|
|
25
|
+
/** Default factory slug — every team gets one auto-created at sign-up.
|
|
26
|
+
* Methods that don't take an explicit `factorySlug` resolve to this. */
|
|
27
|
+
const DEFAULT_FACTORY = "default";
|
|
28
|
+
|
|
29
|
+
/** Read-once cached parentRunId from `process.env.RUN_ID` at module load.
|
|
30
|
+
* `null` if unset or malformed. Exported so tests can override-and-reset. */
|
|
31
|
+
function detectAmbientParentRunId(): string | null {
|
|
32
|
+
const envRunId = (typeof process !== "undefined" ? process.env?.RUN_ID : undefined);
|
|
33
|
+
return envRunId && UUID_REGEX.test(envRunId) ? envRunId : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Build the `/api/v1/factories/:slug/templates/...` prefix. Slug + name are
|
|
37
|
+
* URL-encoded as defense-in-depth: today's server-side patterns
|
|
38
|
+
* (`^[a-z][a-z0-9-]*$`) can't produce reserved characters, but a future
|
|
39
|
+
* relaxation would otherwise quietly become an injection vector. */
|
|
40
|
+
function templatePath(factorySlug: string, ...rest: string[]): string {
|
|
41
|
+
const tail = rest.length > 0
|
|
42
|
+
? "/" + rest.map(encodeURIComponent).join("/")
|
|
43
|
+
: "";
|
|
44
|
+
return `/api/v1/factories/${encodeURIComponent(factorySlug)}/templates${tail}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RegisterResult {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
version: string;
|
|
51
|
+
runtimes?: Array<{ name: string; version: string; id: string }>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface RunStatus {
|
|
55
|
+
id: string;
|
|
56
|
+
status: "running" | "success" | "failed" | "abandoned" | "canceled";
|
|
57
|
+
output?: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Row shape returned by `GET /api-keys`. */
|
|
61
|
+
export interface ApiKey {
|
|
62
|
+
object: "api_key";
|
|
63
|
+
id: string;
|
|
64
|
+
name: string | null;
|
|
65
|
+
last4: string | null;
|
|
66
|
+
scopes: string[];
|
|
67
|
+
teamId: string;
|
|
68
|
+
createdByUserId: string | null;
|
|
69
|
+
/** Non-null when the key is restricted to a single factory. */
|
|
70
|
+
factoryId: string | null;
|
|
71
|
+
createdAt: string;
|
|
72
|
+
expiresAt: string | null;
|
|
73
|
+
lastUsedAt: string | null;
|
|
74
|
+
revokedAt: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Response from `POST /api-keys`. The `key` field is the plaintext token —
|
|
78
|
+
* shown once at creation, never retrievable again. */
|
|
79
|
+
export interface ApiKeyCreated extends ApiKey { key: string }
|
|
80
|
+
|
|
81
|
+
/** Single rollup row from `GET /api/v1/usage`. */
|
|
82
|
+
export interface UsageRollupRow {
|
|
83
|
+
eventType: string;
|
|
84
|
+
unit: string;
|
|
85
|
+
total: number;
|
|
86
|
+
tags: Record<string, unknown>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Response from `GET /api/v1/usage`. */
|
|
90
|
+
export interface UsageResponse {
|
|
91
|
+
object: "list";
|
|
92
|
+
data: UsageRollupRow[];
|
|
93
|
+
has_more: boolean;
|
|
94
|
+
from: string | null;
|
|
95
|
+
to: string | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Response from `POST /api/v1/workflows/:id/cancel`. The endpoint is
|
|
99
|
+
* idempotent: cancelling a run that's already terminal returns its current
|
|
100
|
+
* outcome verbatim (rather than throwing or pretending it just canceled),
|
|
101
|
+
* so `status` widens to every terminal value the server might surface. */
|
|
102
|
+
export interface CancelRunResponse {
|
|
103
|
+
runId: string;
|
|
104
|
+
status: "canceled" | "success" | "failed" | "abandoned";
|
|
105
|
+
canceledAt: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SnapshotListEntry {
|
|
109
|
+
runId: string;
|
|
110
|
+
workflow: string | null;
|
|
111
|
+
version: string | null;
|
|
112
|
+
vercelSnapshotId: string;
|
|
113
|
+
endedAt: string | null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class AgentComposeClient {
|
|
117
|
+
private readonly fetch: typeof ofetch;
|
|
118
|
+
private readonly baseUrl: string;
|
|
119
|
+
private readonly apiKey: string;
|
|
120
|
+
|
|
121
|
+
constructor(baseUrl: string, apiKey: string) {
|
|
122
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
123
|
+
this.apiKey = apiKey;
|
|
124
|
+
this.fetch = ofetch.create({
|
|
125
|
+
baseURL: this.baseUrl,
|
|
126
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
127
|
+
async onResponseError({ response }) {
|
|
128
|
+
const body = response._data as { error?: string } | undefined;
|
|
129
|
+
throw new AgentComposeError(response.status, body?.error ?? response.statusText);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Register (or update) a workflow template inside a factory. Defaults to
|
|
135
|
+
* the team's `default` factory when `factorySlug` is omitted. */
|
|
136
|
+
register(payload: {
|
|
137
|
+
name: string;
|
|
138
|
+
source: string;
|
|
139
|
+
version?: string;
|
|
140
|
+
schedule?: string;
|
|
141
|
+
runtimes?: Array<{ name: string; source: string }>;
|
|
142
|
+
networkPolicy?: unknown;
|
|
143
|
+
placeholders?: Record<string, string>;
|
|
144
|
+
/** Reference to a snapshot the runner should boot from at run start.
|
|
145
|
+
* A run UUID, workflow name, or `name@version`. The referenced
|
|
146
|
+
* workflow must have been registered with `--build` (or any prior
|
|
147
|
+
* successful run with `saveSnapshot: true`). Per-invocation
|
|
148
|
+
* `invoke({ snapshot })` overrides this default. */
|
|
149
|
+
snapshot?: string;
|
|
150
|
+
/** If true, runs default to capturing a long-lived sandbox snapshot on
|
|
151
|
+
* success. Individual invocations can override via
|
|
152
|
+
* `invoke(..., { saveSnapshot })`. */
|
|
153
|
+
saveSnapshot?: boolean;
|
|
154
|
+
/** Factory slug. Defaults to `"default"`. */
|
|
155
|
+
factorySlug?: string;
|
|
156
|
+
}): Promise<RegisterResult> {
|
|
157
|
+
const { factorySlug = DEFAULT_FACTORY, ...body } = payload;
|
|
158
|
+
return this.fetch(templatePath(factorySlug), { method: "POST", body });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Invoke a workflow. Returns run ID immediately — workflow runs asynchronously.
|
|
162
|
+
*
|
|
163
|
+
* `snapshot`: per-invocation override of the template-level `snapshot`
|
|
164
|
+
* field. Same forms — a run UUID, a workflow name, or `name@version`.
|
|
165
|
+
* Use this when one template needs to boot from many different
|
|
166
|
+
* snapshots (e.g. a benchmark workflow running against a different
|
|
167
|
+
* starting state per invocation). Does not change the template's
|
|
168
|
+
* registered default; only affects this run.
|
|
169
|
+
*
|
|
170
|
+
* `saveSnapshot`: `true` overrides the workflow default; `false` opts
|
|
171
|
+
* out. When the run captures a snapshot, its id is stamped on the row
|
|
172
|
+
* and can be referenced as the `snapshot` field on other workflows.
|
|
173
|
+
*
|
|
174
|
+
* `parentRunId`: links the new run to another of the same account's
|
|
175
|
+
* currently-running runs. When omitted, the client auto-detects from
|
|
176
|
+
* `process.env.RUN_ID` — set by the runner sandbox on every dispatch,
|
|
177
|
+
* so workflows invoking other workflows get the parent/child tree for
|
|
178
|
+
* free. Pass `null` to suppress auto-detection (e.g. invoking a top-level
|
|
179
|
+
* sibling workflow from inside a runner for a reason unrelated to the
|
|
180
|
+
* current run). Explicit non-null wins over auto-detection.
|
|
181
|
+
*
|
|
182
|
+
* `factorySlug`: defaults to `"default"`. */
|
|
183
|
+
invoke(
|
|
184
|
+
name: string,
|
|
185
|
+
input?: Record<string, unknown>,
|
|
186
|
+
opts?: {
|
|
187
|
+
snapshot?: string;
|
|
188
|
+
saveSnapshot?: boolean;
|
|
189
|
+
parentRunId?: string | null;
|
|
190
|
+
factorySlug?: string;
|
|
191
|
+
networkPolicy?: SandboxNetworkPolicy;
|
|
192
|
+
placeholders?: Record<string, string>;
|
|
193
|
+
},
|
|
194
|
+
): Promise<{ id: string }> {
|
|
195
|
+
const parentRunId = opts?.parentRunId === undefined
|
|
196
|
+
? detectAmbientParentRunId()
|
|
197
|
+
: opts.parentRunId;
|
|
198
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
199
|
+
return this.fetch(templatePath(factorySlug, name, "invoke"), {
|
|
200
|
+
method: "POST",
|
|
201
|
+
body: {
|
|
202
|
+
input,
|
|
203
|
+
...(opts?.snapshot !== undefined ? { snapshot: opts.snapshot } : {}),
|
|
204
|
+
...(opts?.saveSnapshot !== undefined ? { saveSnapshot: opts.saveSnapshot } : {}),
|
|
205
|
+
...(parentRunId ? { parentRunId } : {}),
|
|
206
|
+
...(opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {}),
|
|
207
|
+
...(opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {}),
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Invoke a workflow and wait for it to settle (success / failed / abandoned).
|
|
213
|
+
* Polls `getStatus` on a fixed interval. Rejects with `AgentComposeError`
|
|
214
|
+
* if the run settles non-success, or a plain `Error` on timeout.
|
|
215
|
+
*
|
|
216
|
+
* Defaults: `timeoutMs = 30min`, `pollIntervalMs = 1000ms`. Tune down for
|
|
217
|
+
* tests, tune up for long-running workflows. The parent-child auto-
|
|
218
|
+
* detection from `invoke()` applies here too. */
|
|
219
|
+
async invokeAndWait(
|
|
220
|
+
name: string,
|
|
221
|
+
input?: Record<string, unknown>,
|
|
222
|
+
opts?: {
|
|
223
|
+
snapshot?: string;
|
|
224
|
+
saveSnapshot?: boolean;
|
|
225
|
+
parentRunId?: string | null;
|
|
226
|
+
factorySlug?: string;
|
|
227
|
+
networkPolicy?: SandboxNetworkPolicy;
|
|
228
|
+
placeholders?: Record<string, string>;
|
|
229
|
+
timeoutMs?: number;
|
|
230
|
+
pollIntervalMs?: number;
|
|
231
|
+
},
|
|
232
|
+
): Promise<RunStatus> {
|
|
233
|
+
const timeoutMs = opts?.timeoutMs ?? 30 * 60 * 1000;
|
|
234
|
+
const pollMs = opts?.pollIntervalMs ?? 1000;
|
|
235
|
+
const { id: runId } = await this.invoke(name, input, {
|
|
236
|
+
...(opts?.snapshot !== undefined ? { snapshot: opts.snapshot } : {}),
|
|
237
|
+
...(opts?.saveSnapshot !== undefined ? { saveSnapshot: opts.saveSnapshot } : {}),
|
|
238
|
+
...(opts?.parentRunId !== undefined ? { parentRunId: opts.parentRunId } : {}),
|
|
239
|
+
...(opts?.factorySlug !== undefined ? { factorySlug: opts.factorySlug } : {}),
|
|
240
|
+
...(opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {}),
|
|
241
|
+
...(opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {}),
|
|
242
|
+
});
|
|
243
|
+
const deadline = Date.now() + timeoutMs;
|
|
244
|
+
while (Date.now() < deadline) {
|
|
245
|
+
const status = await this.getStatus(runId);
|
|
246
|
+
if (status.status === "success" || status.status === "failed" || status.status === "abandoned") {
|
|
247
|
+
return status;
|
|
248
|
+
}
|
|
249
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
250
|
+
}
|
|
251
|
+
// Use AgentComposeError (not plain Error) so catch-blocks handling SDK
|
|
252
|
+
// transport failures also handle timeouts uniformly. HTTP 504 is the
|
|
253
|
+
// closest idiomatic status for "upstream didn't answer in time."
|
|
254
|
+
throw new AgentComposeError(504, `invokeAndWait: run ${runId} did not settle within ${timeoutMs}ms`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** List runs this account has captured snapshots for. */
|
|
258
|
+
async listSnapshots(opts?: { workflow?: string; limit?: number }): Promise<SnapshotListEntry[]> {
|
|
259
|
+
const q = new URLSearchParams();
|
|
260
|
+
if (opts?.workflow) q.set("workflow", opts.workflow);
|
|
261
|
+
if (opts?.limit != null) q.set("limit", String(opts.limit));
|
|
262
|
+
const body = await this.fetch<{ object: "list"; data: SnapshotListEntry[]; has_more: boolean }>(
|
|
263
|
+
`/api/v1/snapshots${q.toString() ? `?${q}` : ""}`,
|
|
264
|
+
);
|
|
265
|
+
return body.data;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Delete the snapshot captured by a specific run. Frees Vercel storage. */
|
|
269
|
+
deleteSnapshot(runId: string): Promise<void> {
|
|
270
|
+
return this.fetch(`/api/v1/workflows/${runId}/snapshot`, { method: "DELETE" });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Poll run status. */
|
|
274
|
+
getStatus(runId: string): Promise<RunStatus> {
|
|
275
|
+
return this.fetch(`/api/v1/workflows/${runId}/status`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** List registered workflow templates. When `factorySlug` is supplied,
|
|
279
|
+
* scopes to that factory; otherwise returns every template the team can
|
|
280
|
+
* see across every factory in one round trip — each row carries its
|
|
281
|
+
* `factorySlug` so callers can route per-template actions to the right
|
|
282
|
+
* factory. */
|
|
283
|
+
async listTemplates(opts?: { factorySlug?: string }): Promise<Array<{ name: string; version: string; factorySlug: string }>> {
|
|
284
|
+
const path = opts?.factorySlug
|
|
285
|
+
? templatePath(opts.factorySlug)
|
|
286
|
+
: "/api/v1/templates";
|
|
287
|
+
const body = await this.fetch<{ templates: Array<{ name: string; version: string; factorySlug: string }> }>(path);
|
|
288
|
+
return body.templates;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Factories ──────────────────────────────────────────────────────────────
|
|
292
|
+
// Projects within a team. Workflows + secrets belong to exactly one factory.
|
|
293
|
+
|
|
294
|
+
/** List factories for the caller's team. */
|
|
295
|
+
async listFactories(): Promise<FactoryRow[]> {
|
|
296
|
+
const body = await this.fetch<{ factories: FactoryRow[] }>("/api/v1/factories");
|
|
297
|
+
return body.factories;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Create a factory. `slug` must be lowercase kebab-case and unique
|
|
301
|
+
* within the team. */
|
|
302
|
+
createFactory(payload: { slug: string; name: string; description?: string }): Promise<FactoryRow> {
|
|
303
|
+
return this.fetch<FactoryRow>("/api/v1/factories", { method: "POST", body: payload });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Get a factory by slug. */
|
|
307
|
+
getFactory(slug: string): Promise<FactoryRow> {
|
|
308
|
+
return this.fetch<FactoryRow>(`/api/v1/factories/${encodeURIComponent(slug)}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Rename or describe a factory. */
|
|
312
|
+
updateFactory(slug: string, updates: { name?: string; description?: string }): Promise<FactoryRow> {
|
|
313
|
+
return this.fetch<FactoryRow>(`/api/v1/factories/${encodeURIComponent(slug)}`, { method: "PATCH", body: updates });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Delete a factory. Refuses `default` and any factory still
|
|
317
|
+
* containing workflows. */
|
|
318
|
+
deleteFactory(slug: string): Promise<void> {
|
|
319
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}`, { method: "DELETE" });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── Secrets ────────────────────────────────────────────────────────────────
|
|
323
|
+
// Scoped to (factory, workflow, key). Stored in GCP Secret Manager;
|
|
324
|
+
// `factorySlug` defaults to `"default"`.
|
|
325
|
+
|
|
326
|
+
/** Create or update a workflow secret. Value is stored in GCP Secret Manager. */
|
|
327
|
+
setSecret(workflowName: string, key: string, value: string, opts?: { factorySlug?: string }): Promise<{ key: string }> {
|
|
328
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
329
|
+
return this.fetch(templatePath(factorySlug, workflowName, "secrets"), {
|
|
330
|
+
method: "POST",
|
|
331
|
+
body: { key, value },
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** List secret keys registered for a workflow (metadata only — values are never returned). */
|
|
336
|
+
async listSecrets(workflowName: string, opts?: { factorySlug?: string }): Promise<Array<{ key: string; createdAt: string; updatedAt: string }>> {
|
|
337
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
338
|
+
const body = await this.fetch<{ secrets: Array<{ secretKey: string; createdAt: string; updatedAt: string }> }>(
|
|
339
|
+
templatePath(factorySlug, workflowName, "secrets"),
|
|
340
|
+
);
|
|
341
|
+
return body.secrets.map(s => ({ key: s.secretKey, createdAt: s.createdAt, updatedAt: s.updatedAt }));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Delete a workflow secret. */
|
|
345
|
+
deleteSecret(workflowName: string, key: string, opts?: { factorySlug?: string }): Promise<void> {
|
|
346
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
347
|
+
return this.fetch(templatePath(factorySlug, workflowName, "secrets", key), { method: "DELETE" });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── API keys ───────────────────────────────────────────────────────────────
|
|
351
|
+
// Both endpoints require an admin-scoped key as the bearer token.
|
|
352
|
+
|
|
353
|
+
/** Create a new API key on the caller's team. The plaintext `key` is
|
|
354
|
+
* returned once — it cannot be retrieved later.
|
|
355
|
+
*
|
|
356
|
+
* When `factorySlug` is set, the new key is restricted to that factory.
|
|
357
|
+
* Factory-scoped keys can only mint other keys bound to the same factory. */
|
|
358
|
+
createApiKey(input: {
|
|
359
|
+
name?: string;
|
|
360
|
+
scopes?: string[];
|
|
361
|
+
expiresAt?: string;
|
|
362
|
+
factorySlug?: string;
|
|
363
|
+
}): Promise<ApiKeyCreated> {
|
|
364
|
+
return this.fetch<ApiKeyCreated>("/api-keys", { method: "POST", body: input });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** List API keys on the caller's team (metadata only — plaintext keys are
|
|
368
|
+
* never returned). */
|
|
369
|
+
async listApiKeys(): Promise<ApiKey[]> {
|
|
370
|
+
const body = await this.fetch<{ object: "list"; data: ApiKey[]; has_more: boolean }>("/api-keys");
|
|
371
|
+
return body.data;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── Usage ──────────────────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
/** Billable usage rollup for the caller's team over the [from, to) window. */
|
|
377
|
+
getUsage(from: Date, to: Date): Promise<UsageResponse> {
|
|
378
|
+
const qs = `?from=${encodeURIComponent(from.toISOString())}&to=${encodeURIComponent(to.toISOString())}`;
|
|
379
|
+
return this.fetch<UsageResponse>(`/api/v1/usage${qs}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── Run control ────────────────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
/** Cancel an in-progress run. Idempotent: cancelling an already-terminal
|
|
385
|
+
* run returns the current state without throwing. The server stamps the
|
|
386
|
+
* run as `canceled` and kills any live sandboxes. */
|
|
387
|
+
cancelRun(runId: string): Promise<CancelRunResponse> {
|
|
388
|
+
return this.fetch<CancelRunResponse>(
|
|
389
|
+
`/api/v1/workflows/${encodeURIComponent(runId)}/cancel`,
|
|
390
|
+
{ method: "POST" },
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Stream lifecycle events for a run as an async iterable. Yields parsed
|
|
395
|
+
* `RunEvent` payloads in order; caller breaks on terminal events
|
|
396
|
+
* (`run_complete`, `run_failed`, `run_canceled`).
|
|
397
|
+
*
|
|
398
|
+
* `lastEventId` enables resume — pass the highest `seq` you've already
|
|
399
|
+
* processed to receive only events you missed.
|
|
400
|
+
*
|
|
401
|
+
* `signal` can be used to abort the stream from the caller side.
|
|
402
|
+
*
|
|
403
|
+
* Uses raw `fetch` (not ofetch) because SSE requires access to the
|
|
404
|
+
* response's `ReadableStream`, which ofetch consumes when parsing. Auth
|
|
405
|
+
* + base URL are still sourced from the same constructor inputs, and
|
|
406
|
+
* non-2xx responses throw the same `AgentComposeError`. */
|
|
407
|
+
async *streamRunLogs(
|
|
408
|
+
runId: string,
|
|
409
|
+
opts?: { lastEventId?: number; signal?: AbortSignal },
|
|
410
|
+
): AsyncGenerator<RunEvent> {
|
|
411
|
+
const headers: Record<string, string> = { Authorization: `Bearer ${this.apiKey}` };
|
|
412
|
+
if (opts?.lastEventId && opts.lastEventId > 0) {
|
|
413
|
+
headers["Last-Event-ID"] = String(opts.lastEventId);
|
|
414
|
+
}
|
|
415
|
+
const res = await fetch(`${this.baseUrl}/api/v1/workflows/${encodeURIComponent(runId)}/stream`, {
|
|
416
|
+
headers,
|
|
417
|
+
...(opts?.signal ? { signal: opts.signal } : {}),
|
|
418
|
+
});
|
|
419
|
+
if (!res.ok || !res.body) {
|
|
420
|
+
let message = res.statusText;
|
|
421
|
+
try {
|
|
422
|
+
const body = await res.json() as { error?: string };
|
|
423
|
+
if (body.error) message = body.error;
|
|
424
|
+
} catch { /* non-JSON error body — fall back to statusText */ }
|
|
425
|
+
throw new AgentComposeError(res.status, message);
|
|
426
|
+
}
|
|
427
|
+
for await (const ev of parseSseStream(res.body)) {
|
|
428
|
+
// The server's `data` payload already includes `event`, `runId`, `seq`,
|
|
429
|
+
// `at`, and the per-event payload — so `ev.data` IS the `RunEvent`.
|
|
430
|
+
// Cast directly; unknown future event names flow through untyped, and
|
|
431
|
+
// callers using the discriminated union see them via the default branch.
|
|
432
|
+
yield ev.data as unknown as RunEvent;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** A factory: a project-level grouping of workflows inside a team. */
|
|
438
|
+
export interface FactoryRow {
|
|
439
|
+
id: string;
|
|
440
|
+
teamId: string;
|
|
441
|
+
slug: string;
|
|
442
|
+
name: string;
|
|
443
|
+
description: string | null;
|
|
444
|
+
createdAt: string;
|
|
445
|
+
updatedAt: string;
|
|
446
|
+
}
|
package/src/env.d.ts
ADDED
package/src/errors.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Thrown by AgentComposeClient when the server returns a non-2xx response. */
|
|
2
|
+
export class AgentComposeError extends Error {
|
|
3
|
+
constructor(
|
|
4
|
+
public readonly status: number,
|
|
5
|
+
message: string,
|
|
6
|
+
) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "AgentComposeError";
|
|
9
|
+
}
|
|
10
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/// <reference path="./env.d.ts" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @agent-compose/sdk
|
|
5
|
+
*
|
|
6
|
+
* Tools for defining runtimes and workflows, registering/invoking them
|
|
7
|
+
* against an agent-compose server, and running LLM agent loops inside a
|
|
8
|
+
* workflow via `runAgent(opts)`.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { defineWorkflow, runAgent, AgentComposeClient } from "@agent-compose/sdk";
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Factory functions
|
|
17
|
+
export { defineRuntime } from "./types/runtime.js";
|
|
18
|
+
export { defineWorkflow } from "./types/workflow.js";
|
|
19
|
+
export type { WorkflowDefinition } from "./types/workflow.js";
|
|
20
|
+
export { defineSandboxEnvironment } from "./types/sandbox-environment.js";
|
|
21
|
+
export type { SandboxEnvironmentDefinition } from "./types/sandbox-environment.js";
|
|
22
|
+
|
|
23
|
+
// Runtime types
|
|
24
|
+
export type {
|
|
25
|
+
AgentRuntime,
|
|
26
|
+
McpServerConfig,
|
|
27
|
+
ModelExecutionContract,
|
|
28
|
+
RuntimeOptions,
|
|
29
|
+
} from "./types/runtime.js";
|
|
30
|
+
|
|
31
|
+
// Workflow types and runtime utilities
|
|
32
|
+
export type {
|
|
33
|
+
WorkflowFn,
|
|
34
|
+
WorkflowCtx,
|
|
35
|
+
WorkflowRun,
|
|
36
|
+
AgentBudget,
|
|
37
|
+
WorkflowHooks,
|
|
38
|
+
} from "./types/workflow.js";
|
|
39
|
+
|
|
40
|
+
// Protocol types (agent-loop input/output shapes)
|
|
41
|
+
export type {
|
|
42
|
+
AgentMessage,
|
|
43
|
+
AgentMessageInit,
|
|
44
|
+
AgentMessageText,
|
|
45
|
+
AgentMessageThinking,
|
|
46
|
+
AgentMessageToolUse,
|
|
47
|
+
AgentMessageToolResult,
|
|
48
|
+
AgentMessageDone,
|
|
49
|
+
AgentMessageError,
|
|
50
|
+
AgentMessageUsage,
|
|
51
|
+
AgentStatus,
|
|
52
|
+
} from "./types/protocol.js";
|
|
53
|
+
|
|
54
|
+
// Sandbox types
|
|
55
|
+
export type {
|
|
56
|
+
SandboxProvider,
|
|
57
|
+
DesktopSandboxProvider,
|
|
58
|
+
} from "./types/sandbox.js";
|
|
59
|
+
|
|
60
|
+
// HTTP client
|
|
61
|
+
export { AgentComposeClient } from "./client.js";
|
|
62
|
+
export type {
|
|
63
|
+
RegisterResult, RunStatus, FactoryRow, SnapshotListEntry,
|
|
64
|
+
ApiKey, ApiKeyCreated,
|
|
65
|
+
UsageRollupRow, UsageResponse,
|
|
66
|
+
CancelRunResponse,
|
|
67
|
+
} from "./client.js";
|
|
68
|
+
|
|
69
|
+
// SSE parser — exposed so tests and downstream callers can reuse it.
|
|
70
|
+
export { parseSseStream } from "./sse.js";
|
|
71
|
+
|
|
72
|
+
// Errors and utilities
|
|
73
|
+
export { AgentComposeError } from "./errors.js";
|
|
74
|
+
export { formatError } from "./utils/errors.js";
|
|
75
|
+
|
|
76
|
+
// Source discovery + bundling utilities
|
|
77
|
+
export { discoverRuntimeName } from "./utils/discovery.js";
|
|
78
|
+
export { bundleWorkflow } from "./utils/bundler.js";
|
|
79
|
+
export type { BundledWorkflow } from "./utils/bundler.js";
|
|
80
|
+
|
|
81
|
+
// Zod schemas
|
|
82
|
+
export { AgentStatusSchema } from "./utils/schemas.js";
|
|
83
|
+
|
|
84
|
+
// Built-in runtimes
|
|
85
|
+
// Note: openAIDesktopRuntime is NOT exported here — it depends on sharp (native bindings)
|
|
86
|
+
// which can't be cross-compiled. Import directly: import openAIDesktopRuntime from "@agent-compose/sdk/runtimes/openai-desktop.js"
|
|
87
|
+
export { createClaudeRuntime, ClaudeRunner } from "./runtimes/claude.js";
|
|
88
|
+
export type { ClaudeRuntimeConfig } from "./runtimes/claude.js";
|
|
89
|
+
export { default as claudeRuntime } from "./runtimes/claude.js";
|
|
90
|
+
|
|
91
|
+
// Streaming event contract
|
|
92
|
+
export type { RunEvent } from "./types/events.js";
|
|
93
|
+
|
|
94
|
+
// Sandbox providers
|
|
95
|
+
export { createSandbox, reconnectSandbox, killAllSandboxes, killSandboxById,
|
|
96
|
+
getSandboxQuotas, listOwnedSandboxes, deleteSandboxSnapshot,
|
|
97
|
+
makeSandboxProvider, makeDesktopSandboxProvider,
|
|
98
|
+
parseSseExecStream, AGENT_COMPOSE_TAG } from "./sandbox.js";
|
|
99
|
+
export type { SandboxCreateOpts, SandboxNetworkPolicy, OwnedSandbox } from "./sandbox.js";
|
|
100
|
+
|
|
101
|
+
// Workflow engine
|
|
102
|
+
export { runWorkflow, WorkflowError, EngineError, classifyError, parseNameVersion } from "./workflows/engine.js";
|
|
103
|
+
export type { WorkflowResult, EngineSubsystem } from "./workflows/engine.js";
|
|
104
|
+
|
|
105
|
+
// Agent loop — for workflows that embed an LLM agent in their run() body.
|
|
106
|
+
export { agentLoop, parseAgentStatus, DEFAULT_CLAUDE_MODEL } from "./agent/agent-loop.js";
|
|
107
|
+
export type { AgentLoopResult } from "./agent/agent-loop.js";
|
|
108
|
+
export { runAgent } from "./agent/run-agent.js";
|
|
109
|
+
export type { RunAgentOpts } from "./agent/run-agent.js";
|
|
110
|
+
export { AgentMessageSchema, parseAgentResponse } from "./agent/protocol.js";
|
|
111
|
+
export { importSourceModule, TMP_DIR, LATEST_VERSION } from "./utils/source-loader.js";
|