@wrenautomation/raven 0.3.0 → 0.7.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/dist/app/cli.js +16 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/model.d.ts +4 -37
- package/dist/model.js +2 -121
- package/dist/plan.d.ts +1 -1
- package/dist/plan.js +0 -5
- package/dist/review.d.ts +1 -1
- package/dist/review.js +0 -5
- package/dist/triage.d.ts +1 -1
- package/dist/triage.js +0 -6
- package/package.json +9 -20
package/dist/app/cli.js
CHANGED
|
@@ -2,20 +2,21 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* `raven`: the planner from a shell. `plan` turns an ask into queries, `triage` marks hits read
|
|
4
4
|
* or skip, `review` picks the edge after a round, `plans` prints a default plan file to tune.
|
|
5
|
-
* The model is a
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The model is a spec from `@wrenautomation/models` (`claude-code` by default, `anthropic:sonnet`,
|
|
6
|
+
* `gateway`...) or any command that reads the prompt on stdin and prints the answer; RAVEN_MODEL
|
|
7
|
+
* or --model changes it. Everything is JSON on stdout.
|
|
8
8
|
*/
|
|
9
9
|
import { readFileSync } from "node:fs";
|
|
10
10
|
import { userInfo } from "node:os";
|
|
11
|
+
import { commandModel } from "@wrenautomation/models/command";
|
|
12
|
+
import { modelFromSpec, SPEC_NAMES } from "@wrenautomation/models/spec";
|
|
11
13
|
import { Command } from "commander";
|
|
12
|
-
import { commandModel } from "../model.js";
|
|
13
14
|
import { plan } from "../plan.js";
|
|
14
15
|
import { defaultPlanFile, PLAN_KINDS } from "../plans/index.js";
|
|
15
16
|
import { review } from "../review.js";
|
|
16
17
|
import { triage } from "../triage.js";
|
|
17
18
|
import { AskKind, HitView, Plan, PlanFile, Round, YieldRow, z } from "../types.js";
|
|
18
|
-
const DEFAULT_MODEL = "claude
|
|
19
|
+
const DEFAULT_MODEL = "claude-code";
|
|
19
20
|
const readJson = (path, shape) => shape.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
20
21
|
const print = (x) => console.log(JSON.stringify(x, null, 2));
|
|
21
22
|
const more = (v, prev) => [...prev, v];
|
|
@@ -27,11 +28,17 @@ const who = () => {
|
|
|
27
28
|
return "human";
|
|
28
29
|
}
|
|
29
30
|
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
/** A spec name (`claude-code`, `anthropic:sonnet`) goes to models; anything else is a command. */
|
|
32
|
+
const modelOf = (o) => {
|
|
33
|
+
if (o.model === false)
|
|
34
|
+
return null;
|
|
35
|
+
const name = o.model.split(":")[0] ?? "";
|
|
36
|
+
return SPEC_NAMES.includes(name)
|
|
37
|
+
? modelFromSpec(o.model)
|
|
38
|
+
: commandModel(o.model);
|
|
39
|
+
};
|
|
33
40
|
const withModel = (c) => c
|
|
34
|
-
.option("--model <
|
|
41
|
+
.option("--model <spec>", "a models spec (claude-code, anthropic:sonnet, gateway) or a command: prompt on stdin, answer on stdout", process.env.RAVEN_MODEL ?? DEFAULT_MODEL)
|
|
35
42
|
.option("--no-model", "no model call: only what is given by hand");
|
|
36
43
|
const program = new Command().name("raven").description("ask in, plan out");
|
|
37
44
|
withModel(program
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { fill } from "./model.js";
|
|
2
2
|
export { mergeProposals, type PlanInput, plan, propose, proposePrompt } from "./plan.js";
|
|
3
3
|
export { defaultPlanFile, PLAN_KINDS } from "./plans/index.js";
|
|
4
4
|
export { renderHits, renderYield } from "./render.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { fill } from "./model.js";
|
|
2
2
|
export { mergeProposals, plan, propose, proposePrompt } from "./plan.js";
|
|
3
3
|
export { defaultPlanFile, PLAN_KINDS } from "./plans/index.js";
|
|
4
4
|
export { renderHits, renderYield } from "./render.js";
|
package/dist/model.d.ts
CHANGED
|
@@ -1,39 +1,6 @@
|
|
|
1
|
-
import type { ZodType } from "zod";
|
|
2
|
-
/** What one model call cost, as far as the model says. All optional: a shell command may say nothing. */
|
|
3
|
-
export interface Usage {
|
|
4
|
-
/** Tokens in and out. */
|
|
5
|
-
input?: number;
|
|
6
|
-
output?: number;
|
|
7
|
-
usd?: number;
|
|
8
|
-
}
|
|
9
|
-
/** A parsed answer and what it cost: the ledger keeps both. */
|
|
10
|
-
export interface Answer<T> {
|
|
11
|
-
value: T;
|
|
12
|
-
usage?: Usage;
|
|
13
|
-
}
|
|
14
|
-
export interface Model {
|
|
15
|
-
/** What goes in `by` on the records this model writes. */
|
|
16
|
-
id: string;
|
|
17
|
-
ask<T>(prompt: string, shape: ZodType<T>): Promise<Answer<T>>;
|
|
18
|
-
}
|
|
19
|
-
/** `{name}` marks filled in one pass; a `{word}` with no value stays as text. */
|
|
20
|
-
export declare const fill: (template: string, values: Readonly<Record<string, string>>) => string;
|
|
21
|
-
/**
|
|
22
|
-
* The first JSON object of the shape in a model's text, found by brace balance: fences, prose
|
|
23
|
-
* and stray braces around it are passed over. Throws with the model's text when nothing parses.
|
|
24
|
-
*/
|
|
25
|
-
export declare function parseAnswer<T>(text: string, shape: ZodType<T>): T;
|
|
26
|
-
/** A command's stdout as an answer: the envelope's text and usage when it printed one, else the text. */
|
|
27
|
-
export declare function readOutput(stdout: string): {
|
|
28
|
-
text: string;
|
|
29
|
-
usage?: Usage;
|
|
30
|
-
};
|
|
31
1
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* A command that says nothing for `timeoutMs` (3 minutes) is killed and the ask fails with
|
|
35
|
-
* why, so a hung model never hangs the run.
|
|
2
|
+
* Raven never picks a model: a caller hands in one `Model` from `@wrenautomation/models`, and its
|
|
3
|
+
* id goes on every record raven writes. This file keeps only the prompt template fill.
|
|
36
4
|
*/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
export declare function scriptedModel(answers: unknown[], id?: string): Model;
|
|
5
|
+
/** `{name}` marks filled in one pass; a `{word}` with no value stays as text. */
|
|
6
|
+
export declare const fill: (template: string, values: Readonly<Record<string, string>>) => string;
|
package/dist/model.js
CHANGED
|
@@ -1,126 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* record it writes. In a terminal the model is a command that reads the prompt on stdin and
|
|
5
|
-
* prints the answer (`claude -p --output-format json` by default); in wren it wraps the LLM
|
|
6
|
-
* client.
|
|
2
|
+
* Raven never picks a model: a caller hands in one `Model` from `@wrenautomation/models`, and its
|
|
3
|
+
* id goes on every record raven writes. This file keeps only the prompt template fill.
|
|
7
4
|
*/
|
|
8
|
-
import { spawn } from "node:child_process";
|
|
9
5
|
/** `{name}` marks filled in one pass; a `{word}` with no value stays as text. */
|
|
10
6
|
export const fill = (template, values) => template.replace(/\{(\w+)\}/g, (mark, name) => values[name] ?? mark);
|
|
11
|
-
/** Every balanced `{...}` in a text, by start position: an outer object comes before its inner ones. */
|
|
12
|
-
function* objects(text) {
|
|
13
|
-
for (let start = text.indexOf("{"); start >= 0; start = text.indexOf("{", start + 1)) {
|
|
14
|
-
let depth = 0;
|
|
15
|
-
let inString = false;
|
|
16
|
-
let escaped = false;
|
|
17
|
-
for (let i = start; i < text.length; i++) {
|
|
18
|
-
const c = text[i];
|
|
19
|
-
if (inString) {
|
|
20
|
-
if (escaped)
|
|
21
|
-
escaped = false;
|
|
22
|
-
else if (c === "\\")
|
|
23
|
-
escaped = true;
|
|
24
|
-
else if (c === '"')
|
|
25
|
-
inString = false;
|
|
26
|
-
}
|
|
27
|
-
else if (c === '"')
|
|
28
|
-
inString = true;
|
|
29
|
-
else if (c === "{")
|
|
30
|
-
depth++;
|
|
31
|
-
else if (c === "}" && --depth === 0) {
|
|
32
|
-
yield text.slice(start, i + 1);
|
|
33
|
-
break;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* The first JSON object of the shape in a model's text, found by brace balance: fences, prose
|
|
40
|
-
* and stray braces around it are passed over. Throws with the model's text when nothing parses.
|
|
41
|
-
*/
|
|
42
|
-
export function parseAnswer(text, shape) {
|
|
43
|
-
for (const candidate of objects(text)) {
|
|
44
|
-
try {
|
|
45
|
-
const out = shape.safeParse(JSON.parse(candidate));
|
|
46
|
-
if (out.success)
|
|
47
|
-
return out.data;
|
|
48
|
-
}
|
|
49
|
-
catch { }
|
|
50
|
-
}
|
|
51
|
-
throw new Error(`no answer of the expected shape in: ${text.slice(0, 400)}`);
|
|
52
|
-
}
|
|
53
|
-
const isEnvelope = (x) => typeof x === "object" && x !== null && typeof x.result === "string";
|
|
54
|
-
/** A command's stdout as an answer: the envelope's text and usage when it printed one, else the text. */
|
|
55
|
-
export function readOutput(stdout) {
|
|
56
|
-
try {
|
|
57
|
-
const parsed = JSON.parse(stdout);
|
|
58
|
-
if (isEnvelope(parsed)) {
|
|
59
|
-
const usage = {};
|
|
60
|
-
if (parsed.usage?.input_tokens !== undefined)
|
|
61
|
-
usage.input = parsed.usage.input_tokens;
|
|
62
|
-
if (parsed.usage?.output_tokens !== undefined)
|
|
63
|
-
usage.output = parsed.usage.output_tokens;
|
|
64
|
-
if (parsed.total_cost_usd !== undefined)
|
|
65
|
-
usage.usd = parsed.total_cost_usd;
|
|
66
|
-
return { text: parsed.result, usage };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
catch { }
|
|
70
|
-
return { text: stdout };
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* A model that is a shell command: the prompt on stdin, the answer on stdout. When stdout is
|
|
74
|
-
* Claude Code's JSON envelope (`claude -p --output-format json`) the usage comes with it.
|
|
75
|
-
* A command that says nothing for `timeoutMs` (3 minutes) is killed and the ask fails with
|
|
76
|
-
* why, so a hung model never hangs the run.
|
|
77
|
-
*/
|
|
78
|
-
export function commandModel(command, id = command, timeoutMs = 180_000) {
|
|
79
|
-
return {
|
|
80
|
-
id,
|
|
81
|
-
ask: (prompt, shape) => new Promise((resolve, reject) => {
|
|
82
|
-
const child = spawn(command, { shell: true, stdio: ["pipe", "pipe", "inherit"] });
|
|
83
|
-
const out = [];
|
|
84
|
-
let timedOut = false;
|
|
85
|
-
const timer = setTimeout(() => {
|
|
86
|
-
timedOut = true;
|
|
87
|
-
child.kill();
|
|
88
|
-
}, timeoutMs);
|
|
89
|
-
child.stdout.on("data", (b) => out.push(b));
|
|
90
|
-
child.on("error", (e) => {
|
|
91
|
-
clearTimeout(timer);
|
|
92
|
-
reject(e);
|
|
93
|
-
});
|
|
94
|
-
child.on("close", (code) => {
|
|
95
|
-
clearTimeout(timer);
|
|
96
|
-
const stdout = Buffer.concat(out).toString("utf8");
|
|
97
|
-
if (timedOut)
|
|
98
|
-
return reject(new Error(`${command}: no answer in ${Math.round(timeoutMs / 1000)}s`));
|
|
99
|
-
if (code !== 0)
|
|
100
|
-
return reject(new Error(`${command} exited ${code}: ${stdout.slice(0, 400)}`));
|
|
101
|
-
try {
|
|
102
|
-
const { text, usage } = readOutput(stdout);
|
|
103
|
-
resolve({ value: parseAnswer(text, shape), ...(usage ? { usage } : {}) });
|
|
104
|
-
}
|
|
105
|
-
catch (e) {
|
|
106
|
-
reject(e);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
child.stdin.end(prompt);
|
|
110
|
-
}),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
/** A model that answers from a fixed list, in order: tests and dry runs. */
|
|
114
|
-
export function scriptedModel(answers, id = "scripted") {
|
|
115
|
-
let i = 0;
|
|
116
|
-
return {
|
|
117
|
-
id,
|
|
118
|
-
ask: async (_prompt, shape) => {
|
|
119
|
-
const a = answers[i++];
|
|
120
|
-
if (a === undefined)
|
|
121
|
-
throw new Error(`scripted model has no answer ${i}`);
|
|
122
|
-
return { value: shape.parse(a) };
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
7
|
//# sourceMappingURL=model.js.map
|
package/dist/plan.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* or a second model are others, and all merge the same way. Human queries come first and never
|
|
4
4
|
* count against the budget: someone chose them.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import type { Model } from "@wrenautomation/models";
|
|
7
7
|
import { type Ask, type Plan, type PlanFile, type Proposal, type Query, type YieldRow } from "./types.js";
|
|
8
8
|
export declare function proposePrompt(ask: Ask, planFile: PlanFile, yieldRows: readonly YieldRow[], now: Date): string;
|
|
9
9
|
/** The model's proposal for an ask under a plan file. */
|
package/dist/plan.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The plan step: proposals in, one plan out. The model is one proposer; a person, a plan file
|
|
3
|
-
* or a second model are others, and all merge the same way. Human queries come first and never
|
|
4
|
-
* count against the budget: someone chose them.
|
|
5
|
-
*/
|
|
6
1
|
import { fill } from "./model.js";
|
|
7
2
|
import { defaultPlanFile } from "./plans/index.js";
|
|
8
3
|
import { lines, renderSteer, renderYield } from "./render.js";
|
package/dist/review.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* rail, not a stop rule: when it is spent raven stops and says so; before that the model decides
|
|
4
4
|
* and says why. The runner takes the edge; raven never loops.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import type { Model } from "@wrenautomation/models";
|
|
7
7
|
import { type Edge, type PlanFile, type Round } from "./types.js";
|
|
8
8
|
export declare function reviewPrompt(round: Round, planFile: PlanFile): string;
|
|
9
9
|
export declare function review(round: Round, model: Model, options?: {
|
package/dist/review.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* After a round: stop, go again from plan, or back to one step. The round budget is a guard
|
|
3
|
-
* rail, not a stop rule: when it is spent raven stops and says so; before that the model decides
|
|
4
|
-
* and says why. The runner takes the edge; raven never loops.
|
|
5
|
-
*/
|
|
6
1
|
import { fill } from "./model.js";
|
|
7
2
|
import { defaultPlanFile } from "./plans/index.js";
|
|
8
3
|
import { lines, renderSteer } from "./render.js";
|
package/dist/triage.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* caps it; a hit the model did not name is a skip, written down as one. The model writes a why
|
|
5
5
|
* on a read only: a skip's why is fixed text, so the answer pays for the reads alone.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import type { Model } from "@wrenautomation/models";
|
|
8
8
|
import { HitView, type HitViewIn, type Plan, type PlanFile, type Triage, type YieldRow } from "./types.js";
|
|
9
9
|
/** What a skip says: the model named it and passed. */
|
|
10
10
|
export declare const SKIPPED = "skipped by the model";
|
package/dist/triage.js
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The filter before the read: for each hit, read or skip, from what the hit shows for free.
|
|
3
|
-
* Hits dedupe by url and keep every source that returned them. The model picks; the read budget
|
|
4
|
-
* caps it; a hit the model did not name is a skip, written down as one. The model writes a why
|
|
5
|
-
* on a read only: a skip's why is fixed text, so the answer pays for the reads alone.
|
|
6
|
-
*/
|
|
7
1
|
import { fill } from "./model.js";
|
|
8
2
|
import { defaultPlanFile } from "./plans/index.js";
|
|
9
3
|
import { lines, renderHits, renderSteer, renderYield } from "./render.js";
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrenautomation/raven",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Ask in, plan out. The planner of Wren's research engine: typed queries with who proposed them and why, proposals merged into one plan under a plan file per ask kind, read-or-skip on hits, and the edge to take after a round. Pure functions and prompts; the model is injected.",
|
|
5
5
|
"license": "PolyForm-Shield-1.0.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/wrenautomation/
|
|
8
|
+
"url": "git+https://github.com/wrenautomation/engine.git",
|
|
9
|
+
"directory": "packages/raven"
|
|
9
10
|
},
|
|
10
11
|
"type": "module",
|
|
11
12
|
"files": [
|
|
@@ -28,25 +29,13 @@
|
|
|
28
29
|
"engines": {
|
|
29
30
|
"node": ">=24"
|
|
30
31
|
},
|
|
31
|
-
"packageManager": "pnpm@9.15.0",
|
|
32
|
-
"scripts": {
|
|
33
|
-
"prepare": "git config core.hooksPath .githooks || true",
|
|
34
|
-
"lint": "biome check --error-on-warnings .",
|
|
35
|
-
"lint:fix": "biome check --write .",
|
|
36
|
-
"typecheck": "tsc --noEmit",
|
|
37
|
-
"build": "rm -rf dist && tsc && chmod +x dist/app/cli.js",
|
|
38
|
-
"test": "vitest run",
|
|
39
|
-
"gates": "lockf -t 900 /tmp/wren_automation-checks.lock sh -c 'pnpm lint && pnpm typecheck && pnpm test'",
|
|
40
|
-
"prepublishOnly": "pnpm build"
|
|
41
|
-
},
|
|
42
32
|
"dependencies": {
|
|
43
33
|
"commander": "15.0.0",
|
|
44
|
-
"zod": "4.6.5"
|
|
34
|
+
"zod": "4.6.5",
|
|
35
|
+
"@wrenautomation/models": "0.7.0"
|
|
45
36
|
},
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"typescript": "5.9.3",
|
|
50
|
-
"vitest": "5.0.1"
|
|
37
|
+
"scripts": {
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"build": "rm -rf dist && tsc && chmod +x dist/app/cli.js"
|
|
51
40
|
}
|
|
52
|
-
}
|
|
41
|
+
}
|