@loops-adk/core 0.1.1 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loops-adk/core",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "author": "Jonny Neill",
6
6
  "description": "Run an agent in a convergence loop with an honest done-gate. A small, nestable loop and DAG primitive: deterministic plus agent-judge conditions, git as memory, review-restart, budgets, and a live TUI.",
@@ -52,6 +52,7 @@
52
52
  "dist",
53
53
  "bin",
54
54
  "skills",
55
+ "assets",
55
56
  "README.md",
56
57
  "LICENSE"
57
58
  ],
@@ -64,6 +65,14 @@
64
65
  "typecheck": "tsc --noEmit",
65
66
  "test": "vitest run",
66
67
  "test:watch": "vitest",
68
+ "bench:ab": "tsx bench/ab.ts",
69
+ "bench:graph": "tsx bench/graph.ts",
70
+ "bench:signal": "BENCH_GRAPH_TASK=graph-tasks/stable-store-contract BENCH_OUT=bench/results-signal.json tsx bench/graph.ts",
71
+ "bench:compare": "tsx bench/compare.ts",
72
+ "bench:report": "tsx bench/report.ts",
73
+ "bench:report:sample": "tsx bench/report.ts bench/results.sample.json",
74
+ "bench:context:dry": "BENCH_DRY=1 BENCH_CB_GROUPS=bench/contextbench/groups.dry.json tsx bench/swecontextbench.ts",
75
+ "bench:mechanism": "tsx bench/mechanism.ts",
67
76
  "example:poll": "tsx src/index.ts run examples/simple-poll.loop.ts --no-tui",
68
77
  "example:gate": "tsx src/index.ts run examples/confidence-gate.loop.ts",
69
78
  "prepack": "npm run build",
