@wrenautomation/raven 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/README.md +7 -3
- package/dist/app/cli.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/model.d.ts +22 -5
- package/dist/model.js +65 -20
- package/dist/plan.js +1 -1
- package/dist/plans/index.js +1 -1
- package/dist/review.js +1 -1
- package/dist/triage.d.ts +3 -3
- package/dist/triage.js +3 -3
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,8 +21,9 @@ learns. Raven never loops; the runner takes the edge.
|
|
|
21
21
|
## CLI
|
|
22
22
|
|
|
23
23
|
Needs Node 24+. The model is a command that reads the prompt on stdin and prints the answer.
|
|
24
|
-
Default `claude -p`, so Claude Code is the model in any terminal
|
|
25
|
-
|
|
24
|
+
Default `claude -p --output-format json`, so Claude Code is the model in any terminal and its
|
|
25
|
+
envelope gives the cost per call; `RAVEN_MODEL` or `--model` changes it (plain text out is fine);
|
|
26
|
+
`--no-model` plans from your own queries only.
|
|
26
27
|
|
|
27
28
|
```sh
|
|
28
29
|
npm i @wrenautomation/raven
|
|
@@ -38,7 +39,10 @@ npx raven plans web > plans/web.json
|
|
|
38
39
|
```ts
|
|
39
40
|
import { plan, triage, review, type Model } from "@wrenautomation/raven";
|
|
40
41
|
|
|
41
|
-
const model: Model = {
|
|
42
|
+
const model: Model = {
|
|
43
|
+
id: "claude-sonnet-5-5",
|
|
44
|
+
ask: async (prompt, shape) => ({ value: await yourClient.json(prompt, shape), usage: { usd: 0.01 } }),
|
|
45
|
+
};
|
|
42
46
|
const p = await plan({ ask: { text, kind: "web", known }, model, proposals: [byHand] });
|
|
43
47
|
const reads = await triage(hits, p, model);
|
|
44
48
|
const edge = await review(round, model);
|
package/dist/app/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import { defaultPlanFile, PLAN_KINDS } from "../plans/index.js";
|
|
|
15
15
|
import { review } from "../review.js";
|
|
16
16
|
import { triage } from "../triage.js";
|
|
17
17
|
import { AskKind, HitView, Plan, PlanFile, Round, YieldRow, z } from "../types.js";
|
|
18
|
-
const DEFAULT_MODEL = "claude -p";
|
|
18
|
+
const DEFAULT_MODEL = "claude -p --output-format json";
|
|
19
19
|
const readJson = (path, shape) => shape.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
20
20
|
const print = (x) => console.log(JSON.stringify(x, null, 2));
|
|
21
21
|
const more = (v, prev) => [...prev, v];
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { commandModel, fill, type Model, parseAnswer, scriptedModel } from "./model.js";
|
|
1
|
+
export { type Answer, commandModel, fill, type Model, parseAnswer, readOutput, scriptedModel, type Usage, } 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 { commandModel, fill, parseAnswer, scriptedModel } from "./model.js";
|
|
1
|
+
export { commandModel, fill, parseAnswer, readOutput, scriptedModel, } 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,19 +1,36 @@
|
|
|
1
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
|
+
}
|
|
2
14
|
export interface Model {
|
|
3
15
|
/** What goes in `by` on the records this model writes. */
|
|
4
16
|
id: string;
|
|
5
|
-
ask<T>(prompt: string, shape: ZodType<T>): Promise<T
|
|
17
|
+
ask<T>(prompt: string, shape: ZodType<T>): Promise<Answer<T>>;
|
|
6
18
|
}
|
|
7
19
|
/** `{name}` marks filled in one pass; a `{word}` with no value stays as text. */
|
|
8
20
|
export declare const fill: (template: string, values: Readonly<Record<string, string>>) => string;
|
|
9
21
|
/**
|
|
10
|
-
* The first JSON object in a model's text,
|
|
11
|
-
* it are
|
|
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.
|
|
12
24
|
*/
|
|
13
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
|
+
};
|
|
14
31
|
/**
|
|
15
|
-
* A model that is a shell command: the prompt on stdin, the answer on stdout.
|
|
16
|
-
*
|
|
32
|
+
* A model that is a shell command: the prompt on stdin, the answer on stdout. When stdout is
|
|
33
|
+
* Claude Code's JSON envelope (`claude -p --output-format json`) the usage comes with it.
|
|
17
34
|
*/
|
|
18
35
|
export declare function commandModel(command: string, id?: string): Model;
|
|
19
36
|
/** A model that answers from a fixed list, in order: tests and dry runs. */
|
package/dist/model.js
CHANGED
|
@@ -1,33 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The model seam. Raven never picks a model: a caller hands in one `Model`, a function from a
|
|
3
|
-
* prompt and an answer shape to a parsed answer, and its id goes on every
|
|
4
|
-
* a terminal the model is a command that reads the prompt on stdin and
|
|
5
|
-
* (`claude -p` by default); in wren it wraps the LLM
|
|
3
|
+
* prompt and an answer shape to a parsed answer with what it cost, and its id goes on every
|
|
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.
|
|
6
7
|
*/
|
|
7
8
|
import { spawn } from "node:child_process";
|
|
8
9
|
/** `{name}` marks filled in one pass; a `{word}` with no value stays as text. */
|
|
9
10
|
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
|
+
}
|
|
10
38
|
/**
|
|
11
|
-
* The first JSON object in a model's text,
|
|
12
|
-
* it are
|
|
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.
|
|
13
41
|
*/
|
|
14
42
|
export function parseAnswer(text, shape) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (out.success)
|
|
21
|
-
return out.data;
|
|
22
|
-
}
|
|
23
|
-
catch { }
|
|
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;
|
|
24
48
|
}
|
|
49
|
+
catch { }
|
|
25
50
|
}
|
|
26
51
|
throw new Error(`no answer of the expected shape in: ${text.slice(0, 400)}`);
|
|
27
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
|
+
}
|
|
28
72
|
/**
|
|
29
|
-
* A model that is a shell command: the prompt on stdin, the answer on stdout.
|
|
30
|
-
*
|
|
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.
|
|
31
75
|
*/
|
|
32
76
|
export function commandModel(command, id = command) {
|
|
33
77
|
return {
|
|
@@ -38,11 +82,12 @@ export function commandModel(command, id = command) {
|
|
|
38
82
|
child.stdout.on("data", (b) => out.push(b));
|
|
39
83
|
child.on("error", reject);
|
|
40
84
|
child.on("close", (code) => {
|
|
41
|
-
const
|
|
85
|
+
const stdout = Buffer.concat(out).toString("utf8");
|
|
42
86
|
if (code !== 0)
|
|
43
|
-
return reject(new Error(`${command} exited ${code}: ${
|
|
87
|
+
return reject(new Error(`${command} exited ${code}: ${stdout.slice(0, 400)}`));
|
|
44
88
|
try {
|
|
45
|
-
|
|
89
|
+
const { text, usage } = readOutput(stdout);
|
|
90
|
+
resolve({ value: parseAnswer(text, shape), ...(usage ? { usage } : {}) });
|
|
46
91
|
}
|
|
47
92
|
catch (e) {
|
|
48
93
|
reject(e);
|
|
@@ -61,7 +106,7 @@ export function scriptedModel(answers, id = "scripted") {
|
|
|
61
106
|
const a = answers[i++];
|
|
62
107
|
if (a === undefined)
|
|
63
108
|
throw new Error(`scripted model has no answer ${i}`);
|
|
64
|
-
return shape.parse(a);
|
|
109
|
+
return { value: shape.parse(a) };
|
|
65
110
|
},
|
|
66
111
|
};
|
|
67
112
|
}
|
package/dist/plan.js
CHANGED
|
@@ -22,7 +22,7 @@ export function proposePrompt(ask, planFile, yieldRows, now) {
|
|
|
22
22
|
}
|
|
23
23
|
/** The model's proposal for an ask under a plan file. */
|
|
24
24
|
export async function propose(ask, planFile, model, yieldRows = [], now = new Date()) {
|
|
25
|
-
const out = await model.ask(proposePrompt(ask, planFile, yieldRows, now), ProposalAnswer);
|
|
25
|
+
const { value: out } = await model.ask(proposePrompt(ask, planFile, yieldRows, now), ProposalAnswer);
|
|
26
26
|
return {
|
|
27
27
|
from: "ai",
|
|
28
28
|
by: model.id,
|
package/dist/plans/index.js
CHANGED
|
@@ -9,7 +9,7 @@ const SHAPE_HITS = `Return ONLY a JSON object, no prose, exactly this shape:
|
|
|
9
9
|
{"hits": [{"url": "...", "read": true, "why": "..."}]}`;
|
|
10
10
|
const SHAPE_EDGE = `Return ONLY a JSON object, no prose, exactly this shape:
|
|
11
11
|
{"next": "stop", "why": "..."}
|
|
12
|
-
where "next" is "stop", "again" (a new round from plan) or "back" (rerun one step of this round, named in "to": "plan", "fetch", "read" or "extract").`;
|
|
12
|
+
where "next" is "stop", "again" (a new round from plan) or "back" (rerun one step of this round, named in "to": "plan", "fetch", "triage", "read" or "extract").`;
|
|
13
13
|
const CONTEXT = `Ask: {ask}
|
|
14
14
|
Known already (look past these):
|
|
15
15
|
{known}
|
package/dist/review.js
CHANGED
|
@@ -35,7 +35,7 @@ export async function review(round, model, options = {}) {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
const planFile = options.planFile ?? defaultPlanFile(round.ask.kind);
|
|
38
|
-
const out = await model.ask(reviewPrompt(round, planFile), EdgeAnswer);
|
|
38
|
+
const { value: out } = await model.ask(reviewPrompt(round, planFile), EdgeAnswer);
|
|
39
39
|
const edge = { next: out.next, why: out.why, from: "ai", by: model.id };
|
|
40
40
|
if (out.next === "back")
|
|
41
41
|
edge.to = out.to ?? "fetch";
|
package/dist/triage.d.ts
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* caps it; a hit the model did not name is a skip, written down as one.
|
|
5
5
|
*/
|
|
6
6
|
import { type Model } from "./model.js";
|
|
7
|
-
import { type
|
|
7
|
+
import { HitView, type HitViewIn, type Plan, type PlanFile, type Triage, type YieldRow } from "./types.js";
|
|
8
8
|
/** One hit per url, sources merged, order of first sight. */
|
|
9
|
-
export declare function dedupeHits(hits: readonly
|
|
9
|
+
export declare function dedupeHits(hits: readonly HitViewIn[]): HitView[];
|
|
10
10
|
export declare function triagePrompt(hits: readonly HitView[], plan: Plan, planFile: PlanFile, yieldRows: readonly YieldRow[]): string;
|
|
11
11
|
/** Read or skip per hit, with why. Reads over the plan's budget flip to skips, in the model's order. */
|
|
12
|
-
export declare function triage(hits: readonly
|
|
12
|
+
export declare function triage(hits: readonly HitViewIn[], plan: Plan, model: Model, options?: {
|
|
13
13
|
planFile?: PlanFile;
|
|
14
14
|
yieldRows?: readonly YieldRow[];
|
|
15
15
|
}): Promise<Triage[]>;
|
package/dist/triage.js
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
import { fill } from "./model.js";
|
|
7
7
|
import { defaultPlanFile } from "./plans/index.js";
|
|
8
8
|
import { lines, renderHits, renderYield } from "./render.js";
|
|
9
|
-
import { z } from "./types.js";
|
|
9
|
+
import { HitView, z, } from "./types.js";
|
|
10
10
|
const TriageAnswer = z.object({
|
|
11
11
|
hits: z.array(z.object({ url: z.string(), read: z.boolean(), why: z.string() })),
|
|
12
12
|
});
|
|
13
13
|
/** One hit per url, sources merged, order of first sight. */
|
|
14
14
|
export function dedupeHits(hits) {
|
|
15
15
|
const byUrl = new Map();
|
|
16
|
-
for (const h of hits) {
|
|
16
|
+
for (const h of hits.map((x) => HitView.parse(x))) {
|
|
17
17
|
const url = h.url.trim();
|
|
18
18
|
const have = byUrl.get(url);
|
|
19
19
|
if (!have) {
|
|
@@ -46,7 +46,7 @@ export async function triage(hits, plan, model, options = {}) {
|
|
|
46
46
|
if (!unique.length)
|
|
47
47
|
return [];
|
|
48
48
|
const planFile = options.planFile ?? defaultPlanFile(plan.ask.kind);
|
|
49
|
-
const out = await model.ask(triagePrompt(unique, plan, planFile, options.yieldRows ?? []), TriageAnswer);
|
|
49
|
+
const { value: out } = await model.ask(triagePrompt(unique, plan, planFile, options.yieldRows ?? []), TriageAnswer);
|
|
50
50
|
const said = new Map(out.hits.map((h) => [h.url.trim(), h]));
|
|
51
51
|
let reads = 0;
|
|
52
52
|
return unique.map((h) => {
|
package/dist/types.d.ts
CHANGED
|
@@ -195,6 +195,8 @@ export declare const HitView: z.ZodObject<{
|
|
|
195
195
|
sources: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
196
196
|
}, z.core.$strip>;
|
|
197
197
|
export type HitView = z.infer<typeof HitView>;
|
|
198
|
+
/** A hit as a caller may pass it: `sources` optional. */
|
|
199
|
+
export type HitViewIn = z.input<typeof HitView>;
|
|
198
200
|
export declare const Triage: z.ZodObject<{
|
|
199
201
|
url: z.ZodString;
|
|
200
202
|
read: z.ZodBoolean;
|
|
@@ -235,6 +237,7 @@ export declare const Round: z.ZodObject<{
|
|
|
235
237
|
}, z.core.$strip>;
|
|
236
238
|
export type Round = z.infer<typeof Round>;
|
|
237
239
|
export declare const Step: z.ZodEnum<{
|
|
240
|
+
triage: "triage";
|
|
238
241
|
read: "read";
|
|
239
242
|
plan: "plan";
|
|
240
243
|
fetch: "fetch";
|
|
@@ -249,6 +252,7 @@ export declare const Edge: z.ZodObject<{
|
|
|
249
252
|
back: "back";
|
|
250
253
|
}>;
|
|
251
254
|
to: z.ZodOptional<z.ZodEnum<{
|
|
255
|
+
triage: "triage";
|
|
252
256
|
read: "read";
|
|
253
257
|
plan: "plan";
|
|
254
258
|
fetch: "fetch";
|
package/dist/types.js
CHANGED
|
@@ -121,7 +121,7 @@ export const Round = z.object({
|
|
|
121
121
|
/** What went wrong or ran out, one line each. */
|
|
122
122
|
notes: z.array(z.string()).default([]),
|
|
123
123
|
});
|
|
124
|
-
export const Step = z.enum(["plan", "fetch", "read", "extract"]);
|
|
124
|
+
export const Step = z.enum(["plan", "fetch", "triage", "read", "extract"]);
|
|
125
125
|
/** The edge out of a round. `again` is a new round from `plan`; `back` reruns one step of this round. */
|
|
126
126
|
export const Edge = z.object({
|
|
127
127
|
next: z.enum(["stop", "again", "back"]),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrenautomation/raven",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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": {
|