@wrenautomation/raven 0.2.1 → 0.3.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,9 +10,9 @@ 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 |
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 |
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
+ | `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
@@ -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
@@ -40,6 +40,7 @@ withModel(program
40
40
  .option("-k, --kind <kind>", `one of ${PLAN_KINDS.join(", ")}`, "web")
41
41
  .option("--site <host>", "keep every query on one host (kind site)")
42
42
  .option("--known <line>", "a fact already in hand; repeat", more, [])
43
+ .option("--steer <line>", "what you say to the model, rendered first; repeat", more, [])
43
44
  .option("--propose <terms>", "a query of your own; repeat", more, [])
44
45
  .option("--by <name>", "who proposed the --propose queries", who())
45
46
  .option("--plan-file <path>", "a tuned plan file (JSON) instead of the default")
@@ -47,7 +48,13 @@ withModel(program
47
48
  .option("--sources <names>", "narrow the plan file's sources, comma separated")
48
49
  .option("--round <n>", "round number", "1")).action(async (text, o) => {
49
50
  const kind = AskKind.parse(o.kind);
50
- const ask = { text, kind, known: o.known, ...(o.site ? { site: o.site } : {}) };
51
+ const ask = {
52
+ text,
53
+ kind,
54
+ known: o.known,
55
+ steer: o.steer,
56
+ ...(o.site ? { site: o.site } : {}),
57
+ };
51
58
  const proposals = o.propose.length
52
59
  ? [
53
60
  {
package/dist/plan.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { fill } from "./model.js";
7
7
  import { defaultPlanFile } from "./plans/index.js";
8
- import { lines, renderYield } from "./render.js";
8
+ import { lines, renderSteer, renderYield } from "./render.js";
9
9
  import { z, } from "./types.js";
10
10
  const ProposalAnswer = z.object({
11
11
  queries: z.array(z.object({ terms: z.string(), why: z.string(), site: z.string().optional() })),
@@ -16,6 +16,7 @@ export function proposePrompt(ask, planFile, yieldRows, now) {
16
16
  year: String(now.getUTCFullYear()),
17
17
  ask: ask.text,
18
18
  site: ask.site ?? "",
19
+ steer: renderSteer(ask.steer),
19
20
  known: lines(ask.known),
20
21
  yield: renderYield(yieldRows),
21
22
  });
@@ -6,39 +6,42 @@ 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}
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
 
32
33
  Ask: {ask}
33
- 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}.
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: 3,
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: 3,
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: 3,
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: 3,
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: 3,
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/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.js CHANGED
@@ -5,16 +5,18 @@
5
5
  */
6
6
  import { fill } from "./model.js";
7
7
  import { defaultPlanFile } from "./plans/index.js";
8
- import { lines } from "./render.js";
8
+ import { lines, renderSteer } from "./render.js";
9
9
  import { Step, z } from "./types.js";
10
10
  const EdgeAnswer = z.object({
11
11
  next: z.enum(["stop", "again", "back"]),
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,
19
+ steer: renderSteer(round.ask.steer),
18
20
  round: String(round.round),
19
21
  rounds: String(round.budget.rounds),
20
22
  hits: String(round.hits),
@@ -23,6 +25,7 @@ export function reviewPrompt(round, planFile) {
23
25
  kept: String(round.kept),
24
26
  found: lines(round.found),
25
27
  notes: lines(round.notes, "(none)"),
28
+ verdicts: lines(round.verdicts.map(verdictLine), "(none)"),
26
29
  });
27
30
  }
28
31
  export async function review(round, model, options = {}) {
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
7
  import { type Model } from "./model.js";
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,18 @@
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
7
  import { fill } from "./model.js";
7
8
  import { defaultPlanFile } from "./plans/index.js";
8
- import { lines, renderHits, renderYield } from "./render.js";
9
+ import { lines, renderHits, renderSteer, renderYield } from "./render.js";
9
10
  import { HitView, z, } from "./types.js";
10
11
  const TriageAnswer = z.object({
11
- hits: z.array(z.object({ url: z.string(), read: z.boolean(), why: z.string() })),
12
+ hits: z.array(z.object({ url: z.string(), read: z.boolean(), why: z.string().nullable().optional() })),
12
13
  });
14
+ /** What a skip says: the model named it and passed. */
15
+ export const SKIPPED = "skipped by the model";
13
16
  /** One hit per url, sources merged, order of first sight. */
14
17
  export function dedupeHits(hits) {
15
18
  const byUrl = new Map();
@@ -35,6 +38,7 @@ export function triagePrompt(hits, plan, planFile, yieldRows) {
35
38
  n: String(plan.budget.reads),
36
39
  ask: plan.ask.text,
37
40
  site: plan.ask.site ?? "",
41
+ steer: renderSteer(plan.ask.steer),
38
42
  known: lines(plan.ask.known),
39
43
  yield: renderYield(yieldRows),
40
44
  hits: renderHits(hits),
@@ -55,11 +59,11 @@ export async function triage(hits, plan, model, options = {}) {
55
59
  if (!a)
56
60
  return { ...base, read: false, why: "not named by the model" };
57
61
  if (!a.read)
58
- return { ...base, read: false, why: a.why };
62
+ return { ...base, read: false, why: a.why?.trim() || SKIPPED };
59
63
  if (reads >= plan.budget.reads)
60
64
  return { ...base, read: false, why: `over the read budget (${plan.budget.reads})` };
61
65
  reads++;
62
- return { ...base, read: true, why: a.why };
66
+ return { ...base, read: true, why: a.why?.trim() || "read" };
63
67
  });
64
68
  }
65
69
  //# 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;
@@ -208,7 +210,19 @@ export declare const Triage: z.ZodObject<{
208
210
  by: z.ZodString;
209
211
  }, z.core.$strip>;
210
212
  export type Triage = z.infer<typeof Triage>;
211
- /** What one round came to, as counts and notes. The runner fills it; raven reads it. */
213
+ /** The extractor's word on one read document: how much of the ask it served, what is still missing. */
214
+ export declare const Verdict: z.ZodObject<{
215
+ url: z.ZodString;
216
+ quality: z.ZodEnum<{
217
+ rich: "rich";
218
+ thin: "thin";
219
+ off: "off";
220
+ empty: "empty";
221
+ }>;
222
+ gap: z.ZodOptional<z.ZodString>;
223
+ }, z.core.$strip>;
224
+ export type Verdict = z.infer<typeof Verdict>;
225
+ /** What one round came to, as counts, verdicts and notes. The runner fills it; raven reads it. */
212
226
  export declare const Round: z.ZodObject<{
213
227
  ask: z.ZodObject<{
214
228
  text: z.ZodString;
@@ -220,6 +234,7 @@ export declare const Round: z.ZodObject<{
220
234
  site: "site";
221
235
  }>;
222
236
  known: z.ZodDefault<z.ZodArray<z.ZodString>>;
237
+ steer: z.ZodDefault<z.ZodArray<z.ZodString>>;
223
238
  site: z.ZodOptional<z.ZodString>;
224
239
  }, z.core.$strip>;
225
240
  round: z.ZodNumber;
@@ -234,6 +249,16 @@ export declare const Round: z.ZodObject<{
234
249
  kept: z.ZodNumber;
235
250
  found: z.ZodDefault<z.ZodArray<z.ZodString>>;
236
251
  notes: z.ZodDefault<z.ZodArray<z.ZodString>>;
252
+ verdicts: z.ZodDefault<z.ZodArray<z.ZodObject<{
253
+ url: z.ZodString;
254
+ quality: z.ZodEnum<{
255
+ rich: "rich";
256
+ thin: "thin";
257
+ off: "off";
258
+ empty: "empty";
259
+ }>;
260
+ gap: z.ZodOptional<z.ZodString>;
261
+ }, z.core.$strip>>>;
237
262
  }, z.core.$strip>;
238
263
  export type Round = z.infer<typeof Round>;
239
264
  export declare const Step: z.ZodEnum<{
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,11 +105,18 @@ 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(),
109
112
  });
110
- /** What one round came to, as counts and notes. The runner fills it; raven reads it. */
113
+ /** The extractor's word on one read document: how much of the ask it served, what is still missing. */
114
+ export const Verdict = z.object({
115
+ url: z.string(),
116
+ quality: z.enum(["rich", "thin", "off", "empty"]),
117
+ gap: z.string().optional(),
118
+ });
119
+ /** What one round came to, as counts, verdicts and notes. The runner fills it; raven reads it. */
111
120
  export const Round = z.object({
112
121
  ask: Ask,
113
122
  round: z.number().int().positive(),
@@ -120,6 +129,8 @@ export const Round = z.object({
120
129
  found: z.array(z.string()).default([]),
121
130
  /** What went wrong or ran out, one line each. */
122
131
  notes: z.array(z.string()).default([]),
132
+ /** The extractor's verdict on each document read this round: the planner sees what the reads were worth. */
133
+ verdicts: z.array(Verdict).default([]),
123
134
  });
124
135
  export const Step = z.enum(["plan", "fetch", "triage", "read", "extract"]);
125
136
  /** 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.1",
3
+ "version": "0.3.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": {