@@ -97,21 +97,30 @@ dag({
97
97
 
98
98
  `needs` are dependencies; `optional` nodes never block; an unmet `when` skips a node; `isolation: 'worktree'` (on the dag) or `isolate: true` (per node) runs writers in parallel worktrees that land back on pass. `sequence` and `parallel` are sugar over `dag`.
99
99
 
100
+ ## Agents and feedback
101
+
102
+ A node can be a named specialist instead of an inline prompt. Define it once with `defineAgent` (persona in markdown via `fromFile`, structure in TS) and hand it to `agentJob({ agent })`; `defineSkill` folds a methodology into its system. The contract fields (`tier`, `outputs`, `failureModes`, …) are metadata for `describe` and validation, not scheduling power: the `dag` orchestrates, agents stay workers.
103
+
104
+ Review feedback is a structured revision request that flows back to the worker on one channel. In a loop, a failing `review` is threaded into the next turn as `ctx.lastReview`; set `consumeFeedback: true` and `agentJob` folds it into the prompt. Aggregate several reviewers with `reviewPanel`; route a fix back to an earlier dag node with a targeted `revisionRequest({ target, findings })` (or the terse `kickback(to, reason)`) when the dag's `maxKickbacks` allows it.
105
+
106
+ Composing a team of specialists, gates, and routed feedback is its own skill: see `skills/design-agent-team/SKILL.md`.
107
+
100
108
  ## Author → validate → run
101
109
 
102
110
  ```bash
103
- loops validate path/to/feature.loop.ts # offline pre-flight: loads + prints the shape, no model calls, no spend
104
- loops describe path/to/feature.loop.ts # print the loop's shape (gate, body, nodes) without running
105
- loops run path/to/feature.loop.ts # live Ink TUI
111
+ loops validate path/to/feature.loop.ts # offline pre-flight: loads + prints the shape, no model calls, no spend
112
+ loops describe path/to/feature.loop.ts # print the loop's shape (gate, body, nodes) without running
113
+ loops describe path/to/feature.loop.ts --json # the same shape as JSON (incl. each agent node's contract)
114
+ loops run path/to/feature.loop.ts # live Ink TUI
106
115
  loops run path/to/feature.loop.ts --no-tui # plain streamed logs
107
- loops run path/to/feature.loop.ts --json # NDJSON event stream (parse this from an agent)
116
+ loops run path/to/feature.loop.ts --json # raw NDJSON event firehose (to supervise a run, prefer --supervise + records, below)
108
117
  ```
109
118
 
110
119
  Always `loops validate` first. It imports and constructs the loop (catching syntax, import, and bad-export errors) without running it, so you fix authoring mistakes for free before spending a single agent turn. It also prints the loop's shape (its gate, body, and dag nodes), so you can confirm you built what you intended. `loops describe` prints that shape on its own.
111
120
 
112
121
  `loops run` works from any repo, including one that uses `loops` as a submodule or dependency. The recipe's folder must be an ES module scope (a `package.json` with `{"type":"module"}`); repos that consume `loops` already have this. If a load fails with an ES-module error, that scope is what is missing.
113
122
 
114
- Add `--supervise` to make a run observable from another process: it registers under `~/.loops/runs/`, and `loops list` / `loops status <runId>` / `loops tail <runId>` read its live state (shape, iteration, last gate verdict, tokens, events). Use this to watch a long run, or to supervise several at once.
123
+ Add `--supervise` to make a run observable from another process: it registers under `~/.loops/runs/`. From an agent, the primary read API is `loops records <runId>`, the semantic decision stream (dispatch / completion / surfacing / revision), filterable with `--kind`, `--path`, `--last`, `--json`, rather than the raw `run --json` firehose. `loops tail <runId>` streams live events, `loops status <runId>` reports terminal state, and `loops list` enumerates runs. Watching a long run or supervising several at once is its own skill: see `skills/supervise-loop-run/SKILL.md`.
115
124
 
116
125
  ## Gotchas
117
126
 
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: design-agent-team
3
+ description: Use when composing a team of specialist agents in a loops `dag`: defining an `AgentDef`, folding in `defineSkill` methodologies, wiring review feedback (`reviewPanel`/`consumeFeedback`/`revisionRequest`), and gating nodes so the graph orchestrates and the agents stay workers, never dispatchers. Load this before turning a loop into a multi-agent team.
4
+ ---
5
+
6
+ # Designing an agent team
7
+
8
+ A `dag` of specialist agents is a team. The load-bearing rule that keeps it a team and not a swarm:
9
+
10
+ **The graph orchestrates; agents do not.** The `dag` is the manager (toposort + dispatch), `Condition`/`quorum` are the gates, `Outcome` is the result channel. An `AgentDef` is only the *contract*: who the agent is, what it may touch, how it works. It carries no scheduling authority. An agent produces an `Outcome`; the graph decides what runs next. Never build an agent whose job is to dispatch other agents; make the graph do it.
11
+
12
+ **REQUIRED BACKGROUND:** you compose these agents into a loop/dag. Read `skills/author-loop/SKILL.md` for the loop mental model, the honest gate, and git-memory first.
13
+
14
+ ## Two builders: a skill is a method, an agent is a worker
15
+
16
+ - `defineSkill({ name, instructions })` is a **methodology** (how to work: TDD, writing-plans). Prose only. A skill never dispatches an agent.
17
+ - `defineAgent({ ... })` is a **worker**: a persona plus its contract. It *composes* skills; the skills' instructions fold into its system prompt.
18
+
19
+ Persona and methodology live in editable markdown (`fromFile`); structure and types live in TS. The `.ts` is the typed wrapper around the `.md`.
20
+
21
+ ```ts
22
+ import { defineAgent, defineSkill, fromFile, agentJob } from '@loops-adk/core';
23
+
24
+ const tdd = defineSkill({
25
+ name: 'tdd',
26
+ instructions: fromFile(new URL('./skills/tdd.md', import.meta.url)),
27
+ });
28
+
29
+ const storeEngineer = defineAgent({
30
+ name: 'store-engineer',
31
+ system: fromFile(new URL('./agents/store-engineer.md', import.meta.url)), // persona, as markdown
32
+ model: 'sonnet',
33
+ tools: ['edit', 'bash'], // the permission boundary
34
+ leaf: true, // may not spawn sub-agents; bottoms the branch out here
35
+ tier: 'worker', // contract metadata (no scheduling power)
36
+ capabilities: ['storage engine', 'id stability'],
37
+ outputs: [{ name: 'patch' }, { name: 'test-report' }],
38
+ skills: [tdd], // methodologies fold into the system
39
+ requiresSkills: ['contract-first'], // metadata unless also in `skills`
40
+ usesSkills: ['small-diff'],
41
+ humanGates: [{ name: 'prod-approval', when: 'deploying production changes' }],
42
+ failureModes: [{ mode: 'tests-flaky', recovery: 'isolate the flake, retry once', severity: 'should-fix' }],
43
+ });
44
+ ```
45
+
46
+ `agentJob({ agent: storeEngineer, prompt, ground: true })` resolves the def into the engine request (`system` = persona + folded skills, plus `model`/`tools`). Inline `system`/`model`/`tools`/`allowedTools` on the `agentJob` still override the def. The contract fields beyond `system`/`model`/`tools` are **optional metadata** for validation, `loops describe`, docs, and future discovery. They change nothing at runtime; they do not grant dispatch authority.
47
+
48
+ **`leaf` is the fan-out brake.** A leaf agent cannot spawn sub-agents (the engine withholds the sub-agent tool). Use it to stop a thorough worker from quietly expanding into a slow, expensive swarm. The team's shape stays the graph you drew, not one the agent invents.
49
+
50
+ ## Wire the team as a graph
51
+
52
+ ```ts
53
+ import { dag, loop, agentJob, gateJob, quorum, agentCheck, commandSucceeds } from '@loops-adk/core';
54
+
55
+ dag({
56
+ name: 'ship',
57
+ nodes: {
58
+ store: loop({ name: 'store', body: agentJob({ agent: storeEngineer, prompt: 'Build the store to its tests.', ground: true }), until: commandSucceeds('npm', ['test']) }),
59
+ api: { needs: ['store'], job: loop({ /* apiEngineer, same shape */ }) },
60
+ review: { needs: ['api'], job: gateJob('review', quorum(2,
61
+ agentCheck({ agent: securityReviewer, question: 'Is it safe?' }),
62
+ agentCheck({ agent: correctnessReviewer, question: 'Is it correct?' }),
63
+ )) },
64
+ },
65
+ });
66
+ ```
67
+
68
+ Each engineer is a Converge loop (build to a `test` gate); reviewers are gates. `quorum(k, ...)` is a k-of-n jury; `gateJob(name, condition)` turns a `Condition` into a `Job` so it can be a node. Because a reviewer is just an agent and `agentCheck` takes an `engine`/`model`, any reviewer runs on any model, so put the adversarial lens on a second model for a genuinely independent signal.
69
+
70
+ ## Feedback is a loop boundary, not a back-edge
71
+
72
+ Review findings are structured, and they flow back to the worker on the same channel whether they come from a loop's `review` slot or a dag kickback.
73
+
74
+ **In a loop:** a failing `review` outcome is threaded into the next body turn as `ctx.lastReview`. Set `consumeFeedback: true` so the worker reads it without you hand-writing "address the feedback" into every prompt:
75
+
76
+ ```ts
77
+ const implement = agentJob({ agent: implementationAgent, prompt: brief, consumeFeedback: true });
78
+ ```
79
+
80
+ **Aggregate several reviewers** with `reviewPanel`. Every reviewer is a gate: the panel passes when all of them clear, or `pass: N` of them (k-of-n). An empty panel is a construction error. Give each reviewer real evidence with `reviewContext`:
81
+
82
+ ```ts
83
+ import { reviewPanel, reviewContext, agentCheck } from '@loops-adk/core';
84
+
85
+ const review = reviewPanel({
86
+ pass: 2, // optional: k-of-n instead of all
87
+ reviewers: [
88
+ { name: 'security', review: agentCheck({ question: 'Is it safe?', context: reviewContext({ diff: true, ledger: true }) }) },
89
+ { name: 'correctness', review: agentCheck({ question: 'Is it correct?', context: reviewContext({ tests: { command: 'npm', args: ['test'] } }) }) },
90
+ { name: 'simplicity', review: agentCheck({ question: 'Is it simple?', context: reviewContext({ files: ['src/**'] }) }) },
91
+ ],
92
+ });
93
+ ```
94
+
95
+ A failing panel emits a `revisionRequest` carrying each failing reviewer's concern as a finding, threaded into the next pass.
96
+
97
+ **Route feedback across a DAG** with a targeted revision. When `DagConfig.maxKickbacks > 0`, a `revisionRequest({ target, findings })` (or the terse `kickback(to, reason)`) re-runs the target node and its transitive dependents, threading the reason in as their `lastReview`. Constrain valid targets with `DagNode.acceptsKickbackTo`. Because every cycle is a bounded re-run, not a graph edge, it always terminates.
98
+
99
+ Give a worker just enough map to act on routed feedback without seeing the whole orchestration, with `graphContext: true`, which appends a small block naming this node, its direct dependencies, and its direct dependents.
100
+
101
+ ## Verify the contract before spending a turn
102
+
103
+ ```bash
104
+ loops validate team.loop.ts # loads + constructs, no model calls
105
+ loops describe team.loop.ts --json # the shape, incl. each agent node's contract (tier, outputs, failure modes)
106
+ ```
107
+
108
+ `describe --json` reflects the contract you declared back at you, so you confirm the team you built is the team you meant. To watch or supervise the team once it runs, see `skills/supervise-loop-run/SKILL.md`.
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: supervise-loop-run
3
+ description: Use when an agent needs to observe, monitor, or supervise a running loops job from another process: discover live runs, read a run's state and shape, stream its events, inspect the decisions it made (dispatch/completion/surfacing/revision), or decide whether to intervene. Load this when watching a long run or supervising several at once. Requires the run to have been started with `--supervise`.
4
+ ---
5
+
6
+ # Supervising a loop run
7
+
8
+ A run started with `loops run <file> --supervise` registers itself under `~/.loops/runs/<runId>/` and writes its live state, raw events (`events.jsonl`), and semantic decisions (`semantic.jsonl`) there as it goes. Another process reads those files with no daemon and no socket: the filesystem is the channel. Every command below is read-only; supervising never touches the run.
9
+
10
+ ```bash
11
+ loops run build.loop.ts --supervise # in one terminal (or backgrounded)
12
+ ```
13
+
14
+ ## The loop: list → status → tail → records → decide
15
+
16
+ **`loops list`** (alias `ls`) discovers runs. Each line is the runId, state (`running` / `dead` / a terminal status like `pass`/`fail`/`paused`), current iteration, age, and title. A run whose process is gone is marked `dead`.
17
+
18
+ **`loops status <runId>`** prints a point-in-time snapshot: terminal-or-live state, the loop's shape, the last gate verdict (which gate, met, confidence, reason), the last outcome, and token usage. Use this to answer where a run stands and whether it is healthy.
19
+
20
+ **`loops tail <runId>`** streams the raw event log live (Ctrl-C to stop). It ends on its own when the run reaches a terminal status or its process disappears. Use this to watch a turn unfold.
21
+
22
+ **`loops records <runId>`** is the **primary agent API**: the semantic decision stream, one line per meaningful thing the run decided. This is what an agent reads to reason about a run, not the raw `--json` event firehose. Five kinds:
23
+
24
+ | kind | meaning |
25
+ | --- | --- |
26
+ | `dispatch` | a job or dag-node started |
27
+ | `completion` | a job / loop / dag finished (carries the outcome status + summary) |
28
+ | `surfacing` | a review or kickback raised feedback (carries severity + reason) |
29
+ | `revision-emitted` | an outcome asked for another pass |
30
+ | `revision-routed` | that revision was routed to a target (accepted/rejected) |
31
+
32
+ Filter it down for a machine-readable slice:
33
+
34
+ ```bash
35
+ loops records <runId> --json # everything, as JSONL
36
+ loops records <runId> --kind completion # just what finished
37
+ loops records <runId> --kind revision # both revision kinds (emitted + routed)
38
+ loops records <runId> --path ship/implementation --json # only this subtree of the loop tree
39
+ loops records <runId> --kind surfacing --since 2026-07-01T09:00:00Z
40
+ loops records <runId> --last 20 # the most recent 20 matching records
41
+ ```
42
+
43
+ `--path` is a slash-separated prefix over the record's position in the loop tree. `--kind revision` is the convenience union of `revision-emitted` and `revision-routed`.
44
+
45
+ ## Deciding what to do next
46
+
47
+ Read `records` (and `status` for tokens/gate) to choose an action, since loops does not act for you:
48
+
49
+ - **Converged**: a top-level `completion` with `status: pass`. Done; nothing to do.
50
+ - **Stuck in review**: repeated `surfacing` / `revision-routed` on the same node with a `block`/`should-fix` severity and the iteration climbing toward its cap. The gate is doing its job or the worker cannot satisfy it; inspect the reason and decide whether to let it run, abort, or (if you drive the run) route different feedback.
51
+ - **Dead**: `list` shows `dead`, or `status` says the process is gone with no terminal outcome. The run crashed or was killed; investigate its last `completion`/event.
52
+ - **Budget-bound**: `status` shows tokens near the run's budget; expect a `paused` outcome next.
53
+
54
+ ## Build your own supervisor
55
+
56
+ The read side is on the public surface, so an agent supervising a fleet (killing the ones that drift, watching the ones mid-revision) reads the same files programmatically:
57
+
58
+ ```ts
59
+ import { listRuns, readRunStatus, runEventsPath, runSemanticRecordsPath } from '@loops-adk/core';
60
+ ```
61
+
62
+ `listRuns()` and `readRunStatus(runId)` mirror `list`/`status`; `runEventsPath`/`runSemanticRecordsPath` locate the two JSONL streams to read directly. `semanticRecordsFromEvent(event)` derives the semantic records from a raw event if you tail the event stream yourself.
63
+
64
+ To author or shape the run you are supervising, see `skills/author-loop/SKILL.md`; to compose the agent team inside it, see `skills/design-agent-team/SKILL.md`.