@wrenautomation/raven 0.2.0 → 0.2.2

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 CHANGED
@@ -12,7 +12,7 @@ Three calls, one record each:
12
12
  |---|---|---|
13
13
  | `plan` | an ask (text, kind, what is known), proposals from people or other models, yield rows from earlier runs | a `Plan`: typed queries, each with `why`, `from` (ai or human) and `by` (model id, plan file or person), the sources it may use, the budget, and a snapshot of what raven was given |
14
14
  | `triage` | hits as a source shows them for free (title, channel, length, date, snippet, which sources returned it) | read or skip per hit, with why; the read budget caps it |
15
- | `review` | what a round came to (counts, found lines, notes) | the edge: `stop`, `again` (a new round from plan) or `back` (rerun one step), with why |
15
+ | `review` | what a round came to (counts, found lines, the extractor's verdict and gap per read, notes) | the edge: `stop`, `again` (a new round from plan) or `back` (rerun one step), with why |
16
16
 
17
17
  Ask kinds: `web`, `video`, `channel`, `person`, `site`. Each has a default plan file
18
18
  (prompts, sources, budget). Copy one, tune it, keep it beside your ledger: that is where a run
@@ -23,7 +23,8 @@ learns. Raven never loops; the runner takes the edge.
23
23
  Needs Node 24+. The model is a command that reads the prompt on stdin and prints the answer.
24
24
  Default `claude -p --output-format json`, so Claude Code is the model in any terminal and its
25
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
+ `--no-model` plans from your own queries only. A command that says nothing for 3 minutes is
27
+ killed and the call fails with why.
27
28
 
28
29
  ```sh
29
30
  npm i @wrenautomation/raven
package/dist/model.d.ts CHANGED
@@ -31,7 +31,9 @@ export declare function readOutput(stdout: string): {
31
31
  /**
32
32
  * A model that is a shell command: the prompt on stdin, the answer on stdout. When stdout is
33
33
  * Claude Code's JSON envelope (`claude -p --output-format json`) the usage comes with it.
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.
34
36
  */
35
- export declare function commandModel(command: string, id?: string): Model;
37
+ export declare function commandModel(command: string, id?: string, timeoutMs?: number): Model;
36
38
  /** A model that answers from a fixed list, in order: tests and dry runs. */
37
39
  export declare function scriptedModel(answers: unknown[], id?: string): Model;
package/dist/model.js CHANGED
@@ -72,17 +72,30 @@ export function readOutput(stdout) {
72
72
  /**
73
73
  * A model that is a shell command: the prompt on stdin, the answer on stdout. When stdout is
74
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.
75
77
  */
76
- export function commandModel(command, id = command) {
78
+ export function commandModel(command, id = command, timeoutMs = 180_000) {
77
79
  return {
78
80
  id,
79
81
  ask: (prompt, shape) => new Promise((resolve, reject) => {
80
82
  const child = spawn(command, { shell: true, stdio: ["pipe", "pipe", "inherit"] });
81
83
  const out = [];
84
+ let timedOut = false;
85
+ const timer = setTimeout(() => {
86
+ timedOut = true;
87
+ child.kill();
88
+ }, timeoutMs);
82
89
  child.stdout.on("data", (b) => out.push(b));
83
- child.on("error", reject);
90
+ child.on("error", (e) => {
91
+ clearTimeout(timer);
92
+ reject(e);
93
+ });
84
94
  child.on("close", (code) => {
95
+ clearTimeout(timer);
85
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`));
86
99
  if (code !== 0)
87
100
  return reject(new Error(`${command} exited ${code}: ${stdout.slice(0, 400)}`));
