@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 +3 -2
- package/dist/app/cli.js +24 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/model.d.ts +4 -37
- package/dist/model.js +2 -121
- package/dist/plan.d.ts +1 -1
- package/dist/plan.js +2 -6
- package/dist/plans/index.js +9 -9
- package/dist/render.d.ts +2 -0
- package/dist/render.js +2 -0
- package/dist/review.d.ts +1 -1
- package/dist/review.js +2 -6
- package/dist/triage.d.ts +5 -2
- package/dist/triage.js +7 -9
- package/dist/types.d.ts +3 -0
- package/dist/types.js +3 -0
- package/package.json +9 -20
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
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 <
|
|
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 = {
|
|
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 {
|
|
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 {
|
|
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
|
-
*
|
|
33
|
-
*
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
*
|
|
3
|
-
*
|
|
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 {
|
|
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
|
});
|
package/dist/plans/index.js
CHANGED
|
@@ -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"
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 {
|
|
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 {
|
|
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.
|
|
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/
|
|
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
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
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
|
+
}
|