@juanhuttemann/pi-deep-research 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +3 -2
- package/src/index.ts +19 -9
- package/src/research.ts +36 -4
- package/src/settings.ts +7 -3
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ runs [deep-research](https://github.com/juanhuttemann/deep-research)
|
|
|
6
6
|
evidence, and checks every claim against the text of the pages it fetched.
|
|
7
7
|
Pi's own model answers the model calls, through Pi's sign-in.
|
|
8
8
|
|
|
9
|
+

|
|
10
|
+
|
|
9
11
|
## Install
|
|
10
12
|
|
|
11
13
|
`drsh` must be installed (0.30.0 or newer):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juanhuttemann/pi-deep-research",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Fact-checked, cited web research inside Pi: runs the deep-research CLI (drsh) on Pi's own model, with live progress, clarifying questions and plan review",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"pi": {
|
|
27
27
|
"extensions": [
|
|
28
28
|
"./src/index.ts"
|
|
29
|
-
]
|
|
29
|
+
],
|
|
30
|
+
"image": "https://raw.githubusercontent.com/juanhuttemann/deep-research/main/docs/images/pi-research.png"
|
|
30
31
|
},
|
|
31
32
|
"scripts": {
|
|
32
33
|
"typecheck": "tsc --noEmit",
|
package/src/index.ts
CHANGED
|
@@ -120,7 +120,10 @@ export default function deepResearch(pi: ExtensionAPI) {
|
|
|
120
120
|
let settings: Setting[] = [];
|
|
121
121
|
let loadError = "";
|
|
122
122
|
let choice: ModelChoice = { kind: "pi" };
|
|
123
|
-
|
|
123
|
+
// lastReport is the last run that wrote a .json, what research_ask reads
|
|
124
|
+
// by default: a run stopped or failed after it leaves none, and must not
|
|
125
|
+
// hide the one before.
|
|
126
|
+
let lastReport: RunRecord | undefined;
|
|
124
127
|
let commandRun: AbortController | undefined;
|
|
125
128
|
|
|
126
129
|
async function loadSettings(): Promise<Setting[]> {
|
|
@@ -132,6 +135,10 @@ export default function deepResearch(pi: ExtensionAPI) {
|
|
|
132
135
|
}
|
|
133
136
|
settings = parseSettings(r.stdout);
|
|
134
137
|
loadError = "";
|
|
138
|
+
// Wherever drsh first answers (the session's start, /research,
|
|
139
|
+
// /research-doctor, the stand-in), the real tools replace the
|
|
140
|
+
// stand-in: drsh installed mid-session is usable from then on.
|
|
141
|
+
registerTools(settings);
|
|
135
142
|
} catch (err) {
|
|
136
143
|
const e = err as NodeJS.ErrnoException;
|
|
137
144
|
loadError = e.code === "ENOENT" ? `drsh was not found (${bin}).\n${installHelp}` : e.message;
|
|
@@ -189,7 +196,7 @@ export default function deepResearch(pi: ExtensionAPI) {
|
|
|
189
196
|
|
|
190
197
|
function remember(rec: RunRecord | undefined): void {
|
|
191
198
|
if (!rec) return;
|
|
192
|
-
|
|
199
|
+
if (rec.json) lastReport = rec;
|
|
193
200
|
pi.appendEntry(runEntry, rec);
|
|
194
201
|
}
|
|
195
202
|
|
|
@@ -388,9 +395,9 @@ export default function deepResearch(pi: ExtensionAPI) {
|
|
|
388
395
|
model: Type.Optional(Type.String({ description: "provider/id of a Pi model to answer with; default as for deep_research." })),
|
|
389
396
|
}),
|
|
390
397
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
391
|
-
const runFile = params.run ||
|
|
398
|
+
const runFile = params.run || lastReport?.json;
|
|
392
399
|
if (!runFile) {
|
|
393
|
-
throw new Error("no research run in this session yet: pass run with a run's .json path, or run deep_research first");
|
|
400
|
+
throw new Error("no research run in this session has written a report yet: pass run with a run's .json path, or run deep_research first");
|
|
394
401
|
}
|
|
395
402
|
const { model, label } = resolveModel(ctx, params.model);
|
|
396
403
|
const { value: r, bridge } = await withEndpoint(ctx, model, signal, (env) =>
|
|
@@ -422,8 +429,7 @@ export default function deepResearch(pi: ExtensionAPI) {
|
|
|
422
429
|
description: "Fact-checked, cited web research with the deep-research CLI (drsh). Unavailable until drsh is installed.",
|
|
423
430
|
parameters: Type.Object({ question: Type.Optional(Type.String()) }),
|
|
424
431
|
async execute() {
|
|
425
|
-
|
|
426
|
-
registerTools(s);
|
|
432
|
+
await loadSettings();
|
|
427
433
|
throw new Error("drsh is available now; call deep_research again");
|
|
428
434
|
},
|
|
429
435
|
});
|
|
@@ -431,14 +437,14 @@ export default function deepResearch(pi: ExtensionAPI) {
|
|
|
431
437
|
|
|
432
438
|
pi.on("session_start", async (_event, ctx) => {
|
|
433
439
|
choice = { kind: "pi" };
|
|
434
|
-
|
|
440
|
+
lastReport = undefined;
|
|
435
441
|
for (const e of ctx.sessionManager.getBranch()) {
|
|
436
442
|
if (e.type !== "custom") continue;
|
|
437
443
|
if (e.customType === modelEntry && e.data) choice = e.data as ModelChoice;
|
|
438
|
-
if (e.customType === runEntry && e.data)
|
|
444
|
+
if (e.customType === runEntry && (e.data as RunRecord | undefined)?.json) lastReport = e.data as RunRecord;
|
|
439
445
|
}
|
|
440
446
|
try {
|
|
441
|
-
|
|
447
|
+
await loadSettings();
|
|
442
448
|
} catch {
|
|
443
449
|
registerStandIn();
|
|
444
450
|
}
|
|
@@ -581,6 +587,10 @@ export default function deepResearch(pi: ExtensionAPI) {
|
|
|
581
587
|
return;
|
|
582
588
|
}
|
|
583
589
|
ctx.ui.setStatus("deep-research", "checking drsh…");
|
|
590
|
+
// The check a user runs after installing drsh: loading its settings
|
|
591
|
+
// brings in the real tools. A drsh that cannot load them is
|
|
592
|
+
// reported by the doctor run below.
|
|
593
|
+
await ensureSettings().catch(() => undefined);
|
|
584
594
|
try {
|
|
585
595
|
const { value: r } = await withEndpoint(ctx, model, undefined, (env) => runPlain(bin, ["doctor"], { env }));
|
|
586
596
|
const verdict = r.code === 0 ? "drsh doctor: ready" : "drsh doctor: something is broken (✗ lines)";
|
package/src/research.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// to ask (clarify off, instant), so this knows no setting by name.
|
|
5
5
|
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { newRunArgs, planRunArgs, resumeArgs, type Answer, type NewRun } from "./args.ts";
|
|
10
10
|
import { initialState, reduce, type Question } from "./events.ts";
|
|
@@ -150,14 +150,46 @@ async function readJSON(path: string | undefined): Promise<unknown> {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// checkpointLeft is the checkpoint a run that ended with no done line left
|
|
154
|
+
// on disk. drsh saves it after each step, as <reports>/*.partial.json, and
|
|
155
|
+
// names it only in the done line, which a killed run never writes: on
|
|
156
|
+
// Windows, where stopping a child process kills it outright, that is every
|
|
157
|
+
// stopped run. Each run has a reports directory of its own, so a checkpoint
|
|
158
|
+
// there is this run's; a resumed run saves to the checkpoint it resumed. A
|
|
159
|
+
// file that is not one drsh can resume from (no question or no plan, as
|
|
160
|
+
// replay.Load refuses) is not offered.
|
|
161
|
+
async function checkpointLeft(c: Call): Promise<string | undefined> {
|
|
162
|
+
let candidates: string[];
|
|
163
|
+
if (c.req.kind === "resume") {
|
|
164
|
+
candidates = [c.req.checkpoint];
|
|
165
|
+
} else {
|
|
166
|
+
const names = await readdir(c.reports).catch(() => [] as string[]);
|
|
167
|
+
candidates = names.filter((n) => n.endsWith(".partial.json")).map((n) => join(c.reports, n));
|
|
168
|
+
}
|
|
169
|
+
const valid: { path: string; mtime: number }[] = [];
|
|
170
|
+
for (const path of candidates) {
|
|
171
|
+
if (resumable(await readJSON(path))) {
|
|
172
|
+
valid.push({ path, mtime: (await stat(path)).mtimeMs });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return valid.sort((a, b) => b.mtime - a.mtime)[0]?.path;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function resumable(raw: unknown): boolean {
|
|
179
|
+
const t = raw as { question?: unknown; plan?: unknown } | null | undefined;
|
|
180
|
+
return typeof t?.question === "string" && t.question.trim() !== "" && Array.isArray(t.plan) && t.plan.length > 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
153
183
|
// outcome reads what a run's done line says it wrote.
|
|
154
|
-
async function outcome(
|
|
184
|
+
async function outcome(c: Call, r: JsonlResult): Promise<{ o: Outcome; isError: boolean }> {
|
|
185
|
+
const model = c.model;
|
|
155
186
|
if (!r.done) {
|
|
156
187
|
const status = r.aborted ? "cancelled" : "failed";
|
|
157
188
|
const detail = r.aborted
|
|
158
189
|
? "the run was stopped before it could end cleanly"
|
|
159
190
|
: `drsh exited (code ${r.code}) without a done line, so it was killed or crashed${r.stderr ? `: ${r.stderr.trim().slice(-1500)}` : ""}`;
|
|
160
|
-
|
|
191
|
+
const checkpoint = await checkpointLeft(c);
|
|
192
|
+
return { o: { status, detail, artifacts: [], model, checkpoint }, isError: !r.aborted };
|
|
161
193
|
}
|
|
162
194
|
const artifacts = r.done.artifacts ?? [];
|
|
163
195
|
const md = artifacts.find((p) => p.endsWith(".md"));
|
|
@@ -210,6 +242,6 @@ export async function research(c: Call): Promise<Result> {
|
|
|
210
242
|
details.state = initialState();
|
|
211
243
|
c.hooks.update(details);
|
|
212
244
|
const r = await stage(c, details, args);
|
|
213
|
-
const { o, isError } = await outcome(
|
|
245
|
+
const { o, isError } = await outcome(c, r);
|
|
214
246
|
return { text: content(o), isError, ...finish(details, o, details.question) };
|
|
215
247
|
}
|
package/src/settings.ts
CHANGED
|
@@ -144,7 +144,10 @@ export function parseCommandArgs(settings: Setting[], input: string): CommandArg
|
|
|
144
144
|
rest.push(w);
|
|
145
145
|
continue;
|
|
146
146
|
}
|
|
147
|
-
|
|
147
|
+
// Split at the first "=" only: a value may hold one.
|
|
148
|
+
const eq = w.indexOf("=");
|
|
149
|
+
const name = eq < 0 ? w.slice(2) : w.slice(2, eq);
|
|
150
|
+
let v: string | undefined = eq < 0 ? undefined : w.slice(eq + 1);
|
|
148
151
|
if (v === undefined) {
|
|
149
152
|
v = words[++i];
|
|
150
153
|
if (v === undefined) {
|
|
@@ -175,7 +178,8 @@ export interface Completion {
|
|
|
175
178
|
description?: string;
|
|
176
179
|
}
|
|
177
180
|
|
|
178
|
-
// completions offers the option names, then a named option's choices
|
|
181
|
+
// completions offers the option names, then a named option's choices; an
|
|
182
|
+
// option is named by its key or its CLI flag, as parseCommandArgs reads it.
|
|
179
183
|
export function completions(settings: Setting[], prefix: string): Completion[] {
|
|
180
184
|
const words = prefix.split(/\s+/);
|
|
181
185
|
const last = words[words.length - 1] ?? "";
|
|
@@ -183,7 +187,7 @@ export function completions(settings: Setting[], prefix: string): Completion[] {
|
|
|
183
187
|
const head = before ? `${before} ` : "";
|
|
184
188
|
const prev = words[words.length - 2] ?? "";
|
|
185
189
|
if (prev.startsWith("--")) {
|
|
186
|
-
const s = settings.find((x) => x.key === prev.slice(2));
|
|
190
|
+
const s = settings.find((x) => x.key === prev.slice(2) || x.flag === prev.slice(2));
|
|
187
191
|
if (s) {
|
|
188
192
|
return s.choices
|
|
189
193
|
.filter((o) => o.label.startsWith(last))
|