@agent-compose/sdk 0.2.3 → 0.2.4

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.
Files changed (88) hide show
  1. package/README.md +145 -33
  2. package/dist/agent/agent-loop.d.ts +83 -5
  3. package/dist/agent/run-agent.d.ts +34 -9
  4. package/dist/client.d.ts +247 -99
  5. package/dist/index.d.ts +26 -11
  6. package/dist/index.js +1967 -745
  7. package/dist/processors/builtins.d.ts +35 -0
  8. package/dist/processors/index.d.ts +4 -0
  9. package/dist/processors/processor.d.ts +91 -0
  10. package/dist/processors/processor.test.d.ts +1 -0
  11. package/dist/processors/runner.d.ts +19 -0
  12. package/dist/request-context/index.d.ts +2 -0
  13. package/dist/request-context/request-context.d.ts +159 -0
  14. package/dist/request-context/request-context.test.d.ts +1 -0
  15. package/dist/runtimes/claude.d.ts +27 -50
  16. package/dist/runtimes/openai-desktop.js +1918 -741
  17. package/dist/runtimes/vercel.d.ts +34 -0
  18. package/dist/runtimes/vercel.js +474 -0
  19. package/dist/sandbox.d.ts +29 -25
  20. package/dist/step-invocation/__tests__/invoker.test.d.ts +1 -0
  21. package/dist/step-invocation/__tests__/protocol.test.d.ts +1 -0
  22. package/dist/step-invocation/__tests__/server.test.d.ts +1 -0
  23. package/dist/step-invocation/index.d.ts +25 -0
  24. package/dist/step-invocation/invoker.d.ts +65 -0
  25. package/dist/step-invocation/protocol.d.ts +44 -0
  26. package/dist/step-invocation/server.d.ts +63 -0
  27. package/dist/step-invocation/types.d.ts +72 -0
  28. package/dist/tools/coding.d.ts +49 -0
  29. package/dist/tools/coding.test.d.ts +1 -0
  30. package/dist/tools/index.d.ts +2 -0
  31. package/dist/types/events.d.ts +36 -0
  32. package/dist/types/execution-context.d.ts +22 -0
  33. package/dist/types/runtime.d.ts +32 -0
  34. package/dist/types/sandbox-environment.d.ts +5 -2
  35. package/dist/types/sandbox.d.ts +14 -12
  36. package/dist/types/workflow-metadata.d.ts +51 -0
  37. package/dist/types/workflow-plan.d.ts +19 -0
  38. package/dist/types/workflow.d.ts +57 -17
  39. package/dist/utils/bundler.d.ts +62 -3
  40. package/dist/workflow-steps/__tests__/observability.test.d.ts +1 -0
  41. package/dist/workflow-steps/index.d.ts +10 -0
  42. package/dist/workflow-steps/observability.d.ts +58 -0
  43. package/dist/workflow-steps/runner.d.ts +96 -0
  44. package/dist/workflow-steps/step.d.ts +25 -0
  45. package/dist/workflow-steps/types.d.ts +135 -0
  46. package/dist/workflow-steps/workflow-steps.test.d.ts +1 -0
  47. package/dist/workflow-steps/workflow.d.ts +50 -0
  48. package/dist/workflows/engine.d.ts +27 -13
  49. package/dist/workflows/invoke-child.d.ts +10 -0
  50. package/package.json +25 -15
  51. package/src/agent/agent-loop.ts +197 -26
  52. package/src/agent/run-agent.ts +40 -15
  53. package/src/client.ts +326 -76
  54. package/src/index.ts +124 -10
  55. package/src/processors/builtins.ts +72 -0
  56. package/src/processors/index.ts +15 -0
  57. package/src/processors/processor.ts +103 -0
  58. package/src/processors/runner.ts +42 -0
  59. package/src/request-context/index.ts +17 -0
  60. package/src/request-context/request-context.ts +302 -0
  61. package/src/runtimes/claude.ts +123 -254
  62. package/src/runtimes/vercel.ts +180 -0
  63. package/src/sandbox.ts +53 -21
  64. package/src/step-invocation/index.ts +33 -0
  65. package/src/step-invocation/invoker.ts +204 -0
  66. package/src/step-invocation/protocol.ts +57 -0
  67. package/src/step-invocation/server.ts +184 -0
  68. package/src/step-invocation/types.ts +70 -0
  69. package/src/tools/coding.ts +126 -0
  70. package/src/tools/index.ts +8 -0
  71. package/src/types/events.ts +40 -0
  72. package/src/types/execution-context.ts +30 -0
  73. package/src/types/runtime.ts +24 -0
  74. package/src/types/sandbox-environment.ts +7 -5
  75. package/src/types/sandbox.ts +16 -12
  76. package/src/types/workflow-metadata.ts +84 -0
  77. package/src/types/workflow-plan.ts +24 -0
  78. package/src/types/workflow.ts +139 -25
  79. package/src/utils/bundler.ts +198 -18
  80. package/src/utils/source-loader.ts +2 -2
  81. package/src/workflow-steps/index.ts +30 -0
  82. package/src/workflow-steps/observability.ts +103 -0
  83. package/src/workflow-steps/runner.ts +244 -0
  84. package/src/workflow-steps/step.ts +38 -0
  85. package/src/workflow-steps/types.ts +134 -0
  86. package/src/workflow-steps/workflow.ts +95 -0
  87. package/src/workflows/engine.ts +69 -40
  88. 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 };