@barnum/barnum 0.2.3 → 0.3.0

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 (56) hide show
  1. package/artifacts/linux-arm64/barnum +0 -0
  2. package/artifacts/linux-x64/barnum +0 -0
  3. package/artifacts/macos-arm64/barnum +0 -0
  4. package/artifacts/macos-x64/barnum +0 -0
  5. package/artifacts/win-x64/barnum.exe +0 -0
  6. package/cli.cjs +33 -0
  7. package/dist/all.d.ts +12 -0
  8. package/dist/all.js +8 -0
  9. package/dist/ast.d.ts +375 -0
  10. package/dist/ast.js +381 -0
  11. package/dist/bind.d.ts +62 -0
  12. package/dist/bind.js +106 -0
  13. package/dist/builtins.d.ts +257 -0
  14. package/dist/builtins.js +600 -0
  15. package/dist/chain.d.ts +2 -0
  16. package/dist/chain.js +8 -0
  17. package/dist/effect-id.d.ts +14 -0
  18. package/dist/effect-id.js +16 -0
  19. package/dist/handler.d.ts +50 -0
  20. package/dist/handler.js +146 -0
  21. package/dist/index.d.ts +8 -0
  22. package/dist/index.js +5 -0
  23. package/dist/pipe.d.ts +11 -0
  24. package/dist/pipe.js +11 -0
  25. package/dist/race.d.ts +53 -0
  26. package/dist/race.js +141 -0
  27. package/dist/recursive.d.ts +34 -0
  28. package/dist/recursive.js +53 -0
  29. package/dist/run.d.ts +7 -0
  30. package/dist/run.js +143 -0
  31. package/dist/schema.d.ts +8 -0
  32. package/dist/schema.js +95 -0
  33. package/dist/try-catch.d.ts +23 -0
  34. package/dist/try-catch.js +36 -0
  35. package/dist/worker.d.ts +11 -0
  36. package/dist/worker.js +46 -0
  37. package/package.json +40 -16
  38. package/src/all.ts +89 -0
  39. package/src/ast.ts +878 -0
  40. package/src/bind.ts +192 -0
  41. package/src/builtins.ts +804 -0
  42. package/src/chain.ts +17 -0
  43. package/src/effect-id.ts +30 -0
  44. package/src/handler.ts +279 -0
  45. package/src/index.ts +30 -0
  46. package/src/pipe.ts +93 -0
  47. package/src/race.ts +183 -0
  48. package/src/recursive.ts +112 -0
  49. package/src/run.ts +181 -0
  50. package/src/schema.ts +118 -0
  51. package/src/try-catch.ts +53 -0
  52. package/src/worker.ts +56 -0
  53. package/README.md +0 -19
  54. package/barnum-config-schema.json +0 -408
  55. package/cli.js +0 -20
  56. package/index.js +0 -23
