@wrenautomation/raven 0.2.2 → 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,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
@@ -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,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.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 } 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"]),
@@ -16,6 +16,7 @@ const verdictLine = (v) => `${v.url}: ${v.quality}${v.gap ? `. ${v.gap}` : ""}`;
16
16
  export function reviewPrompt(round, planFile) {
17
17
  return fill(planFile.review, {
18
18
  ask: round.ask.text,
19
+ steer: renderSteer(round.ask.steer),
19
20
  round: String(round.round),
20
21
  rounds: String(round.budget.rounds),
21
22
  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
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;
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrenautomation/raven",
3
- "version": "0.2.2",
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": {