@wrenautomation/raven 0.2.2 → 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/README.md CHANGED
@@ -10,8 +10,8 @@ Three calls, one record each:
10
10
 
11
11
  | Call | In | Out |
12
12
  |---|---|---|
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
- | `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 |
13
+ | `plan` | an ask (text, kind, what is known, what the person says: `steer`, rendered first in every prompt), 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
+ | `triage` | hits as a source shows them for free (title, channel, length, date, snippet, which sources returned it) | read or skip per hit; the model writes a why on a read only (a skip's is fixed text); the read budget caps it |
15
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
@@ -29,6 +29,7 @@ killed and the call fails with why.
29
29
  ```sh
30
30
  npm i @wrenautomation/raven
31
31
  npx raven plan "how do staffing firms win repeat clients" --known "referrals convert"
32
+ npx raven plan "..." --steer "numbers only, skip vendor pages" # what you say to the model, first in every prompt
32
33
  npx raven plan "..." --kind video --propose "cold email teardown" --by will # yours merge with the model's
33
34
  npx raven triage --plan plan.json --hits hits.json
34
35
  npx raven review --round round.json
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 command that reads the prompt on stdin and prints the answer: `claude -p` by
6
- * default (RAVEN_MODEL or --model to change it), so Claude Code is the planner's model in any
7
- * terminal. Everything is JSON on stdout.
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 -p --output-format json";
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
- const modelOf = (o) => o.model === false
31
- ? null
32
- : commandModel(o.model, o.model === DEFAULT_MODEL ? "claude-code" : o.model);
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 <command>", "a command: prompt on stdin, answer on stdout", process.env.RAVEN_MODEL ?? DEFAULT_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
@@ -40,6 +47,7 @@ withModel(program
40
47
  .option("-k, --kind <kind>", `one of ${PLAN_KINDS.join(", ")}`, "web")
41
48
  .option("--site <host>", "keep every query on one host (kind site)")
42
49
  .option("--known <line>", "a fact already in hand; repeat", more, [])
50
+ .option("--steer <line>", "what you say to the model, rendered first; repeat", more, [])
43
51
  .option("--propose <terms>", "a query of your own; repeat", more, [])
44
52
  .option("--by <name>", "who proposed the --propose queries", who())
45
53
  .option("--plan-file <path>", "a tuned plan file (JSON) instead of the default")
@@ -47,7 +55,13 @@ withModel(program
47
55
  .option("--sources <names>", "narrow the plan file's sources, comma separated")
48
56
  .option("--round <n>", "round number", "1")).action(async (text, o) => {
49
57
  const kind = AskKind.parse(o.kind);
50
- const ask = { text, kind, known: o.known, ...(o.site ? { site: o.site } : {}) };
58
+ const ask = {
59
+ text,
60
+ kind,
61
+ known: o.known,
62
+ steer: o.steer,
63
+ ...(o.site ? { site: o.site } : {}),
64
+ };
51
65
  const proposals = o.propose.length
52
66
  ? [
53
67
  {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { type Answer, commandModel, fill, type Model, parseAnswer, readOutput, scriptedModel, type Usage, } from "./model.js";
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 { commandModel, fill, parseAnswer, readOutput, scriptedModel, } from "./model.js";
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
- * 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.
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
- export declare function commandModel(command: string, id?: string, timeoutMs?: number): Model;
38
- /** A model that answers from a fixed list, in order: tests and dry runs. */
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
- * 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 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.
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 { type Model } from "./model.js";
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,11 +1,6 @@
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
- import { lines, renderYield } from "./render.js";
3
+ import { lines, renderSteer, renderYield } from "./render.js";
9
4
  import { z, } from "./types.js";
10
5
  const ProposalAnswer = z.object({
11
6
  queries: z.array(z.object({ terms: z.string(), why: z.string(), site: z.string().optional() })),
@@ -16,6 +11,7 @@ export function proposePrompt(ask, planFile, yieldRows, now) {
16
11
  year: String(now.getUTCFullYear()),
17
12
  ask: ask.text,
18
13
  site: ask.site ?? "",
14
+ steer: renderSteer(ask.steer),
19
15
  known: lines(ask.known),
20
16
  yield: renderYield(yieldRows),
21
17
  });
@@ -6,19 +6,19 @@ import { PlanFile as PlanFileShape } from "../types.js";
6
6
  const SHAPE_QUERIES = `Return ONLY a JSON object, no prose, exactly this shape:
7
7
  {"queries": [{"terms": "...", "why": "..."}]}`;
8
8
  const SHAPE_HITS = `Return ONLY a JSON object, no prose, exactly this shape:
9
- {"hits": [{"url": "...", "read": true, "why": "..."}]}`;
9
+ {"hits": [{"url": "...", "read": true, "why": "..."}, {"url": "...", "read": false}]}`;
10
10
  const SHAPE_EDGE = `Return ONLY a JSON object, no prose, exactly this shape:
11
11
  {"next": "stop", "why": "..."}
12
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
- Known already (look past these):
14
+ {steer}Known already (look past these):
15
15
  {known}
16
16
  What yielded before (hits, reads, proposed, kept):
17
17
  {yield}`;
18
18
  const TRIAGE_RULES = `- One entry per hit, with its url as given. Mark "read": true for at most {n}.
19
19
  - Read a hit when what it shows for free (title, snippet, channel, length, date) says it holds what the ask needs and is not known already.
20
20
  - A hit returned by more than one source is a good sign, not a duplicate.
21
- - "why" is one short line a person reads to agree or not.`;
21
+ - "why" on a read only: one short line a person reads to agree or not. A skip has no "why".`;
22
22
  const REVIEW = `You decide what happens after one round of research.
23
23
 
24
24
  ${SHAPE_EDGE}
@@ -31,7 +31,7 @@ Rules:
31
31
  - "why" is one short line a person reads to agree or not.
32
32
 
33
33
  Ask: {ask}
34
- Round {round} of at most {rounds}. Hits {hits}, reads {reads}, findings proposed {proposed}, kept {kept}.
34
+ {steer}Round {round} of at most {rounds}. Hits {hits}, reads {reads}, findings proposed {proposed}, kept {kept}.
35
35
  Found:
36
36
  {found}
37
37
  Read (quality: rich, thin, off or empty; then the gap, what the ask still lacks):
@@ -41,7 +41,7 @@ Notes:
41
41
  const web = {
42
42
  id: "raven/web",
43
43
  kind: "web",
44
- version: 2,
44
+ version: 3,
45
45
  propose: `You plan web searches for one research ask.
46
46
 
47
47
  ${SHAPE_QUERIES}
@@ -72,7 +72,7 @@ Hits:
72
72
  const video = {
73
73
  id: "raven/video",
74
74
  kind: "video",
75
- version: 2,
75
+ version: 3,
76
76
  propose: `You plan video searches for one research ask: talks, walkthroughs, interviews and reviews on YouTube and podcast feeds.
77
77
 
78
78
  ${SHAPE_QUERIES}
@@ -103,7 +103,7 @@ Hits:
103
103
  const channel = {
104
104
  id: "raven/channel",
105
105
  kind: "channel",
106
- version: 2,
106
+ version: 3,
107
107
  propose: `You plan searches that find the people and channels worth following for one research ask: creators, newsletters, podcasts, forums.
108
108
 
109
109
  ${SHAPE_QUERIES}
@@ -132,7 +132,7 @@ Hits:
132
132
  const person = {
133
133
  id: "raven/person",
134
134
  kind: "person",
135
- version: 2,
135
+ version: 3,
136
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.
137
137
 
138
138
  ${SHAPE_QUERIES}
@@ -163,7 +163,7 @@ Hits:
163
163
  const site = {
164
164
  id: "raven/site",
165
165
  kind: "site",
166
- version: 2,
166
+ version: 3,
167
167
  propose: `You plan searches inside one website for one research ask. Every query runs on {site} only.
168
168
 
169
169
  ${SHAPE_QUERIES}
package/dist/render.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  /** Text the prompts take: what is known, what yielded, what a hit shows for free. */
2
2
  import type { HitView, YieldRow } from "./types.js";
3
3
  export declare const lines: (xs: readonly string[], none?: string) => string;
4
+ /** "The person says:" and their lines, or nothing: a prompt pays for the heading only when there is one. */
5
+ export declare const renderSteer: (steer: readonly string[] | undefined) => string;
4
6
  /** One line per row, the keys it has first: `source=exa channel=... hits=12 reads=4 proposed=9 kept=3`. */
5
7
  export declare function renderYield(rows: readonly YieldRow[]): string;
6
8
  /** Numbered, one hit per line, only the fields it has. */
package/dist/render.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export const lines = (xs, none = "(nothing)") => xs.length ? xs.map((x) => `- ${x}`).join("\n") : none;
2
+ /** "The person says:" and their lines, or nothing: a prompt pays for the heading only when there is one. */
3
+ export const renderSteer = (steer) => steer?.length ? `The person says:\n${lines(steer)}\n` : "";
2
4
  /** One line per row, the keys it has first: `source=exa channel=... hits=12 reads=4 proposed=9 kept=3`. */
3
5
  export function renderYield(rows) {
4
6
  if (!rows.length)
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 { type Model } from "./model.js";
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,11 +1,6 @@
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
- import { lines } from "./render.js";
3
+ import { lines, renderSteer } from "./render.js";
9
4
  import { Step, z } from "./types.js";
10
5
  const EdgeAnswer = z.object({
11
6
  next: z.enum(["stop", "again", "back"]),
@@ -16,6 +11,7 @@ const verdictLine = (v) => `${v.url}: ${v.quality}${v.gap ? `. ${v.gap}` : ""}`;
16
11
  export function reviewPrompt(round, planFile) {
17
12
  return fill(planFile.review, {
18
13
  ask: round.ask.text,
14
+ steer: renderSteer(round.ask.steer),
19
15
  round: String(round.round),
20
16
  rounds: String(round.budget.rounds),
21
17
  hits: String(round.hits),
package/dist/triage.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * The filter before the read: for each hit, read or skip, from what the hit shows for free.
3
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.
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.
5
6
  */
6
- import { type Model } from "./model.js";
7
+ import type { Model } from "@wrenautomation/models";
7
8
  import { HitView, type HitViewIn, type Plan, type PlanFile, type Triage, type YieldRow } from "./types.js";
9
+ /** What a skip says: the model named it and passed. */
10
+ export declare const SKIPPED = "skipped by the model";
8
11
  /** One hit per url, sources merged, order of first sight. */
9
12
  export declare function dedupeHits(hits: readonly HitViewIn[]): HitView[];
10
13
  export declare function triagePrompt(hits: readonly HitView[], plan: Plan, planFile: PlanFile, yieldRows: readonly YieldRow[]): string;
package/dist/triage.js CHANGED
@@ -1,15 +1,12 @@
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.
5
- */
6
1
  import { fill } from "./model.js";
7
2
  import { defaultPlanFile } from "./plans/index.js";
8
- import { lines, renderHits, renderYield } from "./render.js";
3
+ import { lines, renderHits, renderSteer, renderYield } from "./render.js";
9
4
  import { HitView, z, } from "./types.js";
10
5
  const TriageAnswer = z.object({
11
- hits: z.array(z.object({ url: z.string(), read: z.boolean(), why: z.string() })),
6
+ hits: z.array(z.object({ url: z.string(), read: z.boolean(), why: z.string().nullable().optional() })),
12
7
  });
8
+ /** What a skip says: the model named it and passed. */
9
+ export const SKIPPED = "skipped by the model";
13
10
  /** One hit per url, sources merged, order of first sight. */
14
11
  export function dedupeHits(hits) {
15
12
  const byUrl = new Map();
@@ -35,6 +32,7 @@ export function triagePrompt(hits, plan, planFile, yieldRows) {
35
32
  n: String(plan.budget.reads),
36
33
  ask: plan.ask.text,
37
34
  site: plan.ask.site ?? "",
35
+ steer: renderSteer(plan.ask.steer),
38
36
  known: lines(plan.ask.known),
39
37
  yield: renderYield(yieldRows),
40
38
  hits: renderHits(hits),
@@ -55,11 +53,11 @@ export async function triage(hits, plan, model, options = {}) {
55
53
  if (!a)
56
54
  return { ...base, read: false, why: "not named by the model" };
57
55
  if (!a.read)
58
- return { ...base, read: false, why: a.why };
56
+ return { ...base, read: false, why: a.why?.trim() || SKIPPED };
59
57
  if (reads >= plan.budget.reads)
60
58
  return { ...base, read: false, why: `over the read budget (${plan.budget.reads})` };
61
59
  reads++;
62
- return { ...base, read: true, why: a.why };
60
+ return { ...base, read: true, why: a.why?.trim() || "read" };
63
61
  });
64
62
  }
65
63
  //# sourceMappingURL=triage.js.map
package/dist/types.d.ts CHANGED
@@ -30,6 +30,7 @@ export declare const Ask: z.ZodObject<{
30
30
  site: "site";
31
31
  }>;
32
32
  known: z.ZodDefault<z.ZodArray<z.ZodString>>;
33
+ steer: z.ZodDefault<z.ZodArray<z.ZodString>>;
33
34
  site: z.ZodOptional<z.ZodString>;
34
35
  }, z.core.$strip>;
35
36
  export type Ask = z.infer<typeof Ask>;
@@ -146,6 +147,7 @@ export declare const Plan: z.ZodObject<{
146
147
  site: "site";
147
148
  }>;
148
149
  known: z.ZodDefault<z.ZodArray<z.ZodString>>;
150
+ steer: z.ZodDefault<z.ZodArray<z.ZodString>>;
149
151
  site: z.ZodOptional<z.ZodString>;
150
152
  }, z.core.$strip>;
151
153
  round: z.ZodNumber;
@@ -232,6 +234,7 @@ export declare const Round: z.ZodObject<{
232
234
  site: "site";
233
235
  }>;
234
236
  known: z.ZodDefault<z.ZodArray<z.ZodString>>;
237
+ steer: z.ZodDefault<z.ZodArray<z.ZodString>>;
235
238
  site: z.ZodOptional<z.ZodString>;
236
239
  }, z.core.$strip>;
237
240
  round: z.ZodNumber;
package/dist/types.js CHANGED
@@ -14,6 +14,8 @@ export const Ask = z.object({
14
14
  kind: AskKind,
15
15
  /** Facts already in hand, one line each: the planner looks past them. */
16
16
  known: z.array(z.string()).default([]),
17
+ /** What the person said along the way, one line each: every prompt renders it first. */
18
+ steer: z.array(z.string()).default([]),
17
19
  /** Only for kind `site`, or when a query must stay on one host. */
18
20
  site: z.string().optional(),
19
21
  });
@@ -103,6 +105,7 @@ export const HitView = z.object({
103
105
  export const Triage = z.object({
104
106
  url: z.string(),
105
107
  read: z.boolean(),
108
+ /** The model's line on a read; a skip's is fixed text, so the model writes none. */
106
109
  why: z.string(),
107
110
  from: From,
108
111
  by: z.string(),
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@wrenautomation/raven",
3
- "version": "0.2.2",
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/raven.git"
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
- "devDependencies": {
47
- "@biomejs/biome": "2.5.14",
48
- "@types/node": "22.20.3",
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
+ }