@chrok/braid 0.1.3 → 0.2.1

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,160 @@
1
+ # Execution control
2
+
3
+ Braid 0.2 separates mutable node definitions from execution instances. This is a
4
+ breaking change from 0.1, covering bounded loops (#30) and live graph updates (#31).
5
+
6
+ ## Definitions and instances
7
+
8
+ A node is a reusable definition identified by `id`. An execution has a unique
9
+ `executionId`, a captured node definition, graph `revision`, predecessor execution
10
+ IDs, and, within a loop, `loopId` and a one-based `iteration`. Editing or deleting
11
+ a definition never changes or cancels an already admitted execution. Failure,
12
+ notification, pause, model, prompt, and workspace policies are captured at admission.
13
+
14
+ `result.executions` retains every instance, including earlier rounds and deleted
15
+ nodes. `result.nodes` is a convenience projection of the latest instance per node.
16
+ Workspace records are keyed by execution ID. `terminalExecutionIds` identifies
17
+ successful instances whose outputs were not consumed by another admitted instance;
18
+ `terminalOutputs` provides their latest outputs keyed by node ID. Failed instances
19
+ remain available separately, even when the overall job completes.
20
+
21
+ ## Structured loops
22
+
23
+ ```ts
24
+ const graph = {
25
+ goal: "Refine until the review passes",
26
+ nodes: [
27
+ { type: "execute", id: "edit", prompt: "Implement the feedback." },
28
+ { type: "decision", id: "review", prompt: "Review the result.",
29
+ choices: ["again", "done"], workspace: "read-only" },
30
+ { type: "integrate", id: "apply" },
31
+ ],
32
+ loops: [{ id: "refinement", entry: "edit", maxIterations: 3 }],
33
+ edges: [
34
+ { from: "edit", to: "review" },
35
+ { from: "review", to: "edit", choice: "again", feedback: "refinement" },
36
+ { from: "review", to: "apply", choice: "done" },
37
+ ],
38
+ };
39
+ ```
40
+
41
+ Each loop declares one entry, one decision feedback edge, and a finite positive
42
+ iteration limit. Ignoring feedback and historical references, the graph must be
43
+ acyclic. Loop bodies may fork and join; external edges enter only through the
44
+ entry and leave only through the feedback decision. Nested/overlapping loops and
45
+ arbitrary cycles are rejected. Sequential and independent loops are supported.
46
+
47
+ The first round uses external predecessors or the job's root snapshot. A feedback
48
+ choice creates another round only after all admitted instances in the current
49
+ round have settled and their pause gates have been released. The next entry also
50
+ receives the previous feedback execution. External inputs remain available in
51
+ subsequent rounds. Exits wait for the loop to finish and use the final round.
52
+ Choosing feedback at `maxIterations` fails the job with `LOOP_LIMIT`.
53
+
54
+ Loops do not overlap their own rounds. Live updates can change definitions,
55
+ edges, and loop topology during an iteration; no boundary pause is required.
56
+ Already admitted instances drain normally. Updates do not replay a completed
57
+ activation just because its definition or incoming edges changed. Use a new node
58
+ ID for new work outside a subsequent loop round.
59
+
60
+ ## Live updates and gates
61
+
62
+ ```ts
63
+ import { startBraid } from "@chrok/braid";
64
+
65
+ const run = startBraid({
66
+ goal: "Inspect, then decide the next steps",
67
+ nodes: [{ type: "execute", id: "inspect", prompt: "Find the problem.", pauseAfter: true }],
68
+ edges: [],
69
+ }, { runner });
70
+
71
+ // After execution_paused (or after obtaining a snapshot from your host):
72
+ const state = run.snapshot();
73
+ const executionId = state.pausedExecutionIds[0];
74
+ run.update({
75
+ expectedRevision: state.revision,
76
+ upsertNodes: [{ type: "execute", id: "fix", prompt: "Implement the fix." }],
77
+ addEdges: [{ from: "inspect", to: "fix", executionId }],
78
+ resume: [executionId],
79
+ });
80
+ const result = await run.result;
81
+ ```
82
+
83
+ `startBraid` returns synchronously with `runId`, `result`, `snapshot()`,
84
+ `update(patch)`, `resume(executionIds, expectedRevision)`, and `cancel()`.
85
+ `braid(input, options)` is the convenience API for awaiting the result.
86
+
87
+ Updates require the current `expectedRevision` and commit atomically. A successful
88
+ update increments the graph revision once. A stale revision or invalid candidate
89
+ changes nothing, including pause gates. `upsertNodes` replaces whole definitions;
90
+ `removeNodeIds` also removes incident edges. `addEdges` and `removeEdges` use full
91
+ edge identities, including choice, feedback, and execution ID. `promptTemplates`
92
+ and `loops` replace their respective collections when supplied. Template edits
93
+ are rendered for future admissions; running prompts remain unchanged.
94
+
95
+ Completion routes through the latest graph. If A is running and A→B is replaced
96
+ with A→C, A's completion can admit C. A previously admitted B continues with its
97
+ captured inputs. A historical dependency pins `edge.executionId` and matching
98
+ `from` to a settled execution, even if its definition was removed. Initial
99
+ submissions cannot reference history from another job.
100
+
101
+ `pauseAfter` holds an execution's outgoing scheduling and keeps the job alive,
102
+ even at a leaf. Independent branches continue. Pi sends a pause reminder;
103
+ `notifyOnCompletion` alone only sends a reminder. Resume specific execution IDs,
104
+ either on its own or in the same transaction as a graph update. Removing a paused
105
+ node definition does not discard its gate: explicitly resume or cancel it.
106
+
107
+ A job accepts updates while running or waiting. Once no work or gates remain,
108
+ it enters finalization and rejects further updates. Completed jobs cannot be
109
+ reopened. These are in-memory controls; there is no restart or crash recovery of
110
+ the scheduler itself.
111
+
112
+ ## Workspaces and merge operations
113
+
114
+ Every execution gets a new Git worktree, including read-only executions and
115
+ later loop visits. Root worktrees derive from one initial snapshot; successors
116
+ derive from predecessor checkpoints. Read-only is a capability restriction,
117
+ not a live view of the caller. Outside Git, filesystem tools remain read-only.
118
+
119
+ If inputs have identical trees, or one checkpoint contains all the others in its
120
+ ancestry, an ordinary worker can use that snapshot. Independent changed inputs
121
+ require an explicit `merge`; the worker fails with `WORKSPACE_MERGE_REQUIRED`
122
+ instead of silently selecting or combining them. All active predecessors still
123
+ provide text/error context.
124
+
125
+ | Type | Target | Result |
126
+ | --- | --- | --- |
127
+ | `merge` | New isolated worktree based on the initial job snapshot | Selected predecessor changes combined into a reusable checkpoint |
128
+ | `integrate` | Invoking source checkout / working branch | Selected changes applied to the caller and captured as a checkpoint |
129
+
130
+ Neither operation applies changes automatically. The runner chooses Git/file
131
+ operations, resolves conflicts, and calls `finish_merge` once with one
132
+ `{ executionId, disposition, reason }` per source. Sources are immutable and
133
+ reusable by multiple consumers. Dispositions are recorded on the target
134
+ workspace; source worktrees remain available until job cleanup. Integration
135
+ captures a `backupRef` first and serializes source-checkout access within the
136
+ process. It must preserve unrelated staged, unstaged, and untracked caller edits.
137
+
138
+ There is no implicit final integration. Each instance is checkpointed before
139
+ successors are released, including partial work on optional failure. Finalization
140
+ archives/removes owned worktrees while retaining checkpoint refs. Cancelled or
141
+ failed integration may leave partial source edits or conflicts for inspection;
142
+ it does not reset the caller's checkout.
143
+
144
+ ## Failure and budgets
145
+
146
+ `requireSuccess` defaults to false. Optional failures keep their errors and partial
147
+ artifacts; unconditional successors can recover. A failed decision never activates
148
+ a choice edge. A job can complete with failed optional executions.
149
+
150
+ A captured `requireSuccess: true` failure immediately stops admission, aborts
151
+ running siblings, waits for tracked writes and cleanup, and fails the job with
152
+ `REQUIRED_NODE_FAILED`. Editing the definition's policy later has no effect on
153
+ that instance. Infrastructure, checkpoint/cleanup, cancellation, graph deadline,
154
+ loop limit, and total execution limit failures always fail the job.
155
+
156
+ `maxExecutions` defaults to 1000 and must be a positive safe integer. It bounds
157
+ all materialized execution records, including skipped branches, across all rounds
158
+ and graph updates. `maxIterations` bounds each loop. The graph deadline includes
159
+ waiting at gates and is never reset by an update or resume. Usage sums all
160
+ instances once; Pi only claims job usage on the first terminal job retrieval.
package/docs/releasing.md CHANGED
@@ -4,6 +4,34 @@ Core and Pi are separate public npm packages built from one commit. Both use the
4
4
  same version. The core has no runtime dependencies. Pi declares an exact
5
5
  `@chrok/braid` dependency and includes only its own compiled integration code.
6
6
 
7
+ ## Registry and source association
8
+
9
+ The public packages are [@chrok/braid](https://www.npmjs.com/package/@chrok/braid)
10
+ and [@chrok/pi-braid](https://www.npmjs.com/package/@chrok/pi-braid) on npmjs.
11
+ Keep `publishConfig.registry` set to `https://registry.npmjs.org`. Both manifests
12
+ link to `Epsirom/braid`; Pi's `repository.directory` is `integrations/pi`.
13
+ The repository About website and README badges provide the reverse links.
14
+ GitHub Packages is a separate registry and is not a mirror in this release flow.
15
+
16
+ Package descriptions, keywords, source links, and README content are uploaded
17
+ with the package release. A commit on `main` does not update the published npm
18
+ metadata. Keep development-version notices accurate until publication, and link
19
+ users of the current npm release to its tagged documentation.
20
+
21
+ To inspect the public release without npm account credentials:
22
+
23
+ ```sh
24
+ npm view @chrok/braid dist-tags description repository homepage gitHead dist.attestations --json
25
+ npm view @chrok/pi-braid dist-tags description repository homepage gitHead dist.attestations --json
26
+ ```
27
+
28
+ For a release, query the exact `@X.Y.Z` versions too. Confirm both `gitHead`
29
+ values match the release commit, the source links point to this repository, and
30
+ provenance identifies this repository's release workflow. The successful
31
+ [Publish runs](https://github.com/Epsirom/braid/actions/workflows/release.yml)
32
+ and public npm attestations provide release evidence; inspecting or changing
33
+ the trusted-publisher account settings requires npm authentication.
34
+
7
35
  ## Prepare a release
8
36
 
9
37
  Version tags (`v*`) cannot be moved or deleted. New GitHub releases are immutable:
@@ -18,10 +46,47 @@ corrections require a new version. See [repository settings](repository-settings
18
46
  licenses, and Pi registration in a temporary consumer outside the checkout.
19
47
  A temporary local registry serves the unpublished core tarball; installing
20
48
  only the Pi tarball must fetch core transitively through its version dependency.
21
- 3. Update the changelog and supported Pi version. Commit and review the changes;
49
+ 3. Update the changelog, migration guidance, package descriptions/keywords, and
50
+ supported Pi version. Reconcile README/Pi development notices and ROADMAP
51
+ status with the release being prepared; mark publication complete only after
52
+ both packages are available. Commit and review the changes;
22
53
  require the CI matrix to pass before tagging that commit `vX.Y.Z`.
23
54
  4. Inspect `npm pack --dry-run` and `npm pack --dry-run` from `integrations/pi`.
24
55
  `prepack` rebuilds each package. Never publish stale prebuilt output.
56
+ 5. Prepare the GitHub release draft using the release-note policy below. After
57
+ publishing, verify both registry versions, `latest` tags, provenance, and a
58
+ clean install before reporting the release complete.
59
+
60
+ ## Release notes and contributor credit
61
+
62
+ Every release must describe user-visible features, fixes, and breaking changes
63
+ in both `CHANGELOG.md` and the GitHub release notes. For each item, link the
64
+ implementing pull request and credit its author by GitHub handle, for example:
65
+ `Add graph-local prompt templates ([#33](https://github.com/Epsirom/braid/pull/33)) — @Epsirom.`
66
+ Use the PR author, not the person who merged it. Issue links can add context but
67
+ do not replace PR links. Credit each relevant PR/author when an item combines
68
+ several contributions; credit direct-commit authors with commit links if no PR
69
+ exists. Keep dependency and release maintenance separate from feature summaries.
70
+
71
+ Compare the previous release tag with the exact candidate commit. Inspect the
72
+ merged PRs in that range and their authors; GitHub's generated release notes
73
+ are a useful starting point, not a substitute for checking the actual changes.
74
+ Keep the changelog and GitHub notes consistent, include migration guidance for
75
+ breaking changes, and link the full tag-to-tag comparison.
76
+
77
+ Mark a human author's first contribution to this repository with
78
+ `**First-time contributor**` next to their credit. Also add a **New Contributors**
79
+ section that thanks them and links their first included PR (or direct commit).
80
+ Verify this against all earlier merged PRs and commit history through the
81
+ previous release, not just the latest release notes. An existing contributor's
82
+ first PR in this release is not their first repository contribution. Exclude
83
+ bot accounts from newcomer thanks, while retaining their maintenance credits.
84
+ If there are no first-time human contributors, say so in that section; do not
85
+ infer newcomer status from a missing credit in an older release.
86
+
87
+ Before publishing the draft, check that every feature/fix has its implementing
88
+ PR link and author, newcomer labels match the history, and version, date,
89
+ comparison, and migration links refer to the release being published.
25
90
 
26
91
  ## First publication
27
92
 
@@ -6,6 +6,40 @@ copies of the API payloads; committing a change to them does not apply it to
6
6
  GitHub automatically. Update the existing ruleset in Settings or through the
7
7
  REST API after reviewing a policy change, then verify the live settings.
8
8
 
9
+ ## Repository identity and npm links
10
+
11
+ The GitHub About section describes the current `main` capabilities:
12
+
13
+ > TypeScript runtime for LLM agent graphs with bounded loops, live updates,
14
+ > isolated Git worktrees, and explicit integration. Framework-agnostic core,
15
+ > OpenAI-compatible runner, and Pi extension.
16
+
17
+ The repository website points to
18
+ [@chrok/braid on npm](https://www.npmjs.com/package/@chrok/braid). The root README
19
+ links both published packages and displays their npm version badges. Topics are
20
+ `llm`, `ai-agents`, `agent-runtime`, `agent-orchestration`, `multi-agent`,
21
+ `graph-execution`, `bounded-loops`, `git-worktree`, `parallel-execution`,
22
+ `typescript`, `nodejs`, `openai-compatible`, and `pi-package`.
23
+ The old DAG-only and workflow-engine labels do not describe the 0.2 scope.
24
+
25
+ Both packages publish to `https://registry.npmjs.org`:
26
+
27
+ | Package | Repository location |
28
+ | --- | --- |
29
+ | [@chrok/braid](https://www.npmjs.com/package/@chrok/braid) | Repository root |
30
+ | [@chrok/pi-braid](https://www.npmjs.com/package/@chrok/pi-braid) | `integrations/pi` |
31
+
32
+ Each manifest declares `repository`, `homepage`, and `bugs`; Pi additionally sets
33
+ `repository.directory`. These fields link npm pages back to the correct source
34
+ and issue tracker. Descriptions and keywords take effect on npm when a new
35
+ version is published; editing `main` does not change existing registry versions.
36
+
37
+ GitHub's **Packages** section represents its separate registry. It does not list
38
+ an npmjs package just because that package points to this repository. The project
39
+ uses npmjs only; no GitHub Packages mirror or additional package scope is
40
+ maintained. See [GitHub's npm registry guide](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry)
41
+ and the [release guide](releasing.md#registry-and-source-association).
42
+
9
43
  ## Main branch
10
44
 
11
45
  [Protect main](https://github.com/Epsirom/braid/rules/24072177) applies to `main`,
@@ -1,16 +1,24 @@
1
1
  # Resource limits and deployment responsibility
2
2
 
3
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.
4
+ size, and the number of simultaneous graph submissions have no hard cap. Total materialized executions, including skipped branches,
5
+ are capped at 1000 by default; loops also require a finite iteration limit.
5
6
  The scheduler rescans the graph as work settles. Consult the
6
7
  [benchmark](benchmark.md) for measured local overhead rather than treating the
7
8
  validator's deep-graph tests as a production capacity guarantee.
8
9
 
10
+ Submission-local prompt templates reduce graph/tool-call argument size, not
11
+ worker context size. Core expands them before execution and checks rendered
12
+ prompts are non-empty. There is no raw or rendered prompt length/token cap;
13
+ hosts imposing their own size budgets must account for expanded prompts as
14
+ well as the compact definition.
15
+
9
16
  | Control | Core default | Pi default |
10
17
  | --- | --- | --- |
11
18
  | Active runtime-managed invocations per graph | 4 | 4 |
19
+ | Materialized executions across loops/updates | 1000 (`maxExecutions`) | 1000 (`maxExecutions`) |
12
20
  | Node timeout (starts at admission) | 60 seconds | Unlimited |
13
- | Graph timeout (includes queueing) | 5 minutes | Unlimited |
21
+ | Graph timeout (includes queueing and paused gates) | 5 minutes | Unlimited |
14
22
  | Caller cancellation | `AbortSignal` | `braid_cancel` / panel `c` |
15
23
  | Worker tool loop | Decision continuation / merge Git-tool loop, bounded by deadlines | Unlimited by default; optional `maxToolRounds` and `maxToolCalls` |
16
24
  | Result preview | Full result | 50 KB / 2,000 lines, then temporary full-result file |
@@ -43,9 +51,9 @@ after exporting needed results. Reloading also cancels outstanding work.
43
51
 
44
52
  Git worktree preparation, checkpointing, tracked writes, and cleanup add disk,
45
53
  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; unchanged worktrees are released without
48
- an automatic merge call, but still incur workspace and checkpoint overhead.
54
+ model deadline. Every loop visit creates a new invocation and worktree. Explicit merge/integrate
55
+ nodes count toward the same limits; no final integration call is appended.
56
+ Updates/resumes never reset the graph deadline or execution counter.
49
57
  Benchmark results measured outside Git do not include this lifecycle. Recoverable
50
58
  refs retain Git objects until removed.
51
59
 
@@ -0,0 +1,38 @@
1
+ import assert from "node:assert/strict";
2
+ import { startBraid, type BraidRun } from "../src/index.js";
3
+ import { inTemporaryDirectory } from "./support.js";
4
+
5
+ await inTemporaryDirectory(async cwd => {
6
+ let run!: BraidRun;
7
+ run = startBraid({
8
+ goal: "Refine a proposal, then add the next step using its exact result.",
9
+ nodes: [
10
+ { type: "execute", id: "draft", prompt: "Refine the proposal." },
11
+ { type: "decision", id: "review", prompt: "Review it.", choices: ["again", "done"], pauseAfter: true },
12
+ ],
13
+ loops: [{ id: "refinement", entry: "draft", maxIterations: 2 }],
14
+ edges: [{ from: "draft", to: "review" }, { from: "review", to: "draft", choice: "again", feedback: "refinement" }],
15
+ }, {
16
+ cwd,
17
+ runner: async request => {
18
+ request.decide?.(request.execution.iteration === 2 ? "done" : "again");
19
+ return { output: `${request.node.id}: iteration ${request.execution.iteration ?? "outside loop"}` };
20
+ },
21
+ onEvent: event => {
22
+ if (event.type !== "execution_paused") return;
23
+ if (event.iteration === 1) run.resume([event.executionId!], run.snapshot().revision);
24
+ else run.update({
25
+ expectedRevision: run.snapshot().revision,
26
+ upsertNodes: [{ type: "execute", id: "next", prompt: "Summarize the accepted proposal." }],
27
+ addEdges: [{ from: "review", to: "next", executionId: event.executionId! }],
28
+ resume: [event.executionId!],
29
+ });
30
+ },
31
+ });
32
+ const result = await run.result;
33
+ assert.equal(result.status, "completed");
34
+ assert.equal(Object.keys(result.executions).length, 5);
35
+ assert.equal(result.revision, 1);
36
+ console.log(`${Object.keys(result.executions).length} executions; revision ${result.revision}`);
37
+ console.log(result.terminalOutputs.next!.output);
38
+ });
@@ -20,10 +20,10 @@ const result = await inTemporaryDirectory(cwd => braid({
20
20
  return { output: "Known local facts are still available." };
21
21
  },
22
22
  }));
23
- assert.equal(result.status, "failed");
23
+ assert.equal(result.status, "completed");
24
24
  assert.equal(result.nodes.join!.status, "completed");
25
25
  assert.match(result.terminalOutputs.join!.output, /remote: unavailable \(MODEL_ERROR\)/);
26
26
  // Unconditional successors receive failed predecessors as explicit error context.
27
- // A recovered answer does not erase the graph's original failure status.
27
+ // The graph completes while the optional node failure stays in execution history.
28
28
  console.log(`status: ${result.status}; join: ${result.nodes.join!.status}`);
29
29
  console.log(result.terminalOutputs.join!.output);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@chrok/braid",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "license": "MIT",
5
- "description": "A small, framework-agnostic DAG runtime for isolated model invocations",
5
+ "description": "TypeScript runtime for LLM agent graphs with bounded loops, live updates, and isolated Git worktrees",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=22"
@@ -33,6 +33,7 @@
33
33
  "build": "node scripts/build.mjs",
34
34
  "check": "tsc --noEmit",
35
35
  "test": "tsx --test test/*.test.ts test/*.test.mjs",
36
+ "audit:dependencies": "node scripts/audit-dependencies.mjs",
36
37
  "demo": "tsx examples/basic.ts",
37
38
  "check:pi": "npm run build && npm run check --workspace @chrok/pi-braid",
38
39
  "test:pi": "npm run build && npm test --workspace @chrok/pi-braid",
@@ -40,7 +41,7 @@
40
41
  "build:pi": "npm run build --workspace @chrok/pi-braid",
41
42
  "test:package": "node scripts/package-smoke.mjs",
42
43
  "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
+ "examples": "tsx examples/basic.ts && tsx examples/code-review.ts && tsx examples/failure-handling.ts && tsx examples/custom-runner.ts && tsx examples/execution-control.ts",
44
45
  "bench": "npm run build && node --expose-gc scripts/benchmark.mjs",
45
46
  "demo:panel": "tsx scripts/panel-demo.ts",
46
47
  "test:pi:live": "npm run build:pi && node integrations/pi/test/live/run.mjs"
@@ -61,9 +62,15 @@
61
62
  },
62
63
  "keywords": [
63
64
  "llm",
65
+ "ai-agents",
64
66
  "agents",
65
- "dag",
67
+ "agent-runtime",
68
+ "agent-orchestration",
69
+ "graph",
70
+ "bounded-loops",
71
+ "git-worktree",
66
72
  "orchestration",
73
+ "openai-compatible",
67
74
  "typescript"
68
75
  ],
69
76
  "publishConfig": {