@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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent runtime types — the execution strategy for driving a model.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { SandboxProvider } from "./sandbox.js";
|
|
6
|
+
import type { AgentMessage } from "./protocol.js";
|
|
7
|
+
|
|
8
|
+
/** Configuration for a single MCP server. */
|
|
9
|
+
export interface McpServerConfig {
|
|
10
|
+
command: string;
|
|
11
|
+
args?: string[];
|
|
12
|
+
env?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Options passed to a runtime when creating a ModelExecutionContract. */
|
|
16
|
+
export interface RuntimeOptions {
|
|
17
|
+
allowedTools?: string[];
|
|
18
|
+
maxTurns?: number;
|
|
19
|
+
model?: string;
|
|
20
|
+
/** Label prefix for all stderr output. */
|
|
21
|
+
label?: string;
|
|
22
|
+
/** Working directory — the claude CLI process starts here so relative paths work correctly. */
|
|
23
|
+
cwd?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The execution contract every runtime must satisfy.
|
|
28
|
+
* Given a prompt, yield a stream of agent events.
|
|
29
|
+
*/
|
|
30
|
+
export interface ModelExecutionContract {
|
|
31
|
+
sendMessage(opts: {
|
|
32
|
+
prompt: string;
|
|
33
|
+
sessionId?: string;
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
}): AsyncGenerator<AgentMessage>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An agent runtime — a model execution strategy that drives an agent inside a sandbox.
|
|
40
|
+
*
|
|
41
|
+
* The sandbox provider (e.g. "vercel", "e2b") is an infrastructure concern
|
|
42
|
+
* configured via SANDBOX_PROVIDER — not part of the runtime definition.
|
|
43
|
+
* For non-sandbox agents (API calls, etc.) make the call directly in the workflow;
|
|
44
|
+
* spawnAgent is a sandbox concept.
|
|
45
|
+
*/
|
|
46
|
+
export interface AgentRuntime<S extends SandboxProvider = SandboxProvider> {
|
|
47
|
+
create(sandbox: S, opts: RuntimeOptions): ModelExecutionContract;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Declare a registerable runtime. Validates required fields at module load time.
|
|
52
|
+
* Use `export default defineRuntime({...})` as the single export in runtime source files.
|
|
53
|
+
*/
|
|
54
|
+
export function defineRuntime<S extends SandboxProvider = SandboxProvider>(
|
|
55
|
+
pkg: AgentRuntime<S>,
|
|
56
|
+
): AgentRuntime<S> {
|
|
57
|
+
if (typeof pkg.create !== "function") throw new Error("defineRuntime: 'create' is required");
|
|
58
|
+
return pkg;
|
|
59
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A sandbox environment is a workflow whose job is to leave its VM in a
|
|
3
|
+
* configured state, then snapshot it so other workflows can boot from that
|
|
4
|
+
* state. Capture is **opt-in**: the wrapper below sets `saveSnapshot: true`
|
|
5
|
+
* by default (opt out with `saveSnapshot: false` if you only want side
|
|
6
|
+
* effects).
|
|
7
|
+
*
|
|
8
|
+
* Once captured, reference the snapshot from another workflow's `snapshot`
|
|
9
|
+
* field by run UUID, workflow name, or `name@version`:
|
|
10
|
+
*
|
|
11
|
+
* snapshot: "my-setup" // most recent successful snapshot
|
|
12
|
+
* snapshot: "my-setup@v1" // version-scoped recency
|
|
13
|
+
* snapshot: "<run-uuid>" // pinned to an exact run
|
|
14
|
+
*
|
|
15
|
+
* `defineSandboxEnvironment` is sugar over `defineWorkflow` — it makes the
|
|
16
|
+
* setup recipe read like an imperative script by supplying the local
|
|
17
|
+
* sandbox provider (the runner's own VM) to the callback.
|
|
18
|
+
*
|
|
19
|
+
* Typical flow:
|
|
20
|
+
* 1. Author a setup file with `defineSandboxEnvironment`.
|
|
21
|
+
* 2. `agentc register setup.ts --build` — registers and invokes once to
|
|
22
|
+
* capture the snapshot.
|
|
23
|
+
* 3. Other workflows declare `snapshot: "name"` and boot from it.
|
|
24
|
+
* 4. `agentc snapshot list` / `delete` to manage the Vercel storage bill.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { defineSandboxEnvironment } from "@agent-compose/sdk";
|
|
29
|
+
*
|
|
30
|
+
* export default defineSandboxEnvironment({
|
|
31
|
+
* name: "python",
|
|
32
|
+
* setup: async (sb) => {
|
|
33
|
+
* await sb.commands.run("sudo dnf install -y python3-pip");
|
|
34
|
+
* },
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type { SandboxProvider } from "./sandbox.js";
|
|
40
|
+
import { makeLocalSandboxProvider } from "../sandbox.js";
|
|
41
|
+
import { defineWorkflow } from "./workflow.js";
|
|
42
|
+
import type { WorkflowFn } from "./workflow.js";
|
|
43
|
+
|
|
44
|
+
export interface SandboxEnvironmentDefinition {
|
|
45
|
+
name: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
setup: (sb: SandboxProvider) => Promise<void>;
|
|
48
|
+
/** Override the sugar's `saveSnapshot: true` default. Set `false` to opt
|
|
49
|
+
* out of snapshot capture (rarely useful — an env with no snapshot can't
|
|
50
|
+
* be referenced as the `snapshot` field on another workflow). */
|
|
51
|
+
saveSnapshot?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function defineSandboxEnvironment(
|
|
55
|
+
env: SandboxEnvironmentDefinition,
|
|
56
|
+
): WorkflowFn<void> {
|
|
57
|
+
if (typeof env.setup !== "function") {
|
|
58
|
+
throw new Error(`defineSandboxEnvironment(${env.name}): 'setup' must be a function`);
|
|
59
|
+
}
|
|
60
|
+
return defineWorkflow({
|
|
61
|
+
saveSnapshot: env.saveSnapshot ?? true,
|
|
62
|
+
run: () => env.setup(makeLocalSandboxProvider()),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox abstraction — the compute environment an agent runs inside.
|
|
3
|
+
* Provider-agnostic: E2B, Vercel, Docker, or any other backend implements this.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Base compute interface — pure I/O, no filesystem path or git concerns. */
|
|
7
|
+
export interface SandboxProvider {
|
|
8
|
+
sandboxId: string;
|
|
9
|
+
/** Working directory for the agent process. Set by onStart after environment setup. */
|
|
10
|
+
cwd?: string;
|
|
11
|
+
commands: {
|
|
12
|
+
run(
|
|
13
|
+
cmd: string,
|
|
14
|
+
opts?: {
|
|
15
|
+
cwd?: string;
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
envs?: Record<string, string>;
|
|
18
|
+
onStdout?: (data: string) => void;
|
|
19
|
+
onStderr?: (data: string) => void;
|
|
20
|
+
background?: boolean;
|
|
21
|
+
},
|
|
22
|
+
): Promise<{ exitCode: number; stdout: string }>;
|
|
23
|
+
};
|
|
24
|
+
files: {
|
|
25
|
+
write(path: string, content: string): Promise<unknown>;
|
|
26
|
+
};
|
|
27
|
+
kill(): Promise<void>;
|
|
28
|
+
/** Capture the running sandbox's state as a reusable snapshot. Vercel
|
|
29
|
+
* supports it natively; E2B's model is Dockerfile-based and doesn't map
|
|
30
|
+
* cleanly — `undefined` on providers that don't. Used by the server's
|
|
31
|
+
* `--build` flow to stamp the snapshot id on the workflow row. */
|
|
32
|
+
snapshot?(): Promise<{ snapshotId: string }>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Stateless provider-level snapshot deletion — no live sandbox needed,
|
|
36
|
+
* called from customer-initiated `DELETE /workflows/:runId/snapshot`. */
|
|
37
|
+
export type DeleteSnapshotFn = (snapshotId: string) => Promise<void>;
|
|
38
|
+
|
|
39
|
+
/** Extends SandboxProvider with desktop GUI capabilities. */
|
|
40
|
+
export interface DesktopSandboxProvider extends SandboxProvider {
|
|
41
|
+
screenshot(): Promise<Buffer>;
|
|
42
|
+
leftClick(x: number, y: number): Promise<void>;
|
|
43
|
+
doubleClick(x: number, y: number): Promise<void>;
|
|
44
|
+
rightClick(x: number, y: number): Promise<void>;
|
|
45
|
+
middleClick(x: number, y: number): Promise<void>;
|
|
46
|
+
moveMouse(x: number, y: number): Promise<void>;
|
|
47
|
+
write(text: string): Promise<void>;
|
|
48
|
+
press(key: string): Promise<void>;
|
|
49
|
+
scroll(direction: "up" | "down", ticks: number): Promise<void>;
|
|
50
|
+
drag(from: [number, number], to: [number, number]): Promise<void>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow types — the context and function signature for authoring workflows.
|
|
3
|
+
*
|
|
4
|
+
* A workflow is `async (ctx, sandbox) => T`. Two positional args by design:
|
|
5
|
+
*
|
|
6
|
+
* - `ctx` carries facts + observability about THIS run (id, input,
|
|
7
|
+
* setMetadata, step). Metadata bag.
|
|
8
|
+
* - `sandbox` is a capability handed to you by the engine for doing work
|
|
9
|
+
* (exec commands, write files). Pass it to `runAgent({ sandbox, ... })`
|
|
10
|
+
* and to any helper that takes a SandboxProvider (git utilities, file
|
|
11
|
+
* writers). Constructed once per run; reuse it.
|
|
12
|
+
*
|
|
13
|
+
* LLM agent loops live in `runAgent(opts)` (sdk/src/agent/run-agent.ts).
|
|
14
|
+
* Invoking other workflows uses `AgentComposeClient.invoke[AndWait](...)`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { SandboxNetworkPolicy } from "../sandbox.js";
|
|
18
|
+
import type { SandboxProvider } from "./sandbox.js";
|
|
19
|
+
|
|
20
|
+
/** The identity of this workflow run. */
|
|
21
|
+
export interface WorkflowRun {
|
|
22
|
+
id: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Turn/iteration budget for `runAgent(opts)`. Re-exported here so authors
|
|
26
|
+
* can type per-invoke budget overrides they pass as workflow input. */
|
|
27
|
+
export interface AgentBudget {
|
|
28
|
+
turnsPerIteration: number;
|
|
29
|
+
maxIterations: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Context passed to a workflow function — facts + observability for this run. */
|
|
33
|
+
export interface WorkflowCtx {
|
|
34
|
+
run: WorkflowRun;
|
|
35
|
+
input?: Record<string, unknown>;
|
|
36
|
+
/** Persist key-value metadata on the run record (e.g. prUrl, planUrl). */
|
|
37
|
+
setMetadata: (data: Record<string, unknown>) => Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Wrap a named step for observability. Emits step_started / step_completed /
|
|
40
|
+
* step_failed lifecycle events with duration. Use for long phases you want
|
|
41
|
+
* visible on the run's timeline (setup, external API calls, submit).
|
|
42
|
+
*/
|
|
43
|
+
step<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A workflow is `async (ctx, sandbox) => T`. */
|
|
47
|
+
export type WorkflowFn<T = unknown> = (ctx: WorkflowCtx, sandbox: SandboxProvider) => Promise<T>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Declare a workflow with server-side metadata.
|
|
51
|
+
* Use `export default defineWorkflow({ run, networkPolicy, ... })` to attach
|
|
52
|
+
* a runner network policy so the server brokers credentials for the workflow
|
|
53
|
+
* sandbox.
|
|
54
|
+
*
|
|
55
|
+
* Without defineWorkflow, a plain `export default async (ctx) => {...}` still
|
|
56
|
+
* works — the workflow just runs with secrets passed directly in env.
|
|
57
|
+
*/
|
|
58
|
+
export interface WorkflowDefinition<T = unknown> {
|
|
59
|
+
run: WorkflowFn<T>;
|
|
60
|
+
/**
|
|
61
|
+
* Reference to a snapshot the runner should boot from at run start.
|
|
62
|
+
* Accepts a run UUID, a workflow name, or `name@version` — any workflow
|
|
63
|
+
* registered with `--build` produces a snapshot you can name here. The
|
|
64
|
+
* runner VM starts in that pre-configured state. Per-invocation
|
|
65
|
+
* `invoke({ snapshot })` overrides this default.
|
|
66
|
+
*/
|
|
67
|
+
snapshot?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Capture a snapshot of the sandbox on successful /complete. Snapshots
|
|
70
|
+
* are long-lived (never auto-expire) — customers list + delete them
|
|
71
|
+
* explicitly via `agentc snapshot list/delete`. Per-invocation
|
|
72
|
+
* `invoke({ saveSnapshot })` overrides this default.
|
|
73
|
+
* `defineSandboxEnvironment` sugar sets this to true by default.
|
|
74
|
+
*/
|
|
75
|
+
saveSnapshot?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Outbound network policy for the runner sandbox.
|
|
78
|
+
* Use "*": [] to allow all traffic while still injecting headers for specific domains.
|
|
79
|
+
* Secret values are referenced as $VARIABLE and resolved from GCP at dispatch time.
|
|
80
|
+
* Vercel only — E2B ignores.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* networkPolicy: {
|
|
84
|
+
* allow: {
|
|
85
|
+
* "*": [],
|
|
86
|
+
* "api.github.com": [{ transform: [{ headers: { "Authorization": "basic:$GITHUB_TOKEN" } }] }],
|
|
87
|
+
* }
|
|
88
|
+
* }
|
|
89
|
+
*/
|
|
90
|
+
networkPolicy?: SandboxNetworkPolicy;
|
|
91
|
+
/**
|
|
92
|
+
* Optional placeholder env var values for secrets referenced in the network policy.
|
|
93
|
+
* By default, brokered secrets are removed from the runner env entirely — the real
|
|
94
|
+
* values are only ever present inside the Vercel firewall config, never in the VM.
|
|
95
|
+
* Only set this if a tool or SDK validates the env var format on startup before
|
|
96
|
+
* making any requests (e.g. some CLIs check that ANTHROPIC_API_KEY looks like a
|
|
97
|
+
* real key). The placeholder is a syntactically valid but non-functional stand-in.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* placeholders: {
|
|
101
|
+
* ANTHROPIC_API_KEY: `sk-ant-api03-${"a".repeat(95)}`,
|
|
102
|
+
* }
|
|
103
|
+
*/
|
|
104
|
+
placeholders?: Record<string, string>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Attach server-side metadata to a workflow function.
|
|
109
|
+
* The returned function is a valid WorkflowFn with metadata fields attached
|
|
110
|
+
* for the bundler / registration layer to read.
|
|
111
|
+
*/
|
|
112
|
+
export function defineWorkflow<T = unknown>(
|
|
113
|
+
def: WorkflowDefinition<T>,
|
|
114
|
+
): WorkflowFn<T> & Pick<WorkflowDefinition, "networkPolicy" | "placeholders" | "snapshot" | "saveSnapshot"> {
|
|
115
|
+
return Object.assign(def.run, {
|
|
116
|
+
networkPolicy: def.networkPolicy,
|
|
117
|
+
placeholders: def.placeholders,
|
|
118
|
+
snapshot: def.snapshot,
|
|
119
|
+
saveSnapshot: def.saveSnapshot,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Observability-only lifecycle hooks — passed to the workflow engine, not workflow authors. */
|
|
124
|
+
export interface WorkflowHooks {
|
|
125
|
+
onStepStart?: (step: string) => void;
|
|
126
|
+
onStepComplete?: (step: string, durationMs: number) => void;
|
|
127
|
+
onStepFail?: (step: string, durationMs: number, reason: string) => void;
|
|
128
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle a workflow for registration.
|
|
3
|
+
*
|
|
4
|
+
* Shared between the CLI (agentc register) and tests. Uses `Bun.build`
|
|
5
|
+
* to bundle TypeScript sources into a self-contained ESM module.
|
|
6
|
+
*
|
|
7
|
+
* Post de-broker there are no agent bundles to produce — workflows embed
|
|
8
|
+
* their agent loops inline via `runAgent(...)`, and workflows invoke other
|
|
9
|
+
* workflows via the public SDK client.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { importSourceModule } from "./source-loader.js";
|
|
13
|
+
import type { WorkflowFn, WorkflowDefinition } from "../types/workflow.js";
|
|
14
|
+
import type { SandboxNetworkPolicy } from "../sandbox.js";
|
|
15
|
+
|
|
16
|
+
export interface BundledWorkflow {
|
|
17
|
+
source: string;
|
|
18
|
+
networkPolicy?: SandboxNetworkPolicy;
|
|
19
|
+
placeholders?: Record<string, string>;
|
|
20
|
+
/** Run UUID, workflow name, or `name@version` referencing the snapshot
|
|
21
|
+
* this workflow's runner boots from, if declared via the `snapshot`
|
|
22
|
+
* field on `defineWorkflow`. */
|
|
23
|
+
snapshot?: string;
|
|
24
|
+
/** Default capture-on-success flag from the workflow definition. */
|
|
25
|
+
saveSnapshot?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function bundle(path: string, label: string): Promise<string> {
|
|
29
|
+
// Use globalThis to access Bun without a compile-time dependency on @types/bun.
|
|
30
|
+
const bun = globalThis as unknown as { Bun?: { build(opts: { entrypoints: string[]; format: string; target: string }): Promise<{ success: boolean; outputs: { text(): Promise<string> }[]; logs: { message: string }[] }> } };
|
|
31
|
+
if (!bun.Bun?.build) throw new Error("bundleWorkflow requires the Bun runtime (Bun.build)");
|
|
32
|
+
const result = await bun.Bun.build({ entrypoints: [path], format: "esm", target: "bun" });
|
|
33
|
+
if (!result.success) {
|
|
34
|
+
throw new Error(`Failed to bundle ${label}:\n${result.logs.map((l) => l.message).join("\n")}`);
|
|
35
|
+
}
|
|
36
|
+
return result.outputs[0].text();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Dynamically import a bundled source in-process and extract fields from its
|
|
40
|
+
* default export. Safe for CLI/test contexts. Returns `undefined` on
|
|
41
|
+
* resolver failure (missing deps) so callers fall through. */
|
|
42
|
+
async function extractFromBundle<T>(
|
|
43
|
+
source: string,
|
|
44
|
+
label: string,
|
|
45
|
+
pick: (defaultExport: unknown) => T | undefined,
|
|
46
|
+
): Promise<T | undefined> {
|
|
47
|
+
const tmpPath = `/tmp/agentc-${label}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`;
|
|
48
|
+
try {
|
|
49
|
+
const loaded = await importSourceModule<Record<string, unknown>>(source, tmpPath);
|
|
50
|
+
try { return pick(loaded.mod.default); }
|
|
51
|
+
finally { await loaded.cleanup(); }
|
|
52
|
+
} catch { return undefined; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Bundle a workflow from source. */
|
|
56
|
+
export async function bundleWorkflow(
|
|
57
|
+
workflowPath: string,
|
|
58
|
+
overrides?: { networkPolicy?: SandboxNetworkPolicy; placeholders?: Record<string, string> },
|
|
59
|
+
): Promise<BundledWorkflow> {
|
|
60
|
+
const source = await bundle(workflowPath, `workflow "${workflowPath}"`);
|
|
61
|
+
|
|
62
|
+
type Wf = WorkflowFn & Pick<WorkflowDefinition, "networkPolicy" | "placeholders" | "snapshot" | "saveSnapshot">;
|
|
63
|
+
const wfBits = await extractFromBundle<Pick<Wf, "networkPolicy" | "placeholders" | "snapshot" | "saveSnapshot">>(
|
|
64
|
+
source, "workflow",
|
|
65
|
+
(wf) => wf ? {
|
|
66
|
+
networkPolicy: (wf as Wf).networkPolicy,
|
|
67
|
+
placeholders: (wf as Wf).placeholders,
|
|
68
|
+
snapshot: (wf as Wf).snapshot,
|
|
69
|
+
saveSnapshot: (wf as Wf).saveSnapshot,
|
|
70
|
+
} : undefined,
|
|
71
|
+
);
|
|
72
|
+
const { networkPolicy, placeholders, snapshot, saveSnapshot } = wfBits ?? {};
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
source,
|
|
76
|
+
networkPolicy: overrides?.networkPolicy ?? networkPolicy,
|
|
77
|
+
placeholders: overrides?.placeholders ?? placeholders,
|
|
78
|
+
...(snapshot !== undefined ? { snapshot } : {}),
|
|
79
|
+
...(saveSnapshot !== undefined ? { saveSnapshot } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { AgentStatus } from "../types/protocol.js";
|
|
3
|
+
|
|
4
|
+
/** Zod schema for the AgentStatus block agents emit to signal iteration completion. */
|
|
5
|
+
export const AgentStatusSchema = z.object({
|
|
6
|
+
summary: z.string(),
|
|
7
|
+
completed: z.array(z.string()),
|
|
8
|
+
blockers: z.array(z.string()),
|
|
9
|
+
exit_signal: z.boolean(),
|
|
10
|
+
}) satisfies z.ZodType<AgentStatus>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { dirname } from "path";
|
|
2
|
+
import { writeFile, unlink, mkdir } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
export const TMP_DIR = process.env.TMP_DIR ?? "/tmp/agent-compose";
|
|
5
|
+
export const LATEST_VERSION = "$LATEST";
|
|
6
|
+
|
|
7
|
+
export async function importSourceModule<T>(
|
|
8
|
+
source: string,
|
|
9
|
+
tmpPath: string,
|
|
10
|
+
): Promise<{ mod: T; cleanup: () => Promise<void> }> {
|
|
11
|
+
await mkdir(dirname(tmpPath), { recursive: true });
|
|
12
|
+
await writeFile(tmpPath, source, "utf8");
|
|
13
|
+
const mod = await import(tmpPath) as T;
|
|
14
|
+
const cleanup = () => unlink(tmpPath).catch(() => {});
|
|
15
|
+
return { mod, cleanup };
|
|
16
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow engine — runs a workflow function with a minimal ctx.
|
|
3
|
+
*
|
|
4
|
+
* Post de-broker there is no agent-spawning primitive here. Workflows that
|
|
5
|
+
* want to embed an LLM agent call `runAgent(opts)` directly from
|
|
6
|
+
* `@agent-compose/sdk`. Workflows that want to invoke OTHER workflows use
|
|
7
|
+
* `AgentComposeClient.invoke[AndWait](...)`. The engine's only job now is:
|
|
8
|
+
*
|
|
9
|
+
* - build the `ctx` the workflow function receives
|
|
10
|
+
* - funnel `onStepStart / onStepComplete / onStepFail` hooks
|
|
11
|
+
* - classify errors into `WorkflowError` vs `EngineError` for the runner
|
|
12
|
+
* harness to emit on /fail
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { WorkflowHooks, WorkflowCtx, WorkflowFn } from "../types/workflow.js";
|
|
16
|
+
import { makeLocalSandboxProvider } from "../sandbox.js";
|
|
17
|
+
import { formatError } from "../utils/errors.js";
|
|
18
|
+
|
|
19
|
+
// ── Error classification ─────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Thrown from `runWorkflow` when the authored workflow threw.
|
|
23
|
+
* User-level; not a bug in the platform.
|
|
24
|
+
*/
|
|
25
|
+
export class WorkflowError extends Error {
|
|
26
|
+
readonly kind = "workflow" as const;
|
|
27
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
28
|
+
super(message, options);
|
|
29
|
+
this.name = "WorkflowError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Thrown when the runner harness itself cannot run (or finish) a workflow —
|
|
35
|
+
* bundle missing, max-runtime exceeded, sandbox provider died, secret
|
|
36
|
+
* resolution broken. Platform-level; investigate us, not the user's code.
|
|
37
|
+
*
|
|
38
|
+
* `subsystem` narrows where the failure originated so monitors + dashboards
|
|
39
|
+
* can break down alerts without string-matching on messages.
|
|
40
|
+
*/
|
|
41
|
+
export type EngineSubsystem = "bundle" | "timeout" | "sandbox" | "dispatch" | "unknown";
|
|
42
|
+
export class EngineError extends Error {
|
|
43
|
+
readonly kind = "engine" as const;
|
|
44
|
+
readonly subsystem: EngineSubsystem;
|
|
45
|
+
constructor(message: string, subsystem: EngineSubsystem = "unknown", options?: ErrorOptions) {
|
|
46
|
+
super(message, options);
|
|
47
|
+
this.name = "EngineError";
|
|
48
|
+
this.subsystem = subsystem;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Classify any thrown value into the wire-level `kind` expected by `/fail`. */
|
|
53
|
+
export function classifyError(err: unknown): "workflow" | "engine" {
|
|
54
|
+
if (err instanceof WorkflowError) return "workflow";
|
|
55
|
+
if (err instanceof EngineError) return "engine";
|
|
56
|
+
// Anything uncategorised defaults to engine — safer to over-alert on our
|
|
57
|
+
// side than miss a real infra issue that escaped the typed catch.
|
|
58
|
+
return "engine";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parseNameVersion(ref: string): { name: string; version: string | undefined } {
|
|
62
|
+
const atIdx = ref.lastIndexOf("@");
|
|
63
|
+
return atIdx > 0 ? { name: ref.slice(0, atIdx), version: ref.slice(atIdx + 1) } : { name: ref, version: undefined };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export interface WorkflowResult<T = unknown> {
|
|
69
|
+
/** The value returned by the workflow function. `null` for void workflows. */
|
|
70
|
+
response: T | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function runWorkflow<T = unknown>(
|
|
74
|
+
wf: WorkflowFn<T>,
|
|
75
|
+
ctx: Pick<WorkflowCtx, "run" | "input">,
|
|
76
|
+
opts?: { hooks?: WorkflowHooks; setMetadata?: (data: Record<string, unknown>) => Promise<void> },
|
|
77
|
+
): Promise<WorkflowResult<T>> {
|
|
78
|
+
const hooks = opts?.hooks ?? {};
|
|
79
|
+
const setMetadata = opts?.setMetadata ?? (async () => {});
|
|
80
|
+
// Single SandboxProvider for the whole run — handed to the workflow as
|
|
81
|
+
// its second positional arg. Stateless wrapper over child_process.spawn
|
|
82
|
+
// + fs.writeFile, so reuse is purely a matter of shape consistency.
|
|
83
|
+
const sandbox = makeLocalSandboxProvider();
|
|
84
|
+
|
|
85
|
+
let workflowErr: unknown;
|
|
86
|
+
let wfOutput: unknown;
|
|
87
|
+
try {
|
|
88
|
+
wfOutput = await wf(
|
|
89
|
+
{
|
|
90
|
+
...ctx,
|
|
91
|
+
setMetadata,
|
|
92
|
+
step: async <S>(name: string, fn: () => Promise<S>): Promise<S> => {
|
|
93
|
+
const startedAt = Date.now();
|
|
94
|
+
hooks.onStepStart?.(name);
|
|
95
|
+
try { const r = await fn(); hooks.onStepComplete?.(name, Date.now() - startedAt); return r; }
|
|
96
|
+
catch (err) { hooks.onStepFail?.(name, Date.now() - startedAt, formatError(err)); throw err; }
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
sandbox,
|
|
100
|
+
);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
workflowErr = err;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (workflowErr !== undefined) {
|
|
106
|
+
if (workflowErr instanceof WorkflowError) throw workflowErr;
|
|
107
|
+
throw new WorkflowError(formatError(workflowErr), { cause: workflowErr });
|
|
108
|
+
}
|
|
109
|
+
return { response: (wfOutput ?? null) as T | null };
|
|
110
|
+
}
|