88
101
  try {
@@ -25,7 +25,8 @@ ${SHAPE_EDGE}
25
25
 
26
26
  Rules:
27
27
  - "stop" when the found lines answer the ask, or when another round would likely add nothing.
28
- - "again" when the ask is still open and new queries could find more. Say what to look for next in "why".
28
+ - "again" when the ask is still open and new queries could find more. The gaps say what is missing; say what to look for next in "why".
29
+ - "again" too when the reads were mostly "thin" or "off": the queries found the wrong pages, so plan different ones.
29
30
  - "back" when one step of this round failed for a reason a rerun fixes (a source was down, reads timed out). Name the step in "to".
30
31
  - "why" is one short line a person reads to agree or not.
31
32
 
@@ -33,12 +34,14 @@ Ask: {ask}
33
34
  Round {round} of at most {rounds}. Hits {hits}, reads {reads}, findings proposed {proposed}, kept {kept}.
34
35
  Found:
35
36
  {found}
37
+ Read (quality: rich, thin, off or empty; then the gap, what the ask still lacks):
38
+ {verdicts}
36
39
  Notes:
37
40
  {notes}`;
38
41
  const web = {
39
42
  id: "raven/web",
40
43
  kind: "web",
41
- version: 1,
44
+ version: 2,
42
45
  propose: `You plan web searches for one research ask.
43
46
 
44
47
  ${SHAPE_QUERIES}
@@ -69,7 +72,7 @@ Hits:
69
72
  const video = {
70
73
  id: "raven/video",
71
74
  kind: "video",
72
- version: 1,
75
+ version: 2,
73
76
  propose: `You plan video searches for one research ask: talks, walkthroughs, interviews and reviews on YouTube and podcast feeds.
74
77
 
75
78
  ${SHAPE_QUERIES}
@@ -100,7 +103,7 @@ Hits:
100
103
  const channel = {
101
104
  id: "raven/channel",
102
105
  kind: "channel",
103
- version: 1,
106
+ version: 2,
104
107
  propose: `You plan searches that find the people and channels worth following for one research ask: creators, newsletters, podcasts, forums.
105
108
 
106
109
  ${SHAPE_QUERIES}
@@ -129,7 +132,7 @@ Hits:
129
132
  const person = {
130
133
  id: "raven/person",
131
134
  kind: "person",
132
- version: 1,
135
+ version: 2,
133
136
  propose: `You plan searches about one person or company for one research ask: what they do, what they said, what others say about them.
134
137
 
135
138
  ${SHAPE_QUERIES}
@@ -160,7 +163,7 @@ Hits:
160
163
  const site = {
161
164
  id: "raven/site",
162
165
  kind: "site",
163
- version: 1,
166
+ version: 2,
164
167
  propose: `You plan searches inside one website for one research ask. Every query runs on {site} only.
165
168
 
166
169
  ${SHAPE_QUERIES}
package/dist/review.js CHANGED
@@ -12,6 +12,7 @@ const EdgeAnswer = z.object({
12
12
  to: Step.optional(),
13
13
  why: z.string(),
14
14
  });
15
+ const verdictLine = (v) => `${v.url}: ${v.quality}${v.gap ? `. ${v.gap}` : ""}`;
15
16
  export function reviewPrompt(round, planFile) {
16
17
  return fill(planFile.review, {
17
18
  ask: round.ask.text,
@@ -23,6 +24,7 @@ export function reviewPrompt(round, planFile) {
23
24
  kept: String(round.kept),
24
25
  found: lines(round.found),
25
26
  notes: lines(round.notes, "(none)"),
27
+ verdicts: lines(round.verdicts.map(verdictLine), "(none)"),
26
28
  });
27
29
  }
28
30
  export async function review(round, model, options = {}) {
package/dist/types.d.ts CHANGED
@@ -208,7 +208,19 @@ export declare const Triage: z.ZodObject<{
208
208
  by: z.ZodString;
209
209
  }, z.core.$strip>;
210
210
  export type Triage = z.infer<typeof Triage>;
211
- /** What one round came to, as counts and notes. The runner fills it; raven reads it. */
211
+ /** The extractor's word on one read document: how much of the ask it served, what is still missing. */
212
+ export declare const Verdict: z.ZodObject<{
213
+ url: z.ZodString;
214
+ quality: z.ZodEnum<{
215
+ rich: "rich";
216
+ thin: "thin";
217
+ off: "off";
218
+ empty: "empty";
219
+ }>;
220
+ gap: z.ZodOptional<z.ZodString>;
221
+ }, z.core.$strip>;
222
+ export type Verdict = z.infer<typeof Verdict>;
223
+ /** What one round came to, as counts, verdicts and notes. The runner fills it; raven reads it. */
212
224
  export declare const Round: z.ZodObject<{
213
225
  ask: z.ZodObject<{
214
226
  text: z.ZodString;
@@ -234,6 +246,16 @@ export declare const Round: z.ZodObject<{
234
246
  kept: z.ZodNumber;
235
247
  found: z.ZodDefault<z.ZodArray<z.ZodString>>;
236
248
  notes: z.ZodDefault<z.ZodArray<z.ZodString>>;
249
+ verdicts: z.ZodDefault<z.ZodArray<z.ZodObject<{
250
+ url: z.ZodString;
251
+ quality: z.ZodEnum<{
252
+ rich: "rich";
253
+ thin: "thin";
254
+ off: "off";
255
+ empty: "empty";
256
+ }>;
257
+ gap: z.ZodOptional<z.ZodString>;
258
+ }, z.core.$strip>>>;
237
259
  }, z.core.$strip>;
238
260
  export type Round = z.infer<typeof Round>;
239
261
  export declare const Step: z.ZodEnum<{
package/dist/types.js CHANGED
@@ -107,7 +107,13 @@ export const Triage = z.object({
107
107
  from: From,
108
108
  by: z.string(),
109
109
  });
110
- /** What one round came to, as counts and notes. The runner fills it; raven reads it. */
110
+ /** The extractor's word on one read document: how much of the ask it served, what is still missing. */
111
+ export const Verdict = z.object({
112
+ url: z.string(),
113
+ quality: z.enum(["rich", "thin", "off", "empty"]),
114
+ gap: z.string().optional(),
115
+ });
116
+ /** What one round came to, as counts, verdicts and notes. The runner fills it; raven reads it. */
111
117
  export const Round = z.object({
112
118
  ask: Ask,
113
119
  round: z.number().int().positive(),
@@ -120,6 +126,8 @@ export const Round = z.object({
120
126
  found: z.array(z.string()).default([]),
121
127
  /** What went wrong or ran out, one line each. */
122
128
  notes: z.array(z.string()).default([]),
129
+ /** The extractor's verdict on each document read this round: the planner sees what the reads were worth. */
130
+ verdicts: z.array(Verdict).default([]),
123
131
  });
124
132
  export const Step = z.enum(["plan", "fetch", "triage", "read", "extract"]);
125
133
  /** The edge out of a round. `again` is a new round from `plan`; `back` reruns one step of this round. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrenautomation/raven",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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": {