@@ -0,0 +1,112 @@
1
+ import {
2
+ type Action,
3
+ type Pipeable,
4
+ type TypedAction,
5
+ typedAction,
6
+ branch,
7
+ } from "./ast.js";
8
+ import { all } from "./all.js";
9
+ import { chain } from "./chain.js";
10
+ import {
11
+ constant,
12
+ identity,
13
+ extractField,
14
+ extractIndex,
15
+ tag,
16
+ } from "./builtins.js";
17
+ import { allocateResumeHandlerId } from "./effect-id.js";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Types
21
+ // ---------------------------------------------------------------------------
22
+
23
+ type FunctionDef = [input: unknown, output: unknown];
24
+
25
+ type FunctionRefs<TDefs extends FunctionDef[]> = {
26
+ [K in keyof TDefs]: TypedAction<TDefs[K][0], TDefs[K][1]>;
27
+ };
28
+
29
+ /**
30
+ * Constraint for the entry-point callback return type. Only requires the
31
+ * output phantom fields — omits __in and __in_co so that actions with
32
+ * In = never (e.g. pipelines starting from a call token) are assignable.
33
+ */
34
+ type BodyResult<TOut> = Action & {
35
+ __out?: () => TOut;
36
+ __out_contra?: (output: TOut) => void;
37
+ };
38
+
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ const UNUSED_STATE: any = undefined;
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // defineRecursiveFunctions
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /**
47
+ * Define mutually recursive functions that can call each other.
48
+ *
49
+ * The type parameter is an array of [In, Out] tuples — one per function.
50
+ * TypeScript can't infer these from circular definitions, so they must be
51
+ * explicit.
52
+ *
53
+ * Returns a curried combinator: the first callback defines function bodies,
54
+ * the second receives the same call tokens and returns the workflow entry
55
+ * point.
56
+ *
57
+ * Desugars to a ResumeHandle with a Branch-based handler. Each call token
58
+ * is Chain(Tag("CallN"), ResumePerform(id)). The handler dispatches to the
59
+ * correct function body by tag. The caller's pipeline is preserved as a
60
+ * ResumePerformFrame across each call.
61
+ */
62
+ export function defineRecursiveFunctions<TDefs extends FunctionDef[]>(
63
+ bodiesFn: (...fns: FunctionRefs<TDefs>) => {
64
+ [K in keyof TDefs]: Pipeable<TDefs[K][0], TDefs[K][1]>;
65
+ },
66
+ ): <TOut>(
67
+ entryFn: (...fns: FunctionRefs<TDefs>) => BodyResult<TOut>,
68
+ ) => TypedAction<any, TOut> {
69
+ const resumeHandlerId = allocateResumeHandlerId();
70
+
71
+ const resumePerform: Action = {
72
+ kind: "ResumePerform",
73
+ resume_handler_id: resumeHandlerId,
74
+ };
75
+
76
+ // Call tokens: Chain(Tag("CallN"), ResumePerform(resumeHandlerId))
77
+ const fnCount = bodiesFn.length;
78
+ const callTokens = Array.from({ length: fnCount }, (_, i) =>
79
+ typedAction(chain(tag(`Call${i}`), resumePerform as any) as Action),
80
+ );
81
+
82
+ // Get function body ASTs
83
+ const bodyActions = bodiesFn(
84
+ ...(callTokens as FunctionRefs<TDefs>),
85
+ ) as Action[];
86
+
87
+ // Branch cases: CallN → ExtractField("value") → bodyN
88
+ const cases: Record<string, Action> = {};
89
+ for (let i = 0; i < bodyActions.length; i++) {
90
+ cases[`Call${i}`] = chain(
91
+ extractField("value"),
92
+ bodyActions[i] as any,
93
+ ) as Action;
94
+ }
95
+
96
+ // Return curried entry-point combinator
97
+ return <TOut>(entryFn: (...fns: FunctionRefs<TDefs>) => BodyResult<TOut>) => {
98
+ const userBody = entryFn(...(callTokens as FunctionRefs<TDefs>)) as Action;
99
+
100
+ return typedAction<any, TOut>(
101
+ chain(all(identity, constant(UNUSED_STATE)), {
102
+ kind: "ResumeHandle",
103
+ resume_handler_id: resumeHandlerId,
104
+ body: chain(extractIndex(0), userBody as any) as Action,
105
+ handler: all(
106
+ chain(extractIndex(0), branch(cases) as any),
107
+ constant(UNUSED_STATE),
108
+ ) as Action,
109
+ } as Action) as Action,
110
+ );
111
+ };
112
+ }
package/src/run.ts ADDED
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Workflow execution: resolves the barnum binary, tsx executor, and worker
3
+ * script, then spawns the workflow as a subprocess.
4
+ */
5
+
6
+ import { execFileSync, spawn as nodeSpawn } from "node:child_process";
7
+ import { createRequire } from "node:module";
8
+ import { existsSync } from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+ import type { Action, Config, ExtractOutput, Pipeable } from "./ast.js";
12
+ import { chain } from "./chain.js";
13
+ import { constant } from "./builtins.js";
14
+
15
+ const __dirname = import.meta.dirname;
16
+
17
+ /** Resolve the TypeScript executor. Uses bun if the workflow was launched with bun, otherwise tsx. */
18
+ function resolveExecutor(): string {
19
+ if (process.versions.bun) {
20
+ return "bun";
21
+ }
22
+ const callerRequire = createRequire(process.argv[1] || import.meta.url);
23
+ const tsxPath = callerRequire.resolve("tsx/cli");
24
+ return `node ${tsxPath}`;
25
+ }
26
+
27
+ /** Resolve the platform-specific binary from the @barnum/barnum package artifacts. */
28
+ function resolveInstalledBinary(): string | undefined {
29
+ const platform = os.platform();
30
+ const arch = os.arch();
31
+
32
+ let artifactDir: string;
33
+ let binaryName = "barnum";
34
+
35
+ if (platform === "darwin" && arch === "arm64") {
36
+ artifactDir = "macos-arm64";
37
+ } else if (platform === "darwin") {
38
+ artifactDir = "macos-x64";
39
+ } else if (platform === "linux" && arch === "arm64") {
40
+ artifactDir = "linux-arm64";
41
+ } else if (platform === "linux") {
42
+ artifactDir = "linux-x64";
43
+ } else if (platform === "win32") {
44
+ artifactDir = "win-x64";
45
+ binaryName = "barnum.exe";
46
+ } else {
47
+ return undefined;
48
+ }
49
+
50
+ const callerRequire = createRequire(process.argv[1] || import.meta.url);
51
+ try {
52
+ const packageDir = path.dirname(
53
+ callerRequire.resolve("@barnum/barnum/package.json"),
54
+ );
55
+ const binaryPath = path.join(
56
+ packageDir,
57
+ "artifacts",
58
+ artifactDir,
59
+ binaryName,
60
+ );
61
+ if (existsSync(binaryPath)) {
62
+ return binaryPath;
63
+ }
64
+ } catch {
65
+ // Package not installed
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ type BinaryResolution =
71
+ | { kind: "Env"; path: string }
72
+ | { kind: "NodeModules"; path: string }
73
+ | { kind: "Local"; path: string };
74
+
75
+ /** Resolve the barnum binary. Checks: BARNUM env var, local repo, node_modules. */
76
+ function resolveBinary(): BinaryResolution {
77
+ if (process.env.BARNUM) {
78
+ return { kind: "Env", path: process.env.BARNUM };
79
+ }
80
+
81
+ const repoRoot = path.resolve(__dirname, "../../..");
82
+ if (existsSync(path.join(repoRoot, "Cargo.toml"))) {
83
+ return {
84
+ kind: "Local",
85
+ path: path.join(repoRoot, "target/debug/barnum"),
86
+ };
87
+ }
88
+
89
+ const installedBinaryPath = resolveInstalledBinary();
90
+ if (installedBinaryPath) {
91
+ return { kind: "NodeModules", path: installedBinaryPath };
92
+ }
93
+
94
+ throw new Error(
95
+ "Could not find barnum binary. Set BARNUM env var or install @barnum/barnum.",
96
+ );
97
+ }
98
+
99
+ /** Resolve worker.ts relative to this package. */
100
+ function resolveWorker(): string {
101
+ return path.resolve(__dirname, "../src/worker.ts");
102
+ }
103
+
104
+ /** Build the barnum binary if using the local dev path. */
105
+ function buildBinary(): void {
106
+ const repoRoot = path.resolve(__dirname, "../../..");
107
+ execFileSync("cargo", ["build", "-p", "barnum_cli"], {
108
+ cwd: repoRoot,
109
+ stdio: "ignore",
110
+ });
111
+ }
112
+
113
+ /** Run a pipeline to completion. Returns the workflow's final output value. */
114
+ export function runPipeline<TPipeline extends Action>(
115
+ pipeline: TPipeline,
116
+ input?: unknown,
117
+ ): Promise<ExtractOutput<TPipeline>> {
118
+ const workflow =
119
+ input === undefined
120
+ ? pipeline
121
+ : (chain(constant(input) as Pipeable, pipeline as Pipeable) as Action);
122
+ return spawnBarnum({ workflow });
123
+ }
124
+
125
+ /** Spawn the barnum CLI with the given config. Returns the parsed final value from stdout. */
126
+ function spawnBarnum<TOut>(config: Config): Promise<TOut> {
127
+ const binaryResolution = resolveBinary();
128
+ if (binaryResolution.kind === "Local") {
129
+ buildBinary();
130
+ }
131
+ const executor = resolveExecutor();
132
+ const worker = resolveWorker();
133
+ const configJson = JSON.stringify(config);
134
+
135
+ return new Promise<TOut>((resolve, reject) => {
136
+ const child = nodeSpawn(
137
+ binaryResolution.path,
138
+ [
139
+ "run",
140
+ "--config",
141
+ configJson,
142
+ "--executor",
143
+ executor,
144
+ "--worker",
145
+ worker,
146
+ ],
147
+ {
148
+ stdio: ["inherit", "pipe", "inherit"],
149
+ },
150
+ );
151
+
152
+ const stdoutChunks: Buffer[] = [];
153
+
154
+ child.stdout?.on("data", (chunk: Buffer) => {
155
+ stdoutChunks.push(chunk);
156
+ });
157
+
158
+ child.on("error", (error) => {
159
+ reject(new Error(`Failed to spawn barnum: ${error.message}`));
160
+ });
161
+
162
+ child.on("close", (code) => {
163
+ if (code !== 0) {
164
+ reject(new Error(`barnum exited with code ${code}`));
165
+ return;
166
+ }
167
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
168
+ if (!stdout) {
169
+ resolve(undefined as TOut);
170
+ return;
171
+ }
172
+ try {
173
+ resolve(JSON.parse(stdout) as TOut);
174
+ } catch {
175
+ reject(
176
+ new Error(`barnum produced non-JSON output on stdout: ${stdout}`),
177
+ );
178
+ }
179
+ });
180
+ });
181
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,118 @@
1
+ import type { JSONSchema7 } from "json-schema";
2
+ import { type z, toJSONSchema } from "zod";
3
+
4
+ // Zod v4 schema def types that have child schemas.
5
+ // Verified against Zod 4.3.6 internals — every compound type's def
6
+ // shape and child property name is listed here.
7
+ const CHILD_ACCESSORS: Record<string, (def: any) => z.ZodType[]> = {
8
+ object: (def) => Object.values(def.shape),
9
+ array: (def) => [def.element],
10
+ tuple: (def) => [...def.items, ...(def.rest ? [def.rest] : [])],
11
+ union: (def) => def.options,
12
+ intersection: (def) => [def.left, def.right],
13
+ record: (def) => [def.keyType, def.valueType],
14
+ // Wrappers with a single inner type
15
+ nullable: (def) => [def.innerType],
16
+ optional: (def) => [def.innerType],
17
+ nonoptional: (def) => [def.innerType],
18
+ default: (def) => [def.innerType],
19
+ catch: (def) => [def.innerType],
20
+ readonly: (def) => [def.innerType],
21
+ promise: (def) => [def.innerType],
22
+ // Pipe has two children
23
+ pipe: (def) => [def.in, def.out],
24
+ // Lazy resolves to inner schema
25
+ lazy: (def) => [def.getter()],
26
+ };
27
+
28
+ /**
29
+ * Walk the Zod schema tree and reject patterns that `toJSONSchema()`
30
+ * handles incorrectly or silently drops:
31
+ *
32
+ * - `z.intersection()` — produces `allOf` with `additionalProperties: false`
33
+ * on both sides (from `io: "output"`), making the intersection unmatchable
34
+ * on Draft 7.
35
+ *
36
+ * - `.refine()` / `.superRefine()` — silently stripped from JSON Schema
37
+ * output, so the Rust side would accept values that fail the refinement.
38
+ * Detected by checking for custom checks (`check._zod.def.check === "custom"`)
39
+ * in the schema's checks array.
40
+ */
41
+ function assertNoUnsupportedPatterns(
42
+ schema: z.ZodType,
43
+ label: string,
44
+ visited = new WeakSet<z.ZodType>(),
45
+ ): void {
46
+ if (visited.has(schema)) {
47
+ return;
48
+ }
49
+ visited.add(schema);
50
+
51
+ const def = (schema as any)._zod.def;
52
+
53
+ // Reject intersections
54
+ if (def.type === "intersection") {
55
+ throw new Error(
56
+ `Handler "${label}": z.intersection() is not supported. ` +
57
+ `It produces broken JSON Schema on Draft 7 because both sides ` +
58
+ `get additionalProperties: false. Use z.object().extend() or ` +
59
+ `z.object().merge() instead.`,
60
+ );
61
+ }
62
+
63
+ // Reject custom checks (from .refine() and .superRefine())
64
+ const checks: any[] | undefined = def.checks;
65
+ if (checks) {
66
+ for (const check of checks) {
67
+ if (check._zod.def.check === "custom") {
68
+ throw new Error(
69
+ `Handler "${label}": .refine() and .superRefine() are not ` +
70
+ `supported. Custom validations cannot be expressed in JSON ` +
71
+ `Schema and would be silently dropped.`,
72
+ );
73
+ }
74
+ }
75
+ }
76
+
77
+ // Recurse into children
78
+ const getChildren = CHILD_ACCESSORS[def.type];
79
+ if (getChildren) {
80
+ for (const child of getChildren(def)) {
81
+ assertNoUnsupportedPatterns(child, label, visited);
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Convert a Zod schema to a JSON Schema document suitable for embedding
88
+ * in the serialized AST. Throws if the schema contains types that can't
89
+ * survive the TS → JSON → Rust boundary.
90
+ */
91
+ export function zodToCheckedJsonSchema(
92
+ schema: z.ZodType,
93
+ label: string,
94
+ ): JSONSchema7 {
95
+ // Pre-validate: catch patterns that toJSONSchema() handles incorrectly
96
+ assertNoUnsupportedPatterns(schema, label);
97
+
98
+ let raw: Record<string, unknown>;
99
+ try {
100
+ raw = toJSONSchema(schema, {
101
+ target: "draft-07",
102
+ unrepresentable: "throw",
103
+ io: "output",
104
+ cycles: "throw",
105
+ reused: "inline",
106
+ }) as Record<string, unknown>;
107
+ } catch (error) {
108
+ const message = error instanceof Error ? error.message : String(error);
109
+ throw new Error(
110
+ `Handler "${label}": Zod schema cannot be converted to JSON Schema: ${message}`,
111
+ { cause: error },
112
+ );
113
+ }
114
+
115
+ // Strip $schema — embedded schemas don't need the draft URI.
116
+ const { $schema: _, ...rest } = raw;
117
+ return rest as JSONSchema7;
118
+ }
@@ -0,0 +1,53 @@
1
+ import {
2
+ type Action,
3
+ type Pipeable,
4
+ type TypedAction,
5
+ typedAction,
6
+ buildRestartBranchAction,
7
+ TAG_BREAK,
8
+ } from "./ast.js";
9
+ import { allocateRestartHandlerId } from "./effect-id.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // tryCatch — type-level error handling via restart+Branch
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /**
16
+ * HOAS combinator for type-level error handling. The body callback receives
17
+ * a `throwError` token — a `TypedAction<TError, never>` that, when placed
18
+ * in the pipeline, tags the error as Break, performs to the handler, which
19
+ * restarts the body. The body-level Branch routes to the recovery arm.
20
+ *
21
+ * This handles **type-level errors only** — values returned by handlers via
22
+ * the `Result` type. If a handler panics, throws a JavaScript exception, or
23
+ * the runtime crashes, the existing error propagation path handles it.
24
+ * tryCatch does not catch those. Analogous to Rust's `Result` vs `panic!`.
25
+ *
26
+ * Compiled form (restart+Branch, same substrate as loop/earlyReturn):
27
+ * `Chain(Tag("Continue"),`
28
+ * `RestartHandle(id, ExtractIndex(0),`
29
+ * `Branch({ Continue: body, Break: recovery })))`
30
+ *
31
+ * throwError = `Chain(Tag("Break"), RestartPerform(id))`
32
+ *
33
+ * When throwError fires: error tagged Break → `RestartPerform` → handler extracts
34
+ * payload → body restarts → Branch takes Break arm → recovery receives error.
35
+ */
36
+ export function tryCatch<TIn, TOut, TError>(
37
+ body: (throwError: TypedAction<TError, never>) => Pipeable<TIn, TOut>,
38
+ recovery: Pipeable<TError, TOut>,
39
+ ): TypedAction<TIn, TOut> {
40
+ const restartHandlerId = allocateRestartHandlerId();
41
+
42
+ const throwError = typedAction<TError, never>({
43
+ kind: "Chain",
44
+ first: TAG_BREAK,
45
+ rest: { kind: "RestartPerform", restart_handler_id: restartHandlerId },
46
+ });
47
+
48
+ const bodyAction = body(throwError) as Action;
49
+
50
+ return typedAction(
51
+ buildRestartBranchAction(restartHandlerId, bodyAction, recovery as Action),
52
+ );
53
+ }
package/src/worker.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Worker script: invoked as `tsx worker.ts <module> <export>`.
3
+ *
4
+ * Protocol:
5
+ * Rust → stdin: JSON `{ "value": <any> }`
6
+ * stdout → Rust: JSON result (handler return value)
7
+ *
8
+ * If the handler throws or the module/export can't be resolved, the
9
+ * process exits non-zero. Rust interprets that as a fatal workflow error.
10
+ */
11
+
12
+ // Suppress EPIPE — when the Rust binary exits (e.g., a race was resolved),
13
+ // orphan workers get broken pipe on stdout. This is expected, not an error.
14
+ process.stdout.on("error", (error: NodeJS.ErrnoException) => {
15
+ if (error.code === "EPIPE") {
16
+ process.exit(0);
17
+ }
18
+ throw error;
19
+ });
20
+
21
+ async function main(): Promise<void> {
22
+ const [modulePath, exportName = "default"] = process.argv.slice(2);
23
+
24
+ if (!modulePath) {
25
+ process.stderr.write("worker: missing module path argument\n");
26
+ process.exit(1);
27
+ }
28
+
29
+ // Read entire stdin
30
+ const chunks: Buffer[] = [];
31
+ for await (const chunk of process.stdin) {
32
+ chunks.push(chunk);
33
+ }
34
+ const input = JSON.parse(Buffer.concat(chunks).toString());
35
+
36
+ // Import handler, call it
37
+ const mod = await import(modulePath);
38
+ const handler = mod[exportName];
39
+
40
+ if (!handler?.__definition?.handle) {
41
+ process.stderr.write(
42
+ `worker: ${modulePath}:${exportName} is not a barnum handler\n`,
43
+ );
44
+ process.exit(1);
45
+ }
46
+
47
+ const result = await handler.__definition.handle({ value: input.value });
48
+
49
+ // Write result to stdout
50
+ process.stdout.write(JSON.stringify(result) ?? "null");
51
+ }
52
+
53
+ main().catch((error) => {
54
+ process.stderr.write(`worker: ${error}\n`);
55
+ process.exit(1);
56
+ });
package/README.md DELETED
@@ -1,19 +0,0 @@
1
- # @barnum/barnum
2
-
3
- Barnum CLI - workflow engine for agents.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm install -g @barnum/barnum
9
- ```
10
-
11
- ## Usage
12
-
13
- ```bash
14
- barnum --help
15
- ```
16
-
17
- ## About
18
-
19
- Barnum is a task automation tool that orchestrates AI agents to execute multi-step workflows defined in configuration files.