@agent-compose/sdk 0.2.3 → 0.2.5
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 +145 -33
- package/dist/agent/agent-loop.d.ts +83 -5
- package/dist/agent/run-agent.d.ts +34 -9
- package/dist/client.d.ts +247 -99
- package/dist/index.d.ts +26 -11
- package/dist/index.js +1967 -745
- package/dist/processors/builtins.d.ts +35 -0
- package/dist/processors/index.d.ts +4 -0
- package/dist/processors/processor.d.ts +91 -0
- package/dist/processors/processor.test.d.ts +1 -0
- package/dist/processors/runner.d.ts +19 -0
- package/dist/request-context/index.d.ts +2 -0
- package/dist/request-context/request-context.d.ts +159 -0
- package/dist/request-context/request-context.test.d.ts +1 -0
- package/dist/runtimes/claude.d.ts +27 -50
- package/dist/runtimes/openai-desktop.js +1918 -741
- package/dist/runtimes/vercel.d.ts +34 -0
- package/dist/runtimes/vercel.js +474 -0
- package/dist/sandbox.d.ts +29 -25
- package/dist/step-invocation/__tests__/invoker.test.d.ts +1 -0
- package/dist/step-invocation/__tests__/protocol.test.d.ts +1 -0
- package/dist/step-invocation/__tests__/server.test.d.ts +1 -0
- package/dist/step-invocation/index.d.ts +25 -0
- package/dist/step-invocation/invoker.d.ts +65 -0
- package/dist/step-invocation/protocol.d.ts +44 -0
- package/dist/step-invocation/server.d.ts +63 -0
- package/dist/step-invocation/types.d.ts +72 -0
- package/dist/tools/coding.d.ts +49 -0
- package/dist/tools/coding.test.d.ts +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/types/events.d.ts +36 -0
- package/dist/types/execution-context.d.ts +22 -0
- package/dist/types/runtime.d.ts +32 -0
- package/dist/types/sandbox-environment.d.ts +5 -2
- package/dist/types/sandbox.d.ts +14 -12
- package/dist/types/workflow-metadata.d.ts +51 -0
- package/dist/types/workflow-plan.d.ts +19 -0
- package/dist/types/workflow.d.ts +57 -17
- package/dist/utils/bundler.d.ts +62 -3
- package/dist/workflow-steps/__tests__/observability.test.d.ts +1 -0
- package/dist/workflow-steps/index.d.ts +10 -0
- package/dist/workflow-steps/observability.d.ts +58 -0
- package/dist/workflow-steps/runner.d.ts +96 -0
- package/dist/workflow-steps/step.d.ts +25 -0
- package/dist/workflow-steps/types.d.ts +135 -0
- package/dist/workflow-steps/workflow-steps.test.d.ts +1 -0
- package/dist/workflow-steps/workflow.d.ts +50 -0
- package/dist/workflows/engine.d.ts +27 -13
- package/dist/workflows/invoke-child.d.ts +10 -0
- package/package.json +25 -15
- package/src/agent/agent-loop.ts +197 -26
- package/src/agent/run-agent.ts +40 -15
- package/src/client.ts +326 -76
- package/src/index.ts +124 -10
- package/src/processors/builtins.ts +72 -0
- package/src/processors/index.ts +15 -0
- package/src/processors/processor.ts +103 -0
- package/src/processors/runner.ts +42 -0
- package/src/request-context/index.ts +17 -0
- package/src/request-context/request-context.ts +302 -0
- package/src/runtimes/claude.ts +123 -254
- package/src/runtimes/vercel.ts +180 -0
- package/src/sandbox.ts +53 -21
- package/src/step-invocation/index.ts +33 -0
- package/src/step-invocation/invoker.ts +204 -0
- package/src/step-invocation/protocol.ts +57 -0
- package/src/step-invocation/server.ts +184 -0
- package/src/step-invocation/types.ts +70 -0
- package/src/tools/coding.ts +126 -0
- package/src/tools/index.ts +8 -0
- package/src/types/events.ts +40 -0
- package/src/types/execution-context.ts +30 -0
- package/src/types/runtime.ts +24 -0
- package/src/types/sandbox-environment.ts +7 -5
- package/src/types/sandbox.ts +16 -12
- package/src/types/workflow-metadata.ts +84 -0
- package/src/types/workflow-plan.ts +24 -0
- package/src/types/workflow.ts +139 -25
- package/src/utils/bundler.ts +206 -18
- package/src/utils/source-loader.ts +2 -2
- package/src/workflow-steps/index.ts +30 -0
- package/src/workflow-steps/observability.ts +103 -0
- package/src/workflow-steps/runner.ts +244 -0
- package/src/workflow-steps/step.ts +38 -0
- package/src/workflow-steps/types.ts +134 -0
- package/src/workflow-steps/workflow.ts +95 -0
- package/src/workflows/engine.ts +69 -40
- package/src/workflows/invoke-child.ts +29 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RequestContext — one typed bag carrying tenant identity + freeform
|
|
3
|
+
* per-run state across the boundary between server, dispatch, runner,
|
|
4
|
+
* workflow body, agent loop, and (future) processors.
|
|
5
|
+
*
|
|
6
|
+
* Two halves:
|
|
7
|
+
*
|
|
8
|
+
* - **Reserved keys** are tenant identity + run identity. They are
|
|
9
|
+
* authoritative once the server's auth middleware sets them and
|
|
10
|
+
* CANNOT be mutated downstream — `set()` on a reserved key throws.
|
|
11
|
+
* This is the same multi-tenancy hijack defence we apply at the RLS
|
|
12
|
+
* layer: the runner never gets to claim a different `teamId`.
|
|
13
|
+
*
|
|
14
|
+
* - **Freeform user namespace** is plain `set/get/has/delete` for any
|
|
15
|
+
* JSON-serialisable values the caller wants to thread through. Used
|
|
16
|
+
* by application code (workflows, processors) for their own state.
|
|
17
|
+
*
|
|
18
|
+
* Reserved-key authority:
|
|
19
|
+
*
|
|
20
|
+
* ac__teamId server (auth middleware)
|
|
21
|
+
* ac__runId server (dispatch)
|
|
22
|
+
* ac__workflowId server (dispatch)
|
|
23
|
+
* ac__factoryId server (auth) -- nullable
|
|
24
|
+
* ac__apiKeyScopes server (auth)
|
|
25
|
+
* ac__parentRunId server (dispatch) -- nullable
|
|
26
|
+
* ac__abortSignal runner (per-process) -- not serialised
|
|
27
|
+
*
|
|
28
|
+
* Cross-process flow (server → runner):
|
|
29
|
+
*
|
|
30
|
+
* 1. Server constructs `RequestContext` at the auth boundary using the
|
|
31
|
+
* reserved key authorities listed above.
|
|
32
|
+
* 2. `serialise()` → JSON shape `{ reserved, user }` (abortSignal stripped).
|
|
33
|
+
* 3. Dispatch hands the JSON to the runner sandbox via env (or future
|
|
34
|
+
* transport).
|
|
35
|
+
* 4. Runner calls `RequestContext.deserialise(json)` and attaches its
|
|
36
|
+
* own `AbortSignal`. Reserved keys are re-frozen on materialisation.
|
|
37
|
+
*
|
|
38
|
+
* `invokeChild` propagation: only reserved keys flow to the child; freeform
|
|
39
|
+
* is per-run by design (cross-run state must be explicit via `input`).
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/** Reserved key namespace prefix — anything starting with this is platform-owned. */
|
|
43
|
+
export const AC_RESERVED_PREFIX = "ac__";
|
|
44
|
+
|
|
45
|
+
export const AC_TEAM_ID = "ac__teamId" as const;
|
|
46
|
+
export const AC_RUN_ID = "ac__runId" as const;
|
|
47
|
+
export const AC_WORKFLOW_ID = "ac__workflowId" as const;
|
|
48
|
+
export const AC_FACTORY_ID = "ac__factoryId" as const;
|
|
49
|
+
export const AC_API_KEY_SCOPES = "ac__apiKeyScopes" as const;
|
|
50
|
+
export const AC_PARENT_RUN_ID = "ac__parentRunId" as const;
|
|
51
|
+
export const AC_ABORT_SIGNAL = "ac__abortSignal" as const;
|
|
52
|
+
|
|
53
|
+
/** Reserved keys, as a runtime set for guardrails. Includes all keys above. */
|
|
54
|
+
const RESERVED_KEYS: ReadonlySet<string> = new Set([
|
|
55
|
+
AC_TEAM_ID,
|
|
56
|
+
AC_RUN_ID,
|
|
57
|
+
AC_WORKFLOW_ID,
|
|
58
|
+
AC_FACTORY_ID,
|
|
59
|
+
AC_API_KEY_SCOPES,
|
|
60
|
+
AC_PARENT_RUN_ID,
|
|
61
|
+
AC_ABORT_SIGNAL,
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
/** Reserved keys that cross the dispatch boundary as JSON. Excludes `abortSignal`. */
|
|
65
|
+
const SERIALISABLE_RESERVED_KEYS: ReadonlySet<string> = new Set([
|
|
66
|
+
AC_TEAM_ID,
|
|
67
|
+
AC_RUN_ID,
|
|
68
|
+
AC_WORKFLOW_ID,
|
|
69
|
+
AC_FACTORY_ID,
|
|
70
|
+
AC_API_KEY_SCOPES,
|
|
71
|
+
AC_PARENT_RUN_ID,
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Required reserved values to construct a context. `factoryId` and
|
|
76
|
+
* `parentRunId` are nullable (factory-scoped key absent → null; root run → null).
|
|
77
|
+
* `abortSignal` is optional and runner-set; not serialised.
|
|
78
|
+
*/
|
|
79
|
+
export interface RequestContextReserved {
|
|
80
|
+
teamId: string;
|
|
81
|
+
runId: string;
|
|
82
|
+
workflowId: string;
|
|
83
|
+
factoryId: string | null;
|
|
84
|
+
apiKeyScopes: readonly string[];
|
|
85
|
+
parentRunId: string | null;
|
|
86
|
+
abortSignal?: AbortSignal;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Wire shape for crossing the dispatch boundary. `abortSignal` is stripped. */
|
|
90
|
+
export interface RequestContextWire {
|
|
91
|
+
reserved: {
|
|
92
|
+
teamId: string;
|
|
93
|
+
runId: string;
|
|
94
|
+
workflowId: string;
|
|
95
|
+
factoryId: string | null;
|
|
96
|
+
apiKeyScopes: readonly string[];
|
|
97
|
+
parentRunId: string | null;
|
|
98
|
+
};
|
|
99
|
+
user: Record<string, unknown>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Thrown when a caller tries to `set` or `delete` a reserved key downstream. */
|
|
103
|
+
export class ReservedKeyError extends Error {
|
|
104
|
+
readonly key: string;
|
|
105
|
+
constructor(key: string) {
|
|
106
|
+
super(`request-context: "${key}" is reserved and cannot be mutated downstream`);
|
|
107
|
+
this.name = "ReservedKeyError";
|
|
108
|
+
this.key = key;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Thrown when freeform `set` receives a non-JSON-serialisable value. */
|
|
113
|
+
export class NonSerialisableValueError extends Error {
|
|
114
|
+
constructor(key: string, cause: unknown) {
|
|
115
|
+
super(`request-context: value for "${key}" is not JSON-serialisable`, { cause });
|
|
116
|
+
this.name = "NonSerialisableValueError";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function assertJsonSerialisable(key: string, value: unknown, seen = new WeakSet<object>()): void {
|
|
121
|
+
if (value === null) return;
|
|
122
|
+
|
|
123
|
+
const type = typeof value;
|
|
124
|
+
if (type === "string" || type === "boolean") return;
|
|
125
|
+
if (type === "number") {
|
|
126
|
+
if (Number.isFinite(value)) return;
|
|
127
|
+
throw new NonSerialisableValueError(key, new TypeError("non-finite numbers are not JSON-serialisable"));
|
|
128
|
+
}
|
|
129
|
+
if (type !== "object") {
|
|
130
|
+
throw new NonSerialisableValueError(key, new TypeError(`${type} is not JSON-serialisable`));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const objectValue = value as object;
|
|
134
|
+
|
|
135
|
+
if (seen.has(objectValue)) {
|
|
136
|
+
throw new NonSerialisableValueError(key, new TypeError("cyclic values are not JSON-serialisable"));
|
|
137
|
+
}
|
|
138
|
+
seen.add(objectValue);
|
|
139
|
+
|
|
140
|
+
if (Array.isArray(value)) {
|
|
141
|
+
for (const item of value) assertJsonSerialisable(key, item, seen);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (Object.getPrototypeOf(objectValue) !== Object.prototype) {
|
|
146
|
+
throw new NonSerialisableValueError(key, new TypeError("only plain objects are JSON-serialisable"));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const item of Object.values(objectValue)) assertJsonSerialisable(key, item, seen);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Per-run request context. Construct via `RequestContext.fromReserved(...)`
|
|
154
|
+
* at the auth boundary; downstream code receives an instance and calls
|
|
155
|
+
* `.get` / `.set` on it.
|
|
156
|
+
*
|
|
157
|
+
* Type parameter `U` lets callers narrow the freeform user namespace shape
|
|
158
|
+
* for `.get`/`.set` typing — defaults to `unknown` (truly freeform).
|
|
159
|
+
*/
|
|
160
|
+
export class RequestContext<U extends Record<string, unknown> = Record<string, unknown>> {
|
|
161
|
+
private readonly reserved: RequestContextReserved;
|
|
162
|
+
private readonly user: Map<string, unknown>;
|
|
163
|
+
|
|
164
|
+
private constructor(reserved: RequestContextReserved, user: Map<string, unknown>) {
|
|
165
|
+
this.reserved = Object.freeze({
|
|
166
|
+
...reserved,
|
|
167
|
+
apiKeyScopes: Object.freeze([...reserved.apiKeyScopes]),
|
|
168
|
+
});
|
|
169
|
+
this.user = user;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Construction ────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build a context at the auth/dispatch boundary. Callers (server auth
|
|
176
|
+
* middleware, dispatch) own the reserved values and pass them in once.
|
|
177
|
+
*/
|
|
178
|
+
static fromReserved(reserved: RequestContextReserved): RequestContext {
|
|
179
|
+
return new RequestContext(reserved, new Map());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Materialise from the wire shape produced by `serialise()`.
|
|
184
|
+
* Used by the runner after receiving the JSON via env. The runner attaches
|
|
185
|
+
* its own `AbortSignal` separately via `withAbortSignal()`.
|
|
186
|
+
*/
|
|
187
|
+
static deserialise(wire: RequestContextWire): RequestContext {
|
|
188
|
+
const ctx = new RequestContext(
|
|
189
|
+
{ ...wire.reserved, abortSignal: undefined },
|
|
190
|
+
new Map(Object.entries(wire.user)),
|
|
191
|
+
);
|
|
192
|
+
return ctx;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Return a new instance with the given abort signal attached. Used by the
|
|
197
|
+
* runner once it owns a per-process signal.
|
|
198
|
+
*/
|
|
199
|
+
withAbortSignal(signal: AbortSignal): RequestContext {
|
|
200
|
+
return new RequestContext(
|
|
201
|
+
{ ...this.reserved, abortSignal: signal },
|
|
202
|
+
new Map(this.user),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Reserved-key accessors ──────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
get teamId(): string { return this.reserved.teamId; }
|
|
209
|
+
get runId(): string { return this.reserved.runId; }
|
|
210
|
+
get workflowId(): string { return this.reserved.workflowId; }
|
|
211
|
+
get factoryId(): string | null { return this.reserved.factoryId; }
|
|
212
|
+
get apiKeyScopes(): readonly string[] { return this.reserved.apiKeyScopes; }
|
|
213
|
+
get parentRunId(): string | null { return this.reserved.parentRunId; }
|
|
214
|
+
get abortSignal(): AbortSignal | undefined { return this.reserved.abortSignal; }
|
|
215
|
+
|
|
216
|
+
/** True if the calling key has the given scope. Read-only convenience. */
|
|
217
|
+
hasScope(scope: string): boolean {
|
|
218
|
+
return this.reserved.apiKeyScopes.includes(scope);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Freeform user namespace ─────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Set a freeform key. Throws `ReservedKeyError` if `key` collides with the
|
|
225
|
+
* reserved namespace, and `NonSerialisableValueError` if `value` cannot be
|
|
226
|
+
* JSON-serialised (so cross-process behaviour matches in-process behaviour).
|
|
227
|
+
*/
|
|
228
|
+
set<K extends keyof U & string>(key: K, value: U[K]): void;
|
|
229
|
+
set(key: string, value: unknown): void;
|
|
230
|
+
set(key: string, value: unknown): void {
|
|
231
|
+
if (RESERVED_KEYS.has(key) || key.startsWith(AC_RESERVED_PREFIX)) {
|
|
232
|
+
throw new ReservedKeyError(key);
|
|
233
|
+
}
|
|
234
|
+
assertJsonSerialisable(key, value);
|
|
235
|
+
this.user.set(key, value);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
get<K extends keyof U & string>(key: K): U[K] | undefined;
|
|
239
|
+
get(key: string): unknown;
|
|
240
|
+
get(key: string): unknown {
|
|
241
|
+
return this.user.get(key);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
has(key: string): boolean {
|
|
245
|
+
return this.user.has(key);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
delete(key: string): boolean {
|
|
249
|
+
if (RESERVED_KEYS.has(key) || key.startsWith(AC_RESERVED_PREFIX)) {
|
|
250
|
+
throw new ReservedKeyError(key);
|
|
251
|
+
}
|
|
252
|
+
return this.user.delete(key);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Iterate freeform entries only. Reserved keys are not iterable. */
|
|
256
|
+
entries(): IterableIterator<[string, unknown]> {
|
|
257
|
+
return this.user.entries();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Serialisation ───────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Produce the wire shape for crossing the dispatch boundary. `abortSignal`
|
|
264
|
+
* is stripped; freeform values are passed through (they were validated as
|
|
265
|
+
* JSON-serialisable on `set`).
|
|
266
|
+
*/
|
|
267
|
+
serialise(): RequestContextWire {
|
|
268
|
+
return {
|
|
269
|
+
reserved: {
|
|
270
|
+
teamId: this.reserved.teamId,
|
|
271
|
+
runId: this.reserved.runId,
|
|
272
|
+
workflowId: this.reserved.workflowId,
|
|
273
|
+
factoryId: this.reserved.factoryId,
|
|
274
|
+
apiKeyScopes: [...this.reserved.apiKeyScopes],
|
|
275
|
+
parentRunId: this.reserved.parentRunId,
|
|
276
|
+
},
|
|
277
|
+
user: Object.fromEntries(this.user),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Build a child context for an `invokeChild` call: reserved keys propagate
|
|
283
|
+
* (with `parentRunId` rewritten to the parent's `runId` and `runId`/`workflowId`
|
|
284
|
+
* supplied by the caller for the child run); freeform DOES NOT propagate.
|
|
285
|
+
*
|
|
286
|
+
* Used server-side when dispatch resolves a child invocation; the freeform
|
|
287
|
+
* namespace is intentionally per-run to keep cross-run state explicit.
|
|
288
|
+
*/
|
|
289
|
+
forChild(opts: { runId: string; workflowId: string }): RequestContext {
|
|
290
|
+
return RequestContext.fromReserved({
|
|
291
|
+
teamId: this.reserved.teamId,
|
|
292
|
+
runId: opts.runId,
|
|
293
|
+
workflowId: opts.workflowId,
|
|
294
|
+
factoryId: this.reserved.factoryId,
|
|
295
|
+
apiKeyScopes: this.reserved.apiKeyScopes,
|
|
296
|
+
parentRunId: this.reserved.runId,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Re-export reserved key set for callers that need to filter env or headers. */
|
|
302
|
+
export { SERIALISABLE_RESERVED_KEYS, RESERVED_KEYS };
|