@chrok/braid 0.1.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.
@@ -0,0 +1,53 @@
1
+ # Resource limits and deployment responsibility
2
+
3
+ Braid targets small reasoning graphs. Graph size, prompt/output size, event-log
4
+ size, and the number of simultaneous graph submissions have no hard cap in 0.1.
5
+ The scheduler rescans the graph as work settles. Consult the
6
+ [benchmark](benchmark.md) for measured local overhead rather than treating the
7
+ validator's deep-graph tests as a production capacity guarantee.
8
+
9
+ | Control | Core default | Pi default |
10
+ | --- | --- | --- |
11
+ | Active runtime-managed invocations per graph | 4 | 4 |
12
+ | Node timeout (starts at admission) | 60 seconds | Unlimited |
13
+ | Graph timeout (includes queueing) | 5 minutes | Unlimited |
14
+ | Caller cancellation | `AbortSignal` | `braid_cancel` / panel `c` |
15
+ | Worker tool loop | Decision continuation / merge Git-tool loop, bounded by deadlines | Unlimited by default; optional `maxToolRounds` and `maxToolCalls` |
16
+ | Result preview | Full result | 50 KB / 2,000 lines, then temporary full-result file |
17
+ | Token / monetary budget | Not enforced | Not enforced |
18
+
19
+ Pi keeps unlimited timeouts for interactive background work. For bounded work,
20
+ submit explicit `options`, for example:
21
+
22
+ ```json
23
+ { "maxConcurrency": 2, "nodeTimeoutMs": 30000, "graphTimeoutMs": 120000, "maxToolRounds": 12, "maxToolCalls": 32 }
24
+ ```
25
+
26
+ The concurrency limit belongs to each graph, not to the process or provider
27
+ account. Ten graphs with `maxConcurrency: 4` can collectively start 40 calls.
28
+ The host should cap simultaneous submissions and validate graph dimensions and
29
+ input sizes before admitting untrusted requests. The core's graph timer starts
30
+ after validation; it does not bound validation CPU or input allocation.
31
+
32
+ Token counts report only usage returned by runners. Missing counts, failed
33
+ requests, or timeout-aborted calls can still incur charges. There is no hard cost
34
+ ceiling, and adding a token counter after completion cannot prevent concurrent
35
+ calls from overspending. Use provider account quotas, output-token limits in a
36
+ custom runner, host admission controls, and finite deadlines where needed.
37
+
38
+ All events and full results live in memory; handoff events retain output for each
39
+ edge. Large fan-out/fan-in graphs and verbose workers increase memory and context
40
+ use. Pi retains completed jobs for its session lifetime and full temporary result
41
+ files until the host/user removes them. Limit the length of sessions or reload
42
+ after exporting needed results. Reloading also cancels outstanding work.
43
+
44
+ Git worktree preparation, checkpointing, tracked writes, and cleanup add disk,
45
+ Git-process, and elapsed-time overhead. Cleanup may extend wall time beyond a
46
+ model deadline. Automatically appended merge agents are additional model calls
47
+ and share the graph's remaining time. Benchmark results measured outside Git do
48
+ not include this lifecycle. Recoverable refs retain Git objects until removed.
49
+
50
+ An aborted runtime slot can be reused even if an uncooperative provider continues
51
+ working. Runners must forward `signal`; neither Braid nor JavaScript can forcibly
52
+ stop that remote work. Workspace and tool guards are not a security sandbox. See
53
+ [SECURITY.md](../SECURITY.md) before exposing a runner to untrusted input.
@@ -0,0 +1,86 @@
1
+ import { braid, type BraidInput, type ModelRunner } from "../src/index.js";
2
+ import { createOpenAICompatibleRunner } from "../src/adapters/openai.js";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ const input: BraidInput = {
8
+ goal: "Compare the benefits and risks of adopting a four-day work week.",
9
+ nodes: [
10
+ {
11
+ type: "decision",
12
+ id: "route",
13
+ prompt: "Choose whether this goal needs a comparison or a short answer.",
14
+ choices: ["compare", "brief"],
15
+ },
16
+ {
17
+ type: "execute",
18
+ id: "benefits",
19
+ prompt: "Explain the main potential benefits.",
20
+ },
21
+ {
22
+ type: "execute",
23
+ id: "risks",
24
+ prompt: "Explain the main risks and uncertainties.",
25
+ },
26
+ {
27
+ type: "execute",
28
+ id: "answer",
29
+ prompt: "Synthesize both perspectives into a balanced recommendation.",
30
+ },
31
+ {
32
+ type: "execute",
33
+ id: "brief",
34
+ prompt: "Give a concise answer to the goal.",
35
+ },
36
+ ],
37
+ edges: [
38
+ { from: "route", to: "benefits", choice: "compare" },
39
+ { from: "route", to: "risks", choice: "compare" },
40
+ { from: "benefits", to: "answer" },
41
+ { from: "risks", to: "answer" },
42
+ { from: "route", to: "brief", choice: "brief" },
43
+ ],
44
+ };
45
+
46
+ // Deterministic stand-in for a provider. No network calls or token estimates.
47
+ let runner: ModelRunner = async (request) => {
48
+ if (request.decide) {
49
+ request.decide("compare");
50
+ return { output: "A comparison needs both benefits and risks." };
51
+ }
52
+ if (request.node.id === "benefits")
53
+ return { output: "A shorter week may improve work-life balance." };
54
+ if (request.node.id === "risks")
55
+ return { output: "Coverage and workload compression need evaluation." };
56
+ return {
57
+ output:
58
+ request.predecessors
59
+ .map((item) => `[${item.nodeId}] ${item.output}`)
60
+ .join("\n") +
61
+ "\nRecommendation: test a time-limited pilot with explicit success criteria.",
62
+ };
63
+ };
64
+ let defaultModel = "demo";
65
+
66
+ // Live calls are opt-in: OPENAI_API_KEY=... BRAID_MODEL=... npm run demo -- --live
67
+ if (process.argv.includes("--live")) {
68
+ const apiKey = process.env.OPENAI_API_KEY;
69
+ const model = process.env.BRAID_MODEL;
70
+ const baseURL = process.env.OPENAI_BASE_URL;
71
+ if (!apiKey || !model)
72
+ throw new Error("--live requires OPENAI_API_KEY and BRAID_MODEL");
73
+ defaultModel = model;
74
+ runner = createOpenAICompatibleRunner({
75
+ apiKey,
76
+ ...(baseURL ? { baseURL } : {}),
77
+ });
78
+ }
79
+
80
+ // This text-only demo runs outside Git and needs no repository workspaces.
81
+ const cwd = await mkdtemp(join(tmpdir(), "braid-demo-"));
82
+ try {
83
+ const result = await braid(input, { cwd, runner, defaultModel, maxConcurrency: 2 });
84
+ console.log(JSON.stringify(result, null, 2));
85
+ if (result.status === "failed") process.exitCode = 1;
86
+ } finally { await rm(cwd, { recursive: true, force: true }); }
@@ -0,0 +1,24 @@
1
+ import { braid, type BraidInput, type ModelRunner } from "../src/index.js";
2
+ import { inTemporaryDirectory } from "./support.js";
3
+
4
+ // Supply an actual diff as graph data. Core workers have no filesystem access.
5
+ const diff = "- return cache[key];\n+ return cache[key] ?? await load(key);";
6
+ const graph: BraidInput = {
7
+ goal: `Review this proposed cache change:\n${diff}`,
8
+ nodes: [
9
+ { type: "execute", id: "correctness", prompt: "Review cache semantics and concurrent misses." },
10
+ { type: "execute", id: "tests", prompt: "Identify regression cases worth testing." },
11
+ { type: "execute", id: "review", prompt: "Synthesize actionable findings from both reviews." },
12
+ ],
13
+ edges: [{ from: "correctness", to: "review" }, { from: "tests", to: "review" }],
14
+ };
15
+ const runner: ModelRunner = async ({ node, predecessors }) => ({
16
+ output: node.id === "correctness"
17
+ ? "Concurrent cache misses can call load twice. Confirm whether null is a cached value."
18
+ : node.id === "tests"
19
+ ? "Test an existing value, null, concurrent misses, and a rejected load."
20
+ : predecessors.map(p => `${p.nodeId}: ${p.output}`).join("\n"),
21
+ });
22
+ const result = await inTemporaryDirectory(cwd => braid(graph, { cwd, runner, maxConcurrency: 2 }));
23
+ if (result.status !== "completed") throw new Error(result.error?.message);
24
+ console.log(result.terminalOutputs.review!.output);
@@ -0,0 +1,28 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { braid, type ModelRunner } from "../src/index.js";
3
+ import { inTemporaryDirectory } from "./support.js";
4
+
5
+ // A deterministic adapter demonstrating the contract; replace this stand-in
6
+ // with a fresh provider conversation and expose decide as an actual model tool.
7
+ const runner: ModelRunner = async request => {
8
+ request.signal.throwIfAborted();
9
+ await delay(1, undefined, { signal: request.signal });
10
+ if (request.node.type === "decision") {
11
+ request.decide!("continue");
12
+ return { output: "Continue with the analysis.", model: "offline-example" };
13
+ }
14
+ return {
15
+ output: `Received direct predecessors: ${request.predecessors.map(p => p.nodeId).join(", ")}`,
16
+ model: "offline-example",
17
+ };
18
+ };
19
+ const result = await inTemporaryDirectory(cwd => braid({
20
+ goal: "Demonstrate a cancellable isolated runner.",
21
+ nodes: [
22
+ { type: "decision", id: "route", prompt: "Choose a path.", choices: ["continue", "stop"] },
23
+ { type: "execute", id: "answer", prompt: "List the supplied predecessor IDs." },
24
+ ],
25
+ edges: [{ from: "route", to: "answer", choice: "continue" }],
26
+ }, { cwd, runner, nodeTimeoutMs: 1000, graphTimeoutMs: 5000 }));
27
+ if (result.status !== "completed") throw new Error(result.error?.message);
28
+ console.log(result.terminalOutputs.answer!.output);
@@ -0,0 +1,29 @@
1
+ import assert from "node:assert/strict";
2
+ import { braid } from "../src/index.js";
3
+ import { inTemporaryDirectory } from "./support.js";
4
+
5
+ const result = await inTemporaryDirectory(cwd => braid({
6
+ goal: "Retain independent findings when one provider fails.",
7
+ nodes: [
8
+ { type: "execute", id: "offline", prompt: "Summarize known local facts." },
9
+ { type: "execute", id: "remote", prompt: "Ask the unavailable provider." },
10
+ { type: "execute", id: "join", prompt: "Combine both inputs." },
11
+ ],
12
+ edges: [{ from: "offline", to: "join" }, { from: "remote", to: "join" }],
13
+ }, {
14
+ cwd,
15
+ runner: async ({ node, predecessors }) => {
16
+ if (node.id === "remote") throw new Error("Demo provider unavailable");
17
+ if (node.id === "join") return {
18
+ output: predecessors.map(p => `${p.nodeId}: ${p.error ? `unavailable (${p.error.code})` : p.output}`).join("\n"),
19
+ };
20
+ return { output: "Known local facts are still available." };
21
+ },
22
+ }));
23
+ assert.equal(result.status, "failed");
24
+ assert.equal(result.nodes.join!.status, "completed");
25
+ assert.match(result.terminalOutputs.join!.output, /remote: unavailable \(MODEL_ERROR\)/);
26
+ // Unconditional successors receive failed predecessors as explicit error context.
27
+ // A recovered answer does not erase the graph's original failure status.
28
+ console.log(`status: ${result.status}; join: ${result.nodes.join!.status}`);
29
+ console.log(result.terminalOutputs.join!.output);
@@ -0,0 +1,10 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ /** Text-only examples run outside Git, without worktrees or source integration. */
6
+ export async function inTemporaryDirectory<T>(run: (cwd: string) => Promise<T>): Promise<T> {
7
+ const cwd = await mkdtemp(join(tmpdir(), "braid-example-"));
8
+ try { return await run(cwd); }
9
+ finally { await rm(cwd, { recursive: true, force: true }); }
10
+ }
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@chrok/braid",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "description": "A small, framework-agnostic DAG runtime for isolated model invocations",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=22"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./adapters/openai": {
16
+ "types": "./dist/adapters/openai.d.ts",
17
+ "import": "./dist/adapters/openai.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md",
23
+ "LICENSE",
24
+ "CHANGELOG.md",
25
+ "docs",
26
+ "examples",
27
+ "CONTRIBUTING.md",
28
+ "SECURITY.md",
29
+ "CODE_OF_CONDUCT.md",
30
+ "ROADMAP.md"
31
+ ],
32
+ "scripts": {
33
+ "build": "node scripts/build.mjs",
34
+ "check": "tsc --noEmit",
35
+ "test": "tsx --test test/*.test.ts",
36
+ "demo": "tsx examples/basic.ts",
37
+ "check:pi": "npm --prefix integrations/pi run check",
38
+ "test:pi": "npm --prefix integrations/pi test",
39
+ "prepack": "npm run build",
40
+ "build:pi": "npm --prefix integrations/pi run build",
41
+ "test:package": "node scripts/package-smoke.mjs",
42
+ "verify": "npm run check && npm test && npm run check:pi && npm run test:pi && npm run examples && npm run test:package",
43
+ "examples": "tsx examples/basic.ts && tsx examples/code-review.ts && tsx examples/failure-handling.ts && tsx examples/custom-runner.ts",
44
+ "bench": "npm run build && node --expose-gc scripts/benchmark.mjs",
45
+ "demo:panel": "tsx scripts/panel-demo.ts",
46
+ "test:pi:live": "npm run build:pi && node integrations/pi/test/live/run.mjs"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.0.0",
50
+ "tsx": "^4.0.0",
51
+ "typescript": "^5.0.0"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/Epsirom/braid.git"
56
+ },
57
+ "homepage": "https://github.com/Epsirom/braid#readme",
58
+ "bugs": {
59
+ "url": "https://github.com/Epsirom/braid/issues"
60
+ },
61
+ "keywords": [
62
+ "llm",
63
+ "agents",
64
+ "dag",
65
+ "orchestration",
66
+ "typescript"
67
+ ],
68
+ "publishConfig": {
69
+ "access": "public",
70
+ "registry": "https://registry.npmjs.org"
71
+ }
72
+ }