@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.
- package/CHANGELOG.md +28 -0
- package/CODE_OF_CONDUCT.md +22 -0
- package/CONTRIBUTING.md +61 -0
- package/LICENSE +21 -0
- package/README.md +513 -0
- package/ROADMAP.md +38 -0
- package/SECURITY.md +44 -0
- package/dist/adapters/openai.d.ts +10 -0
- package/dist/adapters/openai.js +240 -0
- package/dist/budgets.d.ts +3 -0
- package/dist/budgets.js +19 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/merge-tools.d.ts +70 -0
- package/dist/merge-tools.js +96 -0
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.js +532 -0
- package/dist/types.d.ts +238 -0
- package/dist/types.js +1 -0
- package/dist/validate.d.ts +16 -0
- package/dist/validate.js +110 -0
- package/dist/workspaces.d.ts +29 -0
- package/dist/workspaces.js +494 -0
- package/docs/assets/pi-panel.svg +40 -0
- package/docs/benchmark-results.json +192 -0
- package/docs/benchmark.md +39 -0
- package/docs/compatibility.md +42 -0
- package/docs/examples.md +31 -0
- package/docs/releasing.md +54 -0
- package/docs/resource-limits.md +53 -0
- package/examples/basic.ts +86 -0
- package/examples/code-review.ts +24 -0
- package/examples/custom-runner.ts +28 -0
- package/examples/failure-handling.ts +29 -0
- package/examples/support.ts +10 -0
- package/package.json +72 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
No changes yet.
|
|
6
|
+
|
|
7
|
+
## 0.1.0 — 2026-09-27
|
|
8
|
+
|
|
9
|
+
Initial public release. Publication is tracked in
|
|
10
|
+
[GitHub releases](https://github.com/Epsirom/braid/releases).
|
|
11
|
+
|
|
12
|
+
- Framework-independent DAG runtime with conditional decisions, bounded
|
|
13
|
+
concurrency, explicit joins, failure propagation, and isolated node context.
|
|
14
|
+
- Caller cancellation, node/graph deadlines, immutable events, partial results,
|
|
15
|
+
model overrides, and provider-reported token accounting.
|
|
16
|
+
- OpenAI-compatible Chat Completions runner with validated decision tool calls.
|
|
17
|
+
- Pi background jobs, completion reminders, cancellation, guarded worker tools,
|
|
18
|
+
and a live flow panel. The Pi package includes the matching core runtime.
|
|
19
|
+
- Core-managed Git worktrees, checkpoint/backup refs, agent-driven merge nodes,
|
|
20
|
+
failure context on unconditional edges, and optional Pi tool budgets.
|
|
21
|
+
- Clean package builds, isolated tarball checks, CI for Node and supported
|
|
22
|
+
operating systems, and an npm trusted-publishing workflow.
|
|
23
|
+
- Install guides, runnable offline examples, compatibility and resource-limit
|
|
24
|
+
documentation, scheduler benchmarks, and contribution/security policies.
|
|
25
|
+
|
|
26
|
+
Limitations: no persistent jobs, retries, token/spend budgets, streaming core
|
|
27
|
+
responses, graph mutation, nested runs, or sandboxing. See the roadmap and
|
|
28
|
+
resource-limit documentation before using it as a service.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Code of conduct
|
|
2
|
+
|
|
3
|
+
Participants should be able to ask questions, disagree, and contribute without
|
|
4
|
+
harassment. Be respectful, assume good faith, focus criticism on the work, and
|
|
5
|
+
accept feedback. Welcome people regardless of background, identity, experience,
|
|
6
|
+
or ability. English and Chinese contributions are welcome.
|
|
7
|
+
|
|
8
|
+
Harassment, threats, discriminatory remarks, unwanted sexual attention, doxxing,
|
|
9
|
+
and repeated personal attacks are not acceptable. Do not publish another
|
|
10
|
+
person's private information without permission.
|
|
11
|
+
|
|
12
|
+
This applies to project issues, pull requests, reviews, and other spaces where
|
|
13
|
+
participants represent Braid. The maintainer may edit or remove harmful content,
|
|
14
|
+
warn participants, limit interaction, or ban repeated or serious violations.
|
|
15
|
+
Actions should be proportionate to the behavior and its impact.
|
|
16
|
+
|
|
17
|
+
Report concerns to [Epsirom](https://github.com/Epsirom) using a contact method on
|
|
18
|
+
the GitHub profile. Do not post sensitive reports publicly. If no private contact
|
|
19
|
+
is available, or a report concerns the maintainer, use GitHub's Report abuse
|
|
20
|
+
feature. Reports are handled discreetly, with information shared only as needed
|
|
21
|
+
to address the incident. This is a volunteer project; response times are not
|
|
22
|
+
guaranteed.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Contributing to Braid
|
|
2
|
+
|
|
3
|
+
Bug reports, documentation fixes, small examples, and focused runtime or adapter
|
|
4
|
+
improvements are welcome. Start with an issue before a large API change. Explain
|
|
5
|
+
the user problem and how it fits the [scope and roadmap](ROADMAP.md).
|
|
6
|
+
|
|
7
|
+
## Development
|
|
8
|
+
|
|
9
|
+
Use Node.js 22.19+ and npm. From a fresh checkout:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
git clone https://github.com/Epsirom/braid.git
|
|
13
|
+
cd braid
|
|
14
|
+
npm ci
|
|
15
|
+
npm ci --prefix integrations/pi
|
|
16
|
+
npm run verify
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
There are two packages and two lockfiles. The core has no runtime dependencies;
|
|
20
|
+
Pi's dependencies belong in `integrations/pi`. Update and commit the corresponding
|
|
21
|
+
lockfile when changing a dependency. Do not commit generated `dist` directories,
|
|
22
|
+
tarballs, credentials, or provider output containing private data.
|
|
23
|
+
|
|
24
|
+
`verify` type-checks both packages, runs deterministic tests and offline examples,
|
|
25
|
+
then builds tarballs and installs them into a temporary consumer outside the
|
|
26
|
+
checkout. That last check needs registry access for Pi dependencies but never
|
|
27
|
+
calls a model. `npm test` is the fast core-only loop; `npm run test:pi` tests Pi.
|
|
28
|
+
`npm run bench` measures the scheduler without a provider.
|
|
29
|
+
|
|
30
|
+
## Changes and reviews
|
|
31
|
+
|
|
32
|
+
- Keep the TypeScript strict checks passing. Follow nearby code and the
|
|
33
|
+
repository's two-space formatting; use explicit public types.
|
|
34
|
+
- For behavior changes, add a regression test that fails before the change.
|
|
35
|
+
Use deferred promises or fake runners instead of live models or long sleeps.
|
|
36
|
+
- Preserve decision-tool validation, join semantics, cancellation, context
|
|
37
|
+
isolation, partial results, and provider usage accounting.
|
|
38
|
+
- Explain the observable change, why it is needed, and how it was verified in
|
|
39
|
+
the PR. Update relevant docs and the Unreleased changelog for user-facing work.
|
|
40
|
+
- Read [compatibility](docs/compatibility.md) before changing public fields or
|
|
41
|
+
error/event shapes. Discuss breaking changes before implementing them.
|
|
42
|
+
|
|
43
|
+
Small documentation fixes do not need extra tests. No CLA is required;
|
|
44
|
+
contributions are provided under this project's MIT license. Do not submit code
|
|
45
|
+
or data you do not have permission to share.
|
|
46
|
+
|
|
47
|
+
## Reporting and maintenance
|
|
48
|
+
|
|
49
|
+
Use [issues](https://github.com/Epsirom/braid/issues) for reproducible bugs,
|
|
50
|
+
questions, and feature proposals. Include package/Node/Pi versions, a minimal
|
|
51
|
+
graph, expected vs actual behavior, and redacted errors. Never include API keys,
|
|
52
|
+
private repository content, or a full model transcript unless safe to publish.
|
|
53
|
+
Report vulnerabilities through [SECURITY.md](SECURITY.md).
|
|
54
|
+
|
|
55
|
+
[Epsirom](https://github.com/Epsirom) maintains the project and reviews releases
|
|
56
|
+
and public API changes. Support is best effort, with no guaranteed response time.
|
|
57
|
+
For conduct concerns, contact the maintainer through a contact method listed on
|
|
58
|
+
their GitHub profile; use GitHub's Report abuse feature if private contact is
|
|
59
|
+
unavailable or the maintainer is involved. See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
|
|
60
|
+
|
|
61
|
+
Maintainers: follow the [release guide](docs/releasing.md).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Epsirom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
# Braid
|
|
2
|
+
|
|
3
|
+
Braid is a small execution runtime for dynamically constructed graphs of isolated
|
|
4
|
+
model invocations. A parent submits a complete DAG in one call; Braid resolves
|
|
5
|
+
routing and dependencies, runs independent nodes concurrently, and returns the
|
|
6
|
+
successful execution-terminal outputs. It is an agent primitive, not a workflow
|
|
7
|
+
builder.
|
|
8
|
+
|
|
9
|
+
[](https://github.com/Epsirom/braid/actions/workflows/ci.yml)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
**v0.1:** Experimental, TypeScript, Node.js 22+, ESM, no runtime dependencies.
|
|
13
|
+
The core has no Pi, provider SDK, or framework dependency.
|
|
14
|
+
|
|
15
|
+
## Why Braid?
|
|
16
|
+
|
|
17
|
+
For a code review, run correctness and test-coverage analysis independently,
|
|
18
|
+
then pass both results to a synthesis node. Add a decision when some tasks need
|
|
19
|
+
only a brief answer. Braid handles dependency readiness, conditional skips,
|
|
20
|
+
failed joins, per-node context, cancellation, and accounting around those calls.
|
|
21
|
+
|
|
22
|
+
```mermaid
|
|
23
|
+
flowchart LR
|
|
24
|
+
route{Choose depth} -->|detailed| correctness[Correctness]
|
|
25
|
+
route -->|detailed| tests[Test coverage]
|
|
26
|
+
correctness --> review[Final review]
|
|
27
|
+
tests --> review
|
|
28
|
+
route -->|brief| brief[Short answer]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use it when independent reasoning branches and explicit handoffs help. A direct
|
|
32
|
+
model call or `Promise.all` is enough for a simple answer or independent calls
|
|
33
|
+
without routing or joins. Braid adds no persistence, workflow editor, or agent
|
|
34
|
+
framework. [Runnable examples](docs/examples.md) show the tradeoffs.
|
|
35
|
+
|
|
36
|
+
## Install in your application
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npm install @chrok/braid
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Save this as `example.mjs` and run `node example.mjs` (no API key needed):
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { braid } from "@chrok/braid";
|
|
46
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
47
|
+
import { tmpdir } from "node:os";
|
|
48
|
+
import { join } from "node:path";
|
|
49
|
+
|
|
50
|
+
// A non-Git directory keeps this text-only example outside workspace management.
|
|
51
|
+
const cwd = await mkdtemp(join(tmpdir(), "braid-hello-"));
|
|
52
|
+
try {
|
|
53
|
+
const result = await braid({
|
|
54
|
+
goal: "Try one isolated invocation.",
|
|
55
|
+
nodes: [{ type: "execute", id: "answer", prompt: "Say hello." }],
|
|
56
|
+
edges: [],
|
|
57
|
+
}, {
|
|
58
|
+
cwd,
|
|
59
|
+
runner: async () => ({ output: "Hello from Braid." }),
|
|
60
|
+
});
|
|
61
|
+
console.log(result.terminalOutputs.answer.output);
|
|
62
|
+
// Hello from Braid.
|
|
63
|
+
} finally {
|
|
64
|
+
await rm(cwd, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For a live provider, use the adapter in the API example below. Installation and
|
|
69
|
+
running the example above do not make model requests. In a Git checkout, Braid
|
|
70
|
+
creates node worktrees and may append a merge agent that can integrate changes
|
|
71
|
+
into the source checkout. Read [workspace behavior](#worktrees-and-merge-agents)
|
|
72
|
+
before running a custom adapter against a repository.
|
|
73
|
+
|
|
74
|
+
## Install in Pi
|
|
75
|
+
|
|
76
|
+
With Pi 0.85.1 and Node.js 22.19+:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
pi install npm:@chrok/pi-braid
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Run `/reload`, ask Pi to analyze a task with Braid, and open `/braid` to inspect
|
|
83
|
+
the job. The extension includes the matching core runtime; no checkout is needed.
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
This snapshot uses the actual panel renderer and fake responses. See the
|
|
88
|
+
[Pi guide](integrations/pi/README.md) for background jobs, cancellation, and local
|
|
89
|
+
installation, and [compatibility](docs/compatibility.md) for the tested versions.
|
|
90
|
+
|
|
91
|
+
## Develop from source
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
git clone https://github.com/Epsirom/braid.git
|
|
95
|
+
cd braid
|
|
96
|
+
npm ci
|
|
97
|
+
npm run check
|
|
98
|
+
npm test
|
|
99
|
+
npm run build
|
|
100
|
+
npm run demo
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The demo uses a deterministic fake model runner and makes no network calls. To
|
|
104
|
+
run the same graph with an OpenAI-compatible Chat Completions endpoint:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
# Set OPENAI_API_KEY and BRAID_MODEL in your environment first.
|
|
108
|
+
npm run demo -- --live
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`OPENAI_BASE_URL` optionally changes the API root (for example,
|
|
112
|
+
`https://your-provider.example/v1`). Live mode makes billable model requests.
|
|
113
|
+
The adapter requires a model with function/tool calling support. No live provider
|
|
114
|
+
is needed for the test suite; its HTTP requests are intercepted in tests.
|
|
115
|
+
|
|
116
|
+
## API
|
|
117
|
+
|
|
118
|
+
After installing the package, submit the graph in one call;
|
|
119
|
+
configuration and the trusted provider adapter are separate from the graph data:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { braid } from "@chrok/braid";
|
|
123
|
+
import { createOpenAICompatibleRunner } from "@chrok/braid/adapters/openai";
|
|
124
|
+
|
|
125
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
126
|
+
const model = process.env.BRAID_MODEL;
|
|
127
|
+
if (!apiKey || !model) throw new Error("Set OPENAI_API_KEY and BRAID_MODEL");
|
|
128
|
+
|
|
129
|
+
const result = await braid({
|
|
130
|
+
goal: "Assess a proposed change and give a recommendation.",
|
|
131
|
+
nodes: [
|
|
132
|
+
{
|
|
133
|
+
type: "decision", id: "route", prompt: "Choose a brief answer or a detailed assessment.",
|
|
134
|
+
choices: ["brief", "detailed"], model: process.env.BRAID_ROUTER_MODEL ?? model,
|
|
135
|
+
},
|
|
136
|
+
{ type: "execute", id: "benefits", prompt: "Assess the potential benefits." },
|
|
137
|
+
{ type: "execute", id: "risks", prompt: "Assess the risks and unknowns." },
|
|
138
|
+
{ type: "execute", id: "answer", prompt: "Use the available context to give a recommendation." },
|
|
139
|
+
],
|
|
140
|
+
edges: [
|
|
141
|
+
{ from: "route", to: "answer", choice: "brief" },
|
|
142
|
+
{ from: "route", to: "benefits", choice: "detailed" },
|
|
143
|
+
{ from: "route", to: "risks", choice: "detailed" },
|
|
144
|
+
{ from: "benefits", to: "answer" },
|
|
145
|
+
{ from: "risks", to: "answer" },
|
|
146
|
+
],
|
|
147
|
+
}, {
|
|
148
|
+
runner: createOpenAICompatibleRunner({ apiKey }),
|
|
149
|
+
defaultModel: model,
|
|
150
|
+
maxConcurrency: 4,
|
|
151
|
+
nodeTimeoutMs: 60_000,
|
|
152
|
+
graphTimeoutMs: 300_000,
|
|
153
|
+
onEvent: event => console.log(`[${event.type}]`, event),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
console.log(result.status, result.terminalOutputs);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The detailed route starts `benefits` and `risks` concurrently. The brief route
|
|
160
|
+
skips both, propagates their inactivity, and runs `answer` with only `route`'s
|
|
161
|
+
output. The graph has no special fork, branch, or join nodes.
|
|
162
|
+
|
|
163
|
+
### Graph schema
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
type BraidNode =
|
|
167
|
+
| { type: "execute"; id: string; prompt: string; model?: string }
|
|
168
|
+
| { type: "decision"; id: string; prompt: string;
|
|
169
|
+
choices: readonly string[]; model?: string }
|
|
170
|
+
| { type: "merge"; id: string; prompt?: string; model?: string };
|
|
171
|
+
|
|
172
|
+
type Edge = { from: string; to: string; choice?: string };
|
|
173
|
+
type BraidInput = { goal: string; nodes: readonly BraidNode[]; edges: readonly Edge[] };
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
IDs are unique, non-empty strings. Prompts, goals, models, and choices must be
|
|
177
|
+
non-empty strings when present. Decision choices must be non-empty and unique.
|
|
178
|
+
Unknown fields, unsupported node types, missing references, duplicate exact
|
|
179
|
+
edges, and cycles are rejected. Cycles are rejected even if a decision might
|
|
180
|
+
make them inactive. Disconnected components are allowed; every root runs.
|
|
181
|
+
Distinct choices may connect the same node pair. Choices need not all have
|
|
182
|
+
outgoing edges, and decisions may themselves be terminal.
|
|
183
|
+
|
|
184
|
+
`validateGraph(input)` is also exported for validation without execution. It and
|
|
185
|
+
`braid` throw `GraphValidationError` for invalid graphs, before invoking a model.
|
|
186
|
+
Invalid runtime options throw `TypeError`. Execution failures return a result
|
|
187
|
+
with `status: "failed"` instead of discarding the run's successful outputs.
|
|
188
|
+
|
|
189
|
+
### Options
|
|
190
|
+
|
|
191
|
+
| Option | Default | Meaning |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
| `runner` | Required | A fresh, isolated invocation for each call |
|
|
194
|
+
| `defaultModel` | Adapter default | Overridden by each node's `model` |
|
|
195
|
+
| `cwd` | `process.cwd()` | Source checkout for core-managed worktrees; non-Git directories grant read-only capabilities |
|
|
196
|
+
| `maxConcurrency` | `4` | Maximum simultaneous runtime-managed node invocations; positive integer |
|
|
197
|
+
| `nodeTimeoutMs` | `60_000` | Separate deadline for each node, starting when it runs (not while queued) |
|
|
198
|
+
| `graphTimeoutMs` | `300_000` | Whole execution deadline, including node queueing; starts after validation |
|
|
199
|
+
| `signal` | None | Caller cancellation signal; aborts running nodes and marks queued nodes cancelled |
|
|
200
|
+
| `onEvent` | None | Live observer for graph/node creation, readiness, starts, handoffs, completions, skips, failures, and graph completion |
|
|
201
|
+
|
|
202
|
+
Timeouts must be positive finite milliseconds, at most `2_147_483_647`, or
|
|
203
|
+
`Infinity` to disable that deadline. Set both timeouts to `Infinity` to run
|
|
204
|
+
without a time limit; caller cancellation still works.
|
|
205
|
+
|
|
206
|
+
## Scheduling and routing semantics
|
|
207
|
+
|
|
208
|
+
Node states are explicit:
|
|
209
|
+
|
|
210
|
+
```text
|
|
211
|
+
pending -> runnable -> running -> completed | failed
|
|
212
|
+
pending -> skipped
|
|
213
|
+
pending | runnable -> skipped (graph timeout)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Edges are resolved from their source node's state:
|
|
217
|
+
|
|
218
|
+
| Source result | Outgoing edge state |
|
|
219
|
+
| --- | --- |
|
|
220
|
+
| Not yet finished | Unresolved |
|
|
221
|
+
| Completed, unlabelled edge | Active |
|
|
222
|
+
| Completed decision, matching choice | Active |
|
|
223
|
+
| Completed decision, nonmatching choice | Inactive |
|
|
224
|
+
| Skipped because all inputs were inactive | Inactive |
|
|
225
|
+
| Failed, unlabelled edge | Active (passes error and available output/workspace) |
|
|
226
|
+
| Failed decision, choice-labelled edge | Blocked |
|
|
227
|
+
| Skipped because of a failed dependency | Blocked |
|
|
228
|
+
|
|
229
|
+
Only decision nodes may have choice-labelled outgoing edges. Unlabelled edges
|
|
230
|
+
are unconditional, including edges from decisions. A single choice can activate
|
|
231
|
+
any number of edges. Choice routing requires successful decision completion.
|
|
232
|
+
A decision must call `decide` exactly once with exactly one declared choice;
|
|
233
|
+
missing, invalid, or repeated calls fail it. Catching a tool validation error
|
|
234
|
+
inside an adapter does not turn that invocation into a success.
|
|
235
|
+
|
|
236
|
+
A pending node waits until **all incoming edges are resolved**. Then:
|
|
237
|
+
|
|
238
|
+
1. Any blocked incoming edge makes it `skipped: upstream_failed`.
|
|
239
|
+
2. If it has incoming edges but none are active, it becomes `skipped: inactive`.
|
|
240
|
+
3. Otherwise it becomes `runnable` (including roots, which have no inputs).
|
|
241
|
+
|
|
242
|
+
Failures propagate as context through unconditional edges, allowing successors
|
|
243
|
+
and merge agents to inspect errors and recover partial work. Failed decisions
|
|
244
|
+
cannot activate choice-labelled edges; their unconditional successors can run.
|
|
245
|
+
Skip propagation uses topological order. There are no automatic retries. The
|
|
246
|
+
graph still reports failure if any node fails, even if a later node recovers its
|
|
247
|
+
work successfully.
|
|
248
|
+
|
|
249
|
+
## Execution events
|
|
250
|
+
|
|
251
|
+
The runtime keeps an immutable `events` log on every execution result and can
|
|
252
|
+
stream the same events through `options.onEvent`. Events are numbered and
|
|
253
|
+
timestamped. The log is diagnostic data and does not alter scheduling; observer
|
|
254
|
+
exceptions and rejected promises are ignored. Event payloads are frozen before
|
|
255
|
+
being retained and delivered.
|
|
256
|
+
|
|
257
|
+
The event sequence includes:
|
|
258
|
+
|
|
259
|
+
- `graph_created`, `node_created`, and `edge_created` when the submitted DAG is
|
|
260
|
+
admitted; an appended final merge emits its own node/edge creation events.
|
|
261
|
+
- `node_runnable` and `node_started` when scheduling admits a node.
|
|
262
|
+
- `workspace_updated` for Git workspace preparation, checkpointing, and cleanup.
|
|
263
|
+
- `handoff` for every direct predecessor output passed to a downstream node,
|
|
264
|
+
including the upstream decision when present.
|
|
265
|
+
- `node_completed`, `node_skipped`, and `node_failed`, including output,
|
|
266
|
+
decision, model, usage, latency, skip reason, or error where applicable.
|
|
267
|
+
A node start/completion/failure event is emitted only once the corresponding
|
|
268
|
+
transition is admitted; a provider call that expires before admission has no
|
|
269
|
+
`node_started` event.
|
|
270
|
+
- `graph_completed` with execution terminal IDs, or `graph_failed` with the
|
|
271
|
+
representative error and any successful terminal IDs.
|
|
272
|
+
|
|
273
|
+
`node_completed` and `node_failed` include node latency; `node_completed` also
|
|
274
|
+
includes the selected decision and reported usage when available. Event output
|
|
275
|
+
is diagnostic context and may be previewed by an adapter; `BraidResult.events`
|
|
276
|
+
retains the complete event payloads.
|
|
277
|
+
|
|
278
|
+
The core event stream is intentionally a log, not a second control API. It does
|
|
279
|
+
not permit graph mutation or runtime intervention. A Pi adapter can use it to
|
|
280
|
+
render live topology, handoffs, failures, and active nodes without reconstructing
|
|
281
|
+
scheduler state from final results. Tool selection remains the responsibility of
|
|
282
|
+
the host agent; the optional Pi adapter supplies explicit proactive-use guidance
|
|
283
|
+
so Braid is considered for complex multi-branch reasoning without forcing it for
|
|
284
|
+
every prompt. In the Pi adapter, Git nodes can inspect and edit individual
|
|
285
|
+
worktrees; nodes outside Git stay read-only. Merge agents handle integration; the parent reviews results and runs shell commands and tests.
|
|
286
|
+
|
|
287
|
+
## Context isolation and model runners
|
|
288
|
+
|
|
289
|
+
The core accepts a `ModelRunner` function with this contract:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import type { ModelRequest } from "@chrok/braid";
|
|
293
|
+
|
|
294
|
+
type ModelRunner = (request: ModelRequest) => Promise<{
|
|
295
|
+
output: string;
|
|
296
|
+
model?: string;
|
|
297
|
+
usage?: { inputTokens: number; outputTokens: number };
|
|
298
|
+
}>;
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Each request contains the goal, node, resolved model, predecessor outputs,
|
|
302
|
+
execution IDs, and an abort signal. Decision nodes additionally receive
|
|
303
|
+
`request.decide(choice)`. Expose that callback as an actual model tool; do not
|
|
304
|
+
infer decisions by parsing the model's prose. Core assigns `request.workspace`,
|
|
305
|
+
provides local `request.git(args, input?)` operations in Git repositories, and
|
|
306
|
+
provides `request.merge.sources` and `request.merge.finish(dispositions)` to
|
|
307
|
+
merge agents. Adapters must enforce workspace capabilities and wrap mutating
|
|
308
|
+
file tools in `request.withWorkspaceWrite(operation)`, so cleanup waits for
|
|
309
|
+
in-flight writes and rejects later writes. Core Git mutations use this barrier.
|
|
310
|
+
The included Pi adapter provides guarded `write`/`edit` alongside its read tools.
|
|
311
|
+
Outside Git, adapters must provide read-only capabilities. Pi never provides
|
|
312
|
+
`bash`, `powershell`, or a test runner to nodes.
|
|
313
|
+
|
|
314
|
+
`request.predecessors` contains direct active predecessors in incoming-edge
|
|
315
|
+
order, including failures on unconditional edges with an `error` field. Each source appears once:
|
|
316
|
+
|
|
317
|
+
```json
|
|
318
|
+
[{ "nodeId": "route", "output": "Use a detailed comparison.", "decision": "detailed", "model": "router" }]
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
There is no parent conversation, global transcript, or automatic transitive
|
|
322
|
+
history. Each invocation gets fresh node/context objects, so modifying them
|
|
323
|
+
cannot affect the graph, another node, or recorded results. Both the submitted
|
|
324
|
+
input and options are snapshotted before asynchronous execution. Core manages
|
|
325
|
+
worktrees for all adapters; adapters decide which tools expose those capabilities.
|
|
326
|
+
Read tools follow the host filesystem permissions and are not a security sandbox.
|
|
327
|
+
|
|
328
|
+
### Worktrees and merge agents
|
|
329
|
+
|
|
330
|
+
In a Git checkout, execute and decision nodes receive detached worktrees under
|
|
331
|
+
`os.tmpdir()/braid-workspaces-*/<unique-id>`. The first node captures tracked
|
|
332
|
+
staged/unstaged changes, deletions, and non-ignored untracked files with a temporary
|
|
333
|
+
index. Snapshot creation preserves the source index, branch, and files. Ignored
|
|
334
|
+
files are not copied, and submodules are not initialized or recursively captured.
|
|
335
|
+
Pi rejects writes inside submodules. If a custom runner populates one, core
|
|
336
|
+
reports cleanup failure and retains the worktree rather than losing those files.
|
|
337
|
+
Empty repositories are supported. Nodes share this baseline until a merge ends;
|
|
338
|
+
subsequent nodes snapshot the current source checkout. Relative working directories
|
|
339
|
+
are preserved. Code changes do not implicitly flow into successor worktrees.
|
|
340
|
+
|
|
341
|
+
Add `{ type: "merge", id: "integrate" }` with incoming edges from any number of
|
|
342
|
+
sources. The merge agent receives predecessor errors, workspace paths, and Git
|
|
343
|
+
checkpoint refs and operates directly in the invoking checkout. **Core does not
|
|
344
|
+
run merge, cherry-pick, or apply automatically.** The agent reviews each source,
|
|
345
|
+
chooses which changes to integrate and how, resolves conflicts, then calls the
|
|
346
|
+
`finish_merge` tool with exactly one disposition and reason per source:
|
|
347
|
+
|
|
348
|
+
```json
|
|
349
|
+
{ "dispositions": [
|
|
350
|
+
{ "nodeId": "implementation", "disposition": "integrated", "reason": "Cherry-picked the reviewed checkpoint" },
|
|
351
|
+
{ "nodeId": "alternative", "disposition": "discarded", "reason": "The selected implementation supersedes this alternative" }
|
|
352
|
+
] }
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Each merge request includes bounded changed-file lists, diff statistics and diff
|
|
356
|
+
previews in `mergeSources[].changes`, plus the invoking checkout's dirty status.
|
|
357
|
+
These are inspection aids; the agent still chooses every integration operation.
|
|
358
|
+
The `finish_merge` schema lists only the current source IDs and requires exactly
|
|
359
|
+
one decision per source. Invalid calls report missing, unexpected and duplicate
|
|
360
|
+
IDs so the agent can correct the call.
|
|
361
|
+
|
|
362
|
+
The model-facing `git` tool takes a `command` from the node's allowed command
|
|
363
|
+
enum and a separate `args` array. For example, `{"command":"show","args":["REF:path"]}`.
|
|
364
|
+
The programmatic `ModelRequest.git` API continues to accept the complete argument
|
|
365
|
+
array. Rejected commands include relevant supported alternatives; Braid never
|
|
366
|
+
silently substitutes a different Git operation. A first argument identical to
|
|
367
|
+
`command` is rejected before execution: `{ "command": "status", "args": ["status"] }`
|
|
368
|
+
would otherwise silently query a path named `status`. Use `args: ["--short"]`
|
|
369
|
+
for the full status, or `args: ["--", "status"]` for an intentional path filter;
|
|
370
|
+
same-named branches can use a full ref such as `refs/heads/diff`.
|
|
371
|
+
|
|
372
|
+
`archived` means integration failed. A missing finish call, unresolved conflicts,
|
|
373
|
+
or any archived source fails the merge node. Once the agent ends, core removes
|
|
374
|
+
its predecessor worktrees. Every removed source retains a checkpoint ref,
|
|
375
|
+
including intentionally discarded changes and ignored node output files. A
|
|
376
|
+
later consumer can inspect a removed source through `git show <checkpointRef>`.
|
|
377
|
+
Core also records `backupRef` for the source checkout before each merge agent.
|
|
378
|
+
|
|
379
|
+
When declared nodes settle and worktrees remain, core appends an ordinary merge
|
|
380
|
+
agent named `__braid_merge__` (with a suffix if needed), using the run's default
|
|
381
|
+
model. It appears in results, events, usage, and terminal outputs. Merge nodes
|
|
382
|
+
are exclusive within a graph and serialized per source checkout across runs in
|
|
383
|
+
the same process. Avoid concurrent external edits to that checkout while merging;
|
|
384
|
+
this lock does not coordinate other processes or the parent editor.
|
|
385
|
+
|
|
386
|
+
Cancellation or graph timeout prevents new merge agents from starting. Core waits
|
|
387
|
+
for tracked writes, archives remaining work, and removes its worktrees. Merge
|
|
388
|
+
agent failure follows the same archive/cleanup path. It does not reset the source
|
|
389
|
+
checkout: partial integration or Git conflict state may remain for review, with
|
|
390
|
+
`backupRef` available for recovery. Filesystem/Git cleanup errors are reported as
|
|
391
|
+
`CLEANUP_FAILED` with retained workspace paths; a process crash cannot run cleanup.
|
|
392
|
+
|
|
393
|
+
`result.workspaces` and `node.workspace` report paths, states, reasons, and refs.
|
|
394
|
+
A cleaned worktree path is historical; use `checkpointRef` to recover its contents:
|
|
395
|
+
|
|
396
|
+
```sh
|
|
397
|
+
git show <checkpointRef>:path/to/file
|
|
398
|
+
git diff <snapshotCommit> <checkpointRef>
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Recovery refs live under `refs/braid/checkpoints/` and `refs/braid/merge-backups/`.
|
|
402
|
+
After reviewing them, remove a particular ref with `git update-ref -d <ref>`.
|
|
403
|
+
They preserve recoverable Git objects without retaining worktree directories.
|
|
404
|
+
|
|
405
|
+
**The adapter is a trust boundary, not a security sandbox.** It must avoid shared
|
|
406
|
+
conversation state, expose only its declared capabilities, and forward `signal` to its provider.
|
|
407
|
+
The core never gives the model arbitrary code execution or a recursive Braid
|
|
408
|
+
tool. An optional Pi adapter translates this same contract without changing the
|
|
409
|
+
runtime; the core v0.1 package does not depend on Pi. See
|
|
410
|
+
[`integrations/pi/README.md`](integrations/pi/README.md) for installation and testing.
|
|
411
|
+
|
|
412
|
+
The included OpenAI-compatible adapter uses fresh Chat Completions contexts,
|
|
413
|
+
a strict `decide({ choice })` tool and one tool-free continuation for decisions.
|
|
414
|
+
Merge nodes use a local `git` / `finish_merge` tool loop; ordinary OpenAI nodes
|
|
415
|
+
have no filesystem tools. Pi exposes read and guarded write tools plus these
|
|
416
|
+
core Git/merge tools. Tool errors go back to merge agents for recovery. Both
|
|
417
|
+
adapters sum usage across their model calls and forward cancellation.
|
|
418
|
+
Pi writes reject external paths, Git metadata, symlinks, hard links, and special
|
|
419
|
+
files. These checks are not an OS sandbox against concurrent filesystem attacks.
|
|
420
|
+
See the [Pi filesystem capabilities](integrations/pi/README.md#node-filesystem-capabilities).
|
|
421
|
+
|
|
422
|
+
Pi tool and time budgets are unlimited by default. Its `options.maxToolRounds`
|
|
423
|
+
and `options.maxToolCalls` can impose positive integer limits per node; counts
|
|
424
|
+
include `decide` and rejected requests. Exceeding either limit fails the node
|
|
425
|
+
before executing the over-budget batch. Returning final text at the limit is
|
|
426
|
+
allowed. See the [Pi budget options](integrations/pi/README.md#node-filesystem-capabilities)
|
|
427
|
+
for configuration, including `nodeTimeoutMs` and `graphTimeoutMs`.
|
|
428
|
+
|
|
429
|
+
For finite budgets, Pi inserts a system reminder with remaining tool and time
|
|
430
|
+
budgets before every model call. The OpenAI-compatible adapter also refreshes
|
|
431
|
+
finite time-budget reminders before each request. The core supplies optional
|
|
432
|
+
`request.deadlines` on the `performance.now()` clock for adapters to calculate
|
|
433
|
+
remaining node and shared graph time. Reminders do not extend hard limits or
|
|
434
|
+
interrupt a model response already in progress. The core API's default timeouts
|
|
435
|
+
remain 60 seconds per node and 5 minutes per graph.
|
|
436
|
+
|
|
437
|
+
## Results, timeouts, and accounting
|
|
438
|
+
|
|
439
|
+
`BraidResult` contains:
|
|
440
|
+
|
|
441
|
+
- `status`: `completed` or `failed`.
|
|
442
|
+
- `terminalOutputs`: `{ [nodeId]: { output, decision?, model? } }` for completed
|
|
443
|
+
nodes with **no active outgoing edges in this execution**. This includes a
|
|
444
|
+
decision selecting a choice with no successor. A completed node does not
|
|
445
|
+
become terminal merely because its active successor failed or was skipped.
|
|
446
|
+
- `nodes`: all node states plus available output, decision, model, usage, error,
|
|
447
|
+
skip reason, start/end timestamps (Unix milliseconds), and latency in ms.
|
|
448
|
+
Skipped nodes have no start time or latency. Nodes without a valid
|
|
449
|
+
response have no output. A failed decision may retain its text and selected
|
|
450
|
+
choice for debugging; choice-labelled edges still remain blocked.
|
|
451
|
+
- `workspaces`: Git workspace states and recovery refs, including cleaned sources.
|
|
452
|
+
- `events`: the immutable execution log described above. `onEvent` observes live
|
|
453
|
+
copies of the same state transitions while the run is in progress.
|
|
454
|
+
- `metadata`: run/root identity, timestamps, monotonic latency, summed reported
|
|
455
|
+
token usage, and `usageReportedNodes`. Missing usage is unknown, not proof of
|
|
456
|
+
zero consumption. Usage counts only what the runner actually returns; a
|
|
457
|
+
timeout or provider error may leave billable usage unavailable.
|
|
458
|
+
- `error`: one representative error when failed; `nodes` retains all errors.
|
|
459
|
+
|
|
460
|
+
A node timeout marks the running node `failed: NODE_TIMEOUT`; unconditional
|
|
461
|
+
successors can consume its error and partial workspace. A graph timeout marks running nodes `failed: GRAPH_TIMEOUT`,
|
|
462
|
+
skips queued/pending nodes with `graph_timeout`, and retains already completed
|
|
463
|
+
terminal outputs. Timers are cleared when no longer needed.
|
|
464
|
+
|
|
465
|
+
Timeouts abort the invocation signal and stop waiting for the model even if it
|
|
466
|
+
ignores cancellation. Workspace setup, tracked writes, and cleanup are awaited
|
|
467
|
+
to avoid deleting work still being written; this may extend total wall time. Late settlements cannot change the returned results, and late
|
|
468
|
+
rejections are observed. JavaScript cannot forcibly preempt synchronous work or
|
|
469
|
+
stop an uncooperative remote request: providers must honor cancellation to stop
|
|
470
|
+
resource consumption. Concurrency limits cover runtime-managed invocations;
|
|
471
|
+
uncancelled provider work after timeout can outlive a slot.
|
|
472
|
+
|
|
473
|
+
## Internal architecture and scope
|
|
474
|
+
|
|
475
|
+
- [`src/types.ts`](src/types.ts): public graph, provider, and result types.
|
|
476
|
+
- [`src/validate.ts`](src/validate.ts): strict validation, graph snapshot,
|
|
477
|
+
dependency indexes, and iterative DAG validation.
|
|
478
|
+
- [`src/runtime.ts`](src/runtime.ts): edge resolution, explicit state transitions,
|
|
479
|
+
bounded concurrent scheduling, invocation deadlines, execution events, and result accounting.
|
|
480
|
+
- [`src/workspaces.ts`](src/workspaces.ts): Git snapshots, checkpoint refs, merge
|
|
481
|
+
serialization, local Git tools, and worktree cleanup.
|
|
482
|
+
- [`src/adapters/openai.ts`](src/adapters/openai.ts): optional provider translation.
|
|
483
|
+
- [`integrations/pi/`](integrations/pi/): thin Pi model-registry/tool adapter, Mermaid graph renderer, and live execution renderer (`display.ts`).
|
|
484
|
+
- [`test/`](test/): deterministic scheduling, execution-event, and intercepted HTTP/tool tests.
|
|
485
|
+
|
|
486
|
+
There is one in-memory execution context per run, plus a process-local mutex
|
|
487
|
+
per source checkout for merge agents. Worktree registration and removal are
|
|
488
|
+
serialized per common Git directory within the process; model calls remain
|
|
489
|
+
concurrent. These locks do not coordinate other processes. `rootRunId` equals
|
|
490
|
+
`runId` in v0.1. Centralized invocation admission and
|
|
491
|
+
usage aggregation leave places to thread a shared root budget in a future
|
|
492
|
+
nested-run implementation; **nested runs and shared budget enforcement are not
|
|
493
|
+
implemented**. The current scheduler deliberately rescans a small DAG after
|
|
494
|
+
completions; `onEvent` is an observer for diagnostics and visualization, not a
|
|
495
|
+
scheduler event bus.
|
|
496
|
+
|
|
497
|
+
Out of scope: loops, arbitrary code nodes, persistent workflows, saved templates,
|
|
498
|
+
resuming saved runs, human approval, editing UI, user-directed graph mutation,
|
|
499
|
+
and recursive Braid calls from model nodes.
|
|
500
|
+
|
|
501
|
+
## Contributing and project status
|
|
502
|
+
|
|
503
|
+
Start with [CONTRIBUTING.md](CONTRIBUTING.md) for setup and verification,
|
|
504
|
+
[ROADMAP.md](ROADMAP.md) for scope, and [CHANGELOG.md](CHANGELOG.md) for changes.
|
|
505
|
+
See [compatibility](docs/compatibility.md), [resource limits](docs/resource-limits.md),
|
|
506
|
+
and [scheduler benchmarks](docs/benchmark.md) before adopting Braid for a service.
|
|
507
|
+
Questions and bugs belong in [GitHub issues](https://github.com/Epsirom/braid/issues);
|
|
508
|
+
report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
|
|
509
|
+
Participation follows the [code of conduct](CODE_OF_CONDUCT.md).
|
|
510
|
+
|
|
511
|
+
## License
|
|
512
|
+
|
|
513
|
+
Braid is released under the [MIT License](LICENSE).
|