@lincoln504/pi-research 1.7.1 → 1.7.3
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 +8 -0
- package/agent-skill/pi-research/README.md +9 -0
- package/agent-skill/pi-research/SKILL.md +1 -1
- package/agent-skill/pi-research/references/configuration.md +2 -1
- package/agent-skill/pi-research/scripts/run.mjs +35 -6
- package/dist/cli.mjs +83 -13
- package/dist/prompts/quick-researcher.md +48 -0
- package/dist/thread-worker.mjs +62 -6
- package/docs/AGENT-SKILL.md +9 -0
- package/docs/ARCHITECTURE.md +25 -9
- package/docs/CHANGELOG.md +34 -0
- package/docs/CONFIGURATION.md +2 -1
- package/docs/SDK.md +8 -0
- package/package.json +19 -13
- package/src/config.ts +12 -2
- package/src/core/llm/llm-utils.ts +7 -2
- package/src/core/pi-version.ts +14 -3
- package/src/index.ts +39 -10
- package/src/infrastructure/browser/thread-worker.mjs +62 -6
- package/src/orchestration/quick-research-orchestrator.ts +18 -9
- package/src/orchestration/researcher.ts +4 -0
- package/src/prompts/quick-researcher.md +48 -0
- package/src/sdk.ts +2 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/scrape.ts +9 -1
- package/src/utils/log-utils.ts +4 -1
- package/src/utils/tool-exec-context.ts +29 -0
- package/src/web-research/scraper-types.ts +7 -0
- package/src/web-research/scraper-utils.ts +73 -2
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.7.3] - 2026-09-30
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Quick (depth 0) research is now shaped like a quick answer, not a report.** Depth 0 is for a single verifiable fact, but it inherited the deep path's volume and framing: five search queries, eight URLs per scrape batch, and the deep `researcher.md` system prompt with two placeholders blanked — a prompt that asks for an exhaustive, critically-deconstructed report, advertises scrape rounds 2+ and a sibling "Session URL Pool" a single-pass run cannot have, and states that omitting information is a failure. Three changes, all scoped to quick runs (the pi extension tool, the CLI and the SDK's `--depth 0`; the deep path is untouched):
|
|
13
|
+
- `QUICK_MAX_QUERIES` default `5` → `3` (range unchanged, 1–10).
|
|
14
|
+
- New `QUICK_MAX_SCRAPE_URLS` (`PI_RESEARCH_QUICK_MAX_SCRAPE_URLS`, default `5`, range 1–20) caps the `scrape` tool's URLs per call for quick runs, plumbed through `createResearcherSession` → `createResearchTools` → `createScrapeTool` the way `maxSearchQueries` already was, so the advertised cap, the per-call slice and the over-cap report stay one number. `PI_RESEARCH_MAX_SCRAPE_URLS` (`8`) remains the deep cap, and `PI_RESEARCH_MAX_SCRAPE_BATCHES` still caps the batch count, so a quick run's scrape ceiling falls from 2×8 to 2×5 URLs.
|
|
15
|
+
- New `src/prompts/quick-researcher.md`: single-pass, answer-first, "stop once the answer is sourced", with every hard requirement the engine depends on kept intact (grounding, `[N]` citations, the `CITED LINKS` format `parseCitations` parses, the untrusted-content rule, and the `RESEARCHER_AGENT_MARKER` the host hook keys on to skip steering injection). The deep `researcher.md` is unchanged, so its byte-identical-across-researchers prompt-cache property is unaffected.
|
|
16
|
+
- **pi packages verified against 0.99.2; the floor moves `0.87.0` → `0.99.0` and the tested ceiling to 0.99.2.** `@earendil-works/pi-ai`, `pi-coding-agent` and `pi-tui` move `>=0.87.0 <1` → `>=0.99.0 <1`, and the lockfile resolves 0.99.2. (`@earendil-works/pi-server` was bumped to the same floor in the interim and then deleted outright later in this release — see below.) The floor moved because this release starts depending on a 0.99-only surface: tool `exposure` (see below) and `ToolDefinition.execute()`'s context widening to `ExtensionToolContext`. A pre-0.99 host does not fail a type check for either — it ignores an unknown `exposure` value and would declare a withdrawn tool to the model, which is exactly the class of silent breakage the floor exists to prevent. The upgrade's compile-time fallout was three direct `ToolDefinition.execute()` call sites (the `/research` and `/knowledge-store` command handlers, the SDK's `searchKnowledge`), which now view their plain context through `asToolExecContext()` (`src/utils/tool-exec-context.ts`, documented there: pi-research's tools never use `ctx.tools` or `ctx.executeTool()`). One runtime contract test caught a new host loader member (`loadExtensionPaths`, a private helper, added to `HOST_INTERNAL_HELPERS`). Full unit suite (3,176 tests) passes against 0.99.2.
|
|
17
|
+
- **The knowledge-search tool is withdrawn from the model's tool list while Knowledge Mode is `none`, using pi 0.99's `exposure`.** Previously `research_knowledge_search` was registered `direct` unconditionally, so a disabled store still showed the model a tool that could only miss; only its prompt block was gated. It is now re-registered with `exposure: 'hidden'` when the mode is `none` and back to `'direct'` when it is not (`syncKnowledgeSearchExposure` in `src/index.ts`, run at load, on `session_start`, and on each new prompt), so the toggle still applies from the next prompt without a pi restart and the disabled tool is genuinely undeclared. `pi.getAllTools()` reports the exposure; a hidden tool is neither declared to the model nor listed in the prompt.
|
|
18
|
+
- **`npm audit` production tree: the `brace-expansion` advisories are fixed by override, and the one copy no override can reach left the shipped tree.** `brace-expansion` `GHSA-q2hr-2g5m-vwhr` / `GHSA-qhr7-859c-m2p7` / `GHSA-6j4f-fj2g-mc7p` (brace-expansion ≤ 5.0.11, a CPU-DoS in glob brace expansion) is fixed by the new `overrides` entry `brace-expansion: ^5.0.12` everywhere in the tree EXCEPT the copy frozen inside the pi host's own `npm-shrinkwrap.json` (`@earendil-works/pi-coding-agent` → `minimatch@10.2.6` → `brace-expansion@5.0.9`), which no downstream override can reach. That single node was briefly allowlisted with the location-scoped exception shape in `config/tooling/audit-exceptions.json`; the peer-dependency change below removed the host's shrinkwrapped subtree from the shipped tree, so the entry is gone and the allowlist is empty again. A second new override, `fast-uri: ^3.1.8`, fixes `GHSA-hrr3-gc8f-f4qj` (dev-only, via `depngn` → `table` → `ajv`). `node scripts/audit-gate.cjs` is green with zero allowlisted advisories.
|
|
19
|
+
- **In-range dependency refresh:** `undici` 8.11.0 → 8.11.2, `vite` 8.3.0 → 8.3.1, `vitest` / `@vitest/coverage-v8` 5.0.1 → 5.0.3, `dependency-cruiser` 18.3.1 → 18.5.0, `@typescript-eslint/*` 8.70.1 → 8.71.0. The deliberate holds are unchanged and re-verified against the new tree: `apache-arrow` 21.1.0 (21.2.0 breaks LanceDB 0.39.0 IPC reads), `playwright-core` 1.60.0 (camoufox-js 0.12.0 peer cap `<1.61.0`), `typebox` 1.3.27 (lockstep with pi 0.99.2's own pin), `@huggingface/transformers` 4.2.0 (4.3.0 reshuffles the ONNX native stack for no needed gain), `typescript` 6.0.3.
|
|
20
|
+
|
|
21
|
+
- **CI action pins refreshed to the newest releases.** `actions/setup-node` v6.5.0 → v7.0.0, `actions/upload-artifact` v4.6.2 → v7.0.1, `actions/download-artifact` v6.0.0 → v8.0.1, `github/codeql-action` v4.37.7 → v4.38.2 (`init` and `analyze` kept on the same commit). `actions/checkout` (v7.0.1) and `actions/cache` (v6.1.0) were already the newest release. Every pin stays a full commit SHA with the version in a trailing comment.
|
|
22
|
+
- **The pinned-stack version claims in the docs were stale and are corrected.** `docs/ARCHITECTURE.md` still cited `typebox` `1.3.7` across pi's 0.84/0.85 lines and `@lancedb/lancedb` 0.38. `typebox` is `1.3.27`, the version `@earendil-works/pi-ai` and `@earendil-works/pi-coding-agent` 0.99.2 both declare (read from their installed `package.json`), and LanceDB is `0.39.0`. The Arrow hold was re-verified against 0.39.0 on 2026-10-01: `21.2.0` still fails every table open with `Failed to read IPC file: Arrow error: Parser error: Unable to get root as footer: RangeOutOfBounds … UnionVariant { variant: "Type::FixedSizeList" }`. The same three numbers were corrected in the ES/JA/ZH translations.
|
|
23
|
+
|
|
24
|
+
- **The `@earendil-works/pi-*` host packages are peer dependencies now, the unused `@earendil-works/pi-server` is gone, and the shipped tree audits clean with no allowlist.** pi's extension loader aliases every `@earendil-works/*` and `typebox` specifier to the HOST's own copies, so the copies inside a managed install are never loaded in-host — they existed only for the standalone CLI and SDK, at a cost of roughly 145MB per managed install (`pi-coding-agent` alone is ~440MB unpacked here). pi's own installer passes `--legacy-peer-deps` on purpose so host packages are not installed alongside. `@earendil-works/pi-ai`, `pi-coding-agent` and `pi-tui` therefore move `dependencies` → `peerDependencies` (`>=0.99.0 <1`), with matching `devDependencies` so this repo's own install, tests and CI still resolve them. Three consequences:
|
|
25
|
+
- The production tree no longer contains `pi-coding-agent`'s `npm-shrinkwrap.json` subtree, so the `brace-expansion` advisories it froze are no longer shipped and `config/tooling/audit-exceptions.json` is empty again (the gate prints a stale-entry warning if the entry is left in place). The full-tree audit still reports it, as the informational dev summary.
|
|
26
|
+
- `@earendil-works/pi-server` is removed. It was declared as a dependency but imported by nothing in `src/`, `dist/` or the agent skill.
|
|
27
|
+
- Standalone installs are unaffected on npm ≥ 7, which resolves peers automatically. An install that skips them (`--legacy-peer-deps`, yarn classic) is now caught: the agent-skill launcher's dependency preflight covers the PATH and `~/.pi/bin` engine locations too (it dereferences the bin symlink before deciding, and skips the check when the layout cannot be identified, so a working install is never failed), and the message plus the docs name the one-line fix. `test/unit/cli.test.ts` covers both the fail-closed path and the pass-through.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **The log-redaction JWT bounds are asserted directly instead of timed.** `redactSecrets` keeps every JWT segment quantifier bounded, because an unbounded `[A-Za-z0-9_-]+` consumed to the end of a dotless base64url run and backtracked one character at a time, and `_`/`-` count as non-alphanumeric left boundaries, so the same run offered a fresh start position every few characters. The guard was a wall-clock scaling ratio, and it failed CI twice: 3.12x against a 3.0 threshold on ubuntu/node 22, 3.47x on node 24 before that. The ratio is not measurable on this input. The bounded pattern costs about 0.004ms per call, so a doubling ratio was reading call overhead and regexp-cache warm-up rather than regex work, and V8's literal prefilter hides the pathology either way: an unbounded variant of the same pattern also scales linearly on `_eyJ` runs. The test now asserts the bound itself (`['{1,1024}', '{1,8192}', '{1,4096}']` on the exported `JWT_PATTERN`), which is deterministic and still fails if a bound is dropped.
|
|
32
|
+
|
|
33
|
+
- **Research runs no longer fail on models that cannot turn thinking off.** The engine's direct LLM calls (coordinator, lead router, synthesizer, JSON repair, knowledge-search extraction and triage) all go through `buildSafeOptions` (`src/core/llm/llm-utils.ts`), which sends thinking `off` by default (`PI_RESEARCH_LLM_THINKING_LEVEL`). Some models reject that: zai/glm-5.3, whose catalog entry marks `off` as unsupported, answers every such call with 400 "This model always engages in thinking and cannot be disabled; please use low, high, or max", so a depth 1–3 run failed at the coordinator's first plan (quick mode was unaffected: it runs as a pi agent session, which already clamps the level). `off` is now raised to the nearest level the model supports (pi-ai's `clampThinkingLevel`, `low` for glm-5.3) only when the model cannot disable thinking; every other model and every explicit level is sent exactly as before. With the fix, the same depth-1 run on glm-5.3 completes.
|
|
34
|
+
|
|
35
|
+
## [1.7.2] - 2026-09-27
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **Scraped pages keep only the useful part of the converter's front matter.** The native converter turns every `<meta>` tag into a front-matter line: 54 lines on a GitHub page (analytics and error-reporting URLs, verification hashes, nonces, app ids), and on arXiv the abstract three times over. Nothing parses these lines; researchers read them and the knowledge store indexes them. `trimFrontMatter` (`src/web-research/scraper-utils.ts`) now keeps the title, description, author and publication/modification dates (first key present per slot: plain, Open Graph, citation or Dublin Core names) and drops the rest; a page with none of them loses the block. GitHub pages shrink by about 11%, arXiv abstract pages by about a third. Side effect: an empty page shell whose front matter alone used to pass the stub check is now treated as a stub, so the browser fallback gets a chance.
|
|
40
|
+
- **Scraped pages no longer carry empty same-page links.** Documentation generators such as Quarto and nbdev put an empty anchor before every code line (`<a href="#cb2-1"></a>`) and next to headings; the native HTML→Markdown converter turned each into `[](#cb2-1)`, so every code block in a research scrape (and in the knowledge store) started each line with one. Both converters now drop links that have no text and point to the same page (`cleanConvertedMarkdown` in `src/web-research/scraper-utils.ts`, run after the existing image-link stripping so heading anchors that wrap an icon — GitHub's `[](#title)` — go too). Links with text, and empty links to other pages, are kept. On an nbdev documentation page (fastcore.fast.ai/docments.html) this removes 583 such links, 16% of the scraped text.
|
|
41
|
+
|
|
8
42
|
## [1.7.1] - 2026-09-27
|
|
9
43
|
|
|
10
44
|
### Changed
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -75,7 +75,8 @@ Research
|
|
|
75
75
|
| `PI_RESEARCH_MAX_RESEARCHERS` (TUI) | `3` | 1–5 | Parallel researchers. |
|
|
76
76
|
| `PI_RESEARCH_DEFAULT_RESEARCH_DEPTH` (TUI) `[project]` | `1` | 0–3 | Depth for `/research` and the CLI when `--depth` is omitted (0=quick single-pass — requires `PI_RESEARCH_QUICK_RESEARCH` —, 1=normal, 2=deep, 3=ultra). The agent-facing `research` tool-usage prompt renders its depth guidance relative to this setting too: when quick is the configured default, depth 0 is the ordinary choice; a user who keeps the default (1) gets byte-identical guidance. |
|
|
77
77
|
| `PI_RESEARCH_QUICK_RESEARCH` (TUI) `[project]` | `false` | `true` · `false` | Opt-in gate for depth-0 (quick) research. When `false` (default), the agent-facing `research` tool keeps its upstream `minimum: 1` schema — agents can neither see nor send depth 0 — and a `DEFAULT_RESEARCH_DEPTH` of 0 resolves to 1. Toggling applies on the next session. |
|
|
78
|
-
| `PI_RESEARCH_QUICK_MAX_QUERIES` | `
|
|
78
|
+
| `PI_RESEARCH_QUICK_MAX_QUERIES` | `3` | 1–10 | Max queries per `search` call in quick (depth 0) research. Applies to every quick run — the pi extension tool, the CLI, and the SDK's `--depth 0` — since they share the same researcher session factory. Deliberately below deep research's 30-query cap; the ceiling of 10 keeps quick mode from ever exceeding the old 5–10 prompt guidance. |
|
|
79
|
+
| `PI_RESEARCH_QUICK_MAX_SCRAPE_URLS` | `5` | 1–20 | Max URLs fetched per scrape batch in quick (depth 0) research. Separate from `PI_RESEARCH_MAX_SCRAPE_URLS` (deep, `8`) because quick mode answers a single-fact question and the deep budget pulls in pages that question does not need; `PI_RESEARCH_MAX_SCRAPE_BATCHES` still caps how many batches a quick run may use. Applies to every quick run (pi extension tool, CLI, SDK `--depth 0`). |
|
|
79
80
|
| `PI_RESEARCH_MAX_SCRAPE_BATCHES` (TUI) | `2` | 0–99 | Scrape batches per researcher (0 = unlimited). When prompt caching is known to be active for the resolved research model (Anthropic API models, or a provider route explicitly configured for Anthropic-style cache control), the effective limit is this value plus one — the cached prompt prefix makes the extra batch cheap. |
|
|
80
81
|
| `PI_RESEARCH_MAX_GATHERING_CALLS` | `12` | 1–100 | Shared web-gathering calls per researcher (`search` + `security_search` + `stackexchange` + `youtube_transcript`). |
|
|
81
82
|
| `PI_RESEARCH_MAX_CONCURRENT_SCRAPES` | `3` | 1–20 | Concurrent URLs fetched per scrape batch. |
|
package/docs/SDK.md
CHANGED
|
@@ -13,6 +13,14 @@ run the `pi` extension, which keeps its own private copy your scripts can't impo
|
|
|
13
13
|
npm install @lincoln504/pi-research
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
The `@earendil-works/pi-*` host packages are peer dependencies, so npm ≥7 installs them
|
|
17
|
+
with the command above. If your install skips peers — `--legacy-peer-deps`, yarn
|
|
18
|
+
classic — the SDK cannot import them, so add the one it loads directly:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @earendil-works/pi-coding-agent
|
|
22
|
+
```
|
|
23
|
+
|
|
16
24
|
On npm ≥11.19 (and npm 12), dependency install scripts are skipped by default. Nothing
|
|
17
25
|
here needs them: better-sqlite3 13 carries prebuilt bindings for every supported
|
|
18
26
|
platform and loads them at runtime, and the stealth browser self-provisions on its
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lincoln504/pi-research",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
4
4
|
"description": "Web research for your agents with smart and safe tooling + knowledge store",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -115,10 +115,6 @@
|
|
|
115
115
|
"image": "https://raw.githubusercontent.com/Lincoln504/pi-research/main/docs/media/hero.png"
|
|
116
116
|
},
|
|
117
117
|
"dependencies": {
|
|
118
|
-
"@earendil-works/pi-ai": ">=0.87.0 <1",
|
|
119
|
-
"@earendil-works/pi-coding-agent": ">=0.87.0 <1",
|
|
120
|
-
"@earendil-works/pi-server": ">=0.87.0 <1",
|
|
121
|
-
"@earendil-works/pi-tui": ">=0.87.0 <1",
|
|
122
118
|
"@kreuzberg/html-to-markdown-node": "^3.7.2",
|
|
123
119
|
"@lancedb/lancedb": "0.39.0",
|
|
124
120
|
"apache-arrow": "21.1.0",
|
|
@@ -132,22 +128,29 @@
|
|
|
132
128
|
"playwright-core": "1.60.0",
|
|
133
129
|
"poolifier": "^5.3.2",
|
|
134
130
|
"typebox": "1.3.27",
|
|
135
|
-
"undici": "^8.
|
|
131
|
+
"undici": "^8.11.2",
|
|
136
132
|
"youtubei.js": "^18.0.0"
|
|
137
133
|
},
|
|
134
|
+
"peerDependencies": {
|
|
135
|
+
"@earendil-works/pi-ai": ">=0.99.0 <1",
|
|
136
|
+
"@earendil-works/pi-coding-agent": ">=0.99.0 <1",
|
|
137
|
+
"@earendil-works/pi-tui": ">=0.99.0 <1"
|
|
138
|
+
},
|
|
138
139
|
"optionalDependencies": {
|
|
139
140
|
"@huggingface/transformers": "4.2.0",
|
|
140
141
|
"webgpu": "^0.6.0"
|
|
141
142
|
},
|
|
142
143
|
"overrides": {
|
|
143
144
|
"lodash.isequal": "npm:fast-deep-equal@^3.1.3",
|
|
145
|
+
"brace-expansion": "^5.0.12",
|
|
146
|
+
"fast-uri": "^3.1.8",
|
|
144
147
|
"ip-address": "^10.2.0",
|
|
145
148
|
"apache-arrow": "21.1.0",
|
|
146
149
|
"onnxruntime-web": "1.24.3",
|
|
147
150
|
"node-domexception": "npm:@nolyfill/domexception@^1.0.28",
|
|
148
151
|
"ua-parser-js": "^1.0.41",
|
|
149
152
|
"protobufjs": "7.6.5",
|
|
150
|
-
"vite": "^8.
|
|
153
|
+
"vite": "^8.3.1",
|
|
151
154
|
"adm-zip": "^0.6.1",
|
|
152
155
|
"jsdom": {
|
|
153
156
|
"undici": "^8.10.2"
|
|
@@ -155,17 +158,20 @@
|
|
|
155
158
|
"sharp": "^0.35.0"
|
|
156
159
|
},
|
|
157
160
|
"devDependencies": {
|
|
161
|
+
"@earendil-works/pi-ai": ">=0.99.0 <1",
|
|
162
|
+
"@earendil-works/pi-coding-agent": ">=0.99.0 <1",
|
|
163
|
+
"@earendil-works/pi-tui": ">=0.99.0 <1",
|
|
158
164
|
"@eslint/js": "^10.0.1",
|
|
159
165
|
"@types/jsdom": "^30.0.0",
|
|
160
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
161
|
-
"@typescript-eslint/parser": "8.
|
|
162
|
-
"@vitest/coverage-v8": "^5.0.
|
|
163
|
-
"dependency-cruiser": "^18.
|
|
166
|
+
"@typescript-eslint/eslint-plugin": "8.71.0",
|
|
167
|
+
"@typescript-eslint/parser": "8.71.0",
|
|
168
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
169
|
+
"dependency-cruiser": "^18.5.0",
|
|
164
170
|
"depngn": "^0.6.0",
|
|
165
171
|
"eslint": "10.11.0",
|
|
166
172
|
"npm-check-updates": "^23.1.0",
|
|
167
173
|
"typescript": ">=6.0.3 <6.1.0",
|
|
168
|
-
"vite": "^8.
|
|
169
|
-
"vitest": "^5.0.
|
|
174
|
+
"vite": "^8.3.1",
|
|
175
|
+
"vitest": "^5.0.3"
|
|
170
176
|
}
|
|
171
177
|
}
|
package/src/config.ts
CHANGED
|
@@ -96,12 +96,19 @@ export const ConfigSchema = Type.Object({
|
|
|
96
96
|
* (default: 5; 1 = every query). Configured via env/config file; not surfaced
|
|
97
97
|
* in the config TUI. */
|
|
98
98
|
YOUTUBE_QUERY_EVERY_N: Type.Number({ minimum: 1, maximum: 100, default: 5 }),
|
|
99
|
-
/** Max queries per `search` call in quick (depth 0) research (default:
|
|
99
|
+
/** Max queries per `search` call in quick (depth 0) research (default: 3, range: 1-10).
|
|
100
100
|
* Applies to every quick run — the pi extension tool, the CLI, and the SDK's
|
|
101
101
|
* `--depth 0` — since all three share the same researcher session factory. The
|
|
102
102
|
* ceiling is 10 so quick mode can never exceed the old 5–10 prompt guidance.
|
|
103
103
|
* Configured via env/config file; not surfaced in the config TUI. */
|
|
104
|
-
QUICK_MAX_QUERIES: Type.Number({ minimum: 1, maximum: 10, default:
|
|
104
|
+
QUICK_MAX_QUERIES: Type.Number({ minimum: 1, maximum: 10, default: 3 }),
|
|
105
|
+
/** Max URLs scraped per `scrape` call in quick (depth 0) research (default: 5,
|
|
106
|
+
* range: 1-20). Quick mode is for single-fact lookups, where the deep default
|
|
107
|
+
* (MAX_SCRAPE_URLS, 8) pulls in pages the question does not need; its per-batch
|
|
108
|
+
* budget is therefore separate. Batches are still capped by MAX_SCRAPE_BATCHES.
|
|
109
|
+
* Applies to every quick run (pi extension tool, CLI, SDK `--depth 0`).
|
|
110
|
+
* Configured via env/config file; not surfaced in the config TUI. */
|
|
111
|
+
QUICK_MAX_SCRAPE_URLS: Type.Number({ minimum: 1, maximum: 20, default: 5 }),
|
|
105
112
|
/** Health check timeout in milliseconds (default: 10000ms) */
|
|
106
113
|
HEALTH_CHECK_TIMEOUT_MS: Type.Number({ minimum: 2000, maximum: 120000, default: 10000 }),
|
|
107
114
|
/** Default timeout for browser page operations like search (default: 45000ms) */
|
|
@@ -282,6 +289,7 @@ const USER_MIGRATION_KEYS = [
|
|
|
282
289
|
'PI_RESEARCH_YOUTUBE_TRANSCRIPT_LANG',
|
|
283
290
|
'PI_RESEARCH_YOUTUBE_QUERY_EVERY_N',
|
|
284
291
|
'PI_RESEARCH_QUICK_MAX_QUERIES',
|
|
292
|
+
'PI_RESEARCH_QUICK_MAX_SCRAPE_URLS',
|
|
285
293
|
'PI_RESEARCH_CONSOLE_LOG',
|
|
286
294
|
'PI_RESEARCH_MODEL',
|
|
287
295
|
'PI_RESEARCH_KNOWLEDGE_DIR',
|
|
@@ -970,6 +978,7 @@ export function saveConfig(config: Config, scope: 'local' | 'user' = 'local', cw
|
|
|
970
978
|
PI_RESEARCH_YOUTUBE_TRANSCRIPT_LANG: config.YOUTUBE_TRANSCRIPT_LANG,
|
|
971
979
|
PI_RESEARCH_YOUTUBE_QUERY_EVERY_N: String(config.YOUTUBE_QUERY_EVERY_N),
|
|
972
980
|
PI_RESEARCH_QUICK_MAX_QUERIES: String(config.QUICK_MAX_QUERIES),
|
|
981
|
+
PI_RESEARCH_QUICK_MAX_SCRAPE_URLS: String(config.QUICK_MAX_SCRAPE_URLS),
|
|
973
982
|
PI_RESEARCH_BROWSER_TASK_TIMEOUT_MS: String(config.BROWSER_TASK_TIMEOUT_MS),
|
|
974
983
|
PI_RESEARCH_LLM_TIMEOUT_MS: String(config.LLM_TIMEOUT_MS),
|
|
975
984
|
PI_RESEARCH_LLM_THINKING_LEVEL: config.LLM_THINKING_LEVEL,
|
|
@@ -1202,6 +1211,7 @@ export function createConfig(env: Record<string, string | undefined>, processEnv
|
|
|
1202
1211
|
YOUTUBE_TRANSCRIPT_LANG: parseEnvString(e, 'PI_RESEARCH_YOUTUBE_TRANSCRIPT_LANG', DEFAULTS.YOUTUBE_TRANSCRIPT_LANG)!,
|
|
1203
1212
|
YOUTUBE_QUERY_EVERY_N: parseEnvNumber(e, 'PI_RESEARCH_YOUTUBE_QUERY_EVERY_N', DEFAULTS.YOUTUBE_QUERY_EVERY_N, 1, 100, true),
|
|
1204
1213
|
QUICK_MAX_QUERIES: parseEnvNumber(e, 'PI_RESEARCH_QUICK_MAX_QUERIES', DEFAULTS.QUICK_MAX_QUERIES, 1, 10, true),
|
|
1214
|
+
QUICK_MAX_SCRAPE_URLS: parseEnvNumber(e, 'PI_RESEARCH_QUICK_MAX_SCRAPE_URLS', DEFAULTS.QUICK_MAX_SCRAPE_URLS, 1, 20, true),
|
|
1205
1215
|
HEALTH_CHECK_TIMEOUT_MS: parseEnvNumber(e, 'PI_RESEARCH_HEALTH_CHECK_TIMEOUT_MS', DEFAULTS.HEALTH_CHECK_TIMEOUT_MS, 2000, 120000),
|
|
1206
1216
|
SEARCH_TIMEOUT_MS: parseEnvNumber(e, 'PI_RESEARCH_SEARCH_TIMEOUT_MS', DEFAULTS.SEARCH_TIMEOUT_MS, 5000, 120000),
|
|
1207
1217
|
TUI_REFRESH_DEBOUNCE_MS: parseEnvNumber(e, 'PI_RESEARCH_TUI_REFRESH_DEBOUNCE_MS', DEFAULTS.TUI_REFRESH_DEBOUNCE_MS, 0, 1000),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Shared logic for safe and robust LLM interactions.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { type Model, type AssistantMessage, type SimpleStreamOptions, type ModelThinkingLevel, type Tool, type TSchema, type Context, type ToolCall } from '@earendil-works/pi-ai';
|
|
7
|
+
import { clampThinkingLevel, type Model, type AssistantMessage, type SimpleStreamOptions, type ModelThinkingLevel, type Tool, type TSchema, type Context, type ToolCall } from '@earendil-works/pi-ai';
|
|
8
8
|
import { extractText } from '../../utils/text-utils.ts';
|
|
9
9
|
import { logger } from '../../logger.ts';
|
|
10
10
|
import { completeSimple } from './pi-ai-completion.ts';
|
|
@@ -20,6 +20,10 @@ import { completeSimple } from './pi-ai-completion.ts';
|
|
|
20
20
|
* 'off' is deliberate: these engine calls emit structured JSON / cited reports, so a
|
|
21
21
|
* thinking block only consumes the output-token budget (often truncating before the
|
|
22
22
|
* text block is emitted) for at most a marginal quality gain that does not justify the cost.
|
|
23
|
+
* Some models cannot turn thinking off (their `thinkingLevelMap` marks `off` as
|
|
24
|
+
* unsupported, e.g. zai/glm-5.3, which rejects the request with a 400); for those
|
|
25
|
+
* 'off' becomes the nearest level the model supports (pi-ai's clampThinkingLevel,
|
|
26
|
+
* the same rule pi's own agent sessions apply). Other levels pass through unchanged.
|
|
23
27
|
*
|
|
24
28
|
* Callers should also pass the research `sessionId` in `options`. pi-ai forwards it as the
|
|
25
29
|
* provider's prompt-cache / session-affinity key — OpenAI's `prompt_cache_key`, OpenRouter's
|
|
@@ -40,7 +44,8 @@ export function buildSafeOptions(
|
|
|
40
44
|
defaultCap: number = 4096,
|
|
41
45
|
thinkingLevel: ModelThinkingLevel = 'off'
|
|
42
46
|
): SimpleStreamOptions {
|
|
43
|
-
const
|
|
47
|
+
const requested: ModelThinkingLevel = (options.reasoning as ModelThinkingLevel | undefined) ?? thinkingLevel;
|
|
48
|
+
const effective: ModelThinkingLevel = requested === 'off' ? clampThinkingLevel(model, 'off') : requested;
|
|
44
49
|
return {
|
|
45
50
|
...options,
|
|
46
51
|
// Ensure maxTokens is never null/None to avoid provider-side crashes
|
package/src/core/pi-version.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - As a pi EXTENSION, the host supplies `@earendil-works/*`. Our package.json
|
|
9
9
|
* range never constrains it, so an in-process check is the only enforcement.
|
|
10
|
-
* - As a standalone CLI/SDK, the range is `>=0.
|
|
10
|
+
* - As a standalone CLI/SDK, the range is `>=0.99.0 <1` and published tarballs
|
|
11
11
|
* carry no lockfile, so every fresh install resolves the newest 0.x at that
|
|
12
12
|
* instant. pi is pre-1.0: a minor bump may break anything under semver, and
|
|
13
13
|
* already has (0.83.0 extended the ResourceLoader contract).
|
|
@@ -83,8 +83,19 @@ export function compareVersions(a: SemverParts, b: SemverParts): number {
|
|
|
83
83
|
* finishTurn boundary. The SDK paths (session service, model-registry factory)
|
|
84
84
|
* are written against that surface; an older host may resolve but is not a
|
|
85
85
|
* tested or supported target.
|
|
86
|
+
*
|
|
87
|
+
* 0.99.0 added tool `exposure` (including `hidden`, which is how a tool is
|
|
88
|
+
* withdrawn when tools cannot be unregistered) and widened
|
|
89
|
+
* `ToolDefinition.execute()`'s context from `ExtensionContext` to
|
|
90
|
+
* `ExtensionToolContext` (adds `tools` and `executeTool()`). pi-research uses
|
|
91
|
+
* `exposure` to keep an optional tool out of the model's tool list without a
|
|
92
|
+
* restart, and adapts plain contexts with `asToolExecContext()` at the three
|
|
93
|
+
* places it invokes its own tools directly (see utils/tool-exec-context.ts).
|
|
94
|
+
* Relying on either on an older host is a silent misbehaviour rather than a
|
|
95
|
+
* type error — a pre-0.99 host ignores `exposure: hidden` and would declare a
|
|
96
|
+
* withdrawn tool to the model — so the floor moved with the adoption.
|
|
86
97
|
*/
|
|
87
|
-
export const PI_MIN_VERSION: SemverParts = { major: 0, minor:
|
|
98
|
+
export const PI_MIN_VERSION: SemverParts = { major: 0, minor: 99, patch: 0 };
|
|
88
99
|
|
|
89
100
|
/**
|
|
90
101
|
* Newest host line this release was actually exercised against (CI + a real run).
|
|
@@ -92,7 +103,7 @@ export const PI_MIN_VERSION: SemverParts = { major: 0, minor: 87, patch: 0 };
|
|
|
92
103
|
* Compared on MAJOR.MINOR only: a patch bump within a tested line is not a new
|
|
93
104
|
* surface, and warning on it would be noise.
|
|
94
105
|
*/
|
|
95
|
-
export const PI_TESTED_MAX_VERSION: SemverParts = { major: 0, minor:
|
|
106
|
+
export const PI_TESTED_MAX_VERSION: SemverParts = { major: 0, minor: 99, patch: 2 };
|
|
96
107
|
|
|
97
108
|
export type PiCompatibilityLevel = 'ok' | 'unparseable' | 'too-old' | 'untested';
|
|
98
109
|
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { logger } from './logger.ts';
|
|
|
14
14
|
import { checkPiCompatibility } from './core/pi-version.ts';
|
|
15
15
|
import { randomUUID } from 'node:crypto';
|
|
16
16
|
import { shutdownManager } from './utils/shutdown-manager.ts';
|
|
17
|
+
import { asToolExecContext } from './utils/tool-exec-context.ts';
|
|
17
18
|
import { healthRegistry } from './healthcheck/index.ts';
|
|
18
19
|
import { registerBeforeExitSafetyNet } from './knowledge/embedder-utils.ts';
|
|
19
20
|
import { getConfig, validateConfig } from './config.ts';
|
|
@@ -92,11 +93,12 @@ function extractResultText(result: AgentToolResult<unknown>): string {
|
|
|
92
93
|
* Pi Research Extension
|
|
93
94
|
*/
|
|
94
95
|
export default async function (pi: ExtensionAPI) {
|
|
95
|
-
// Runtime version check — must match the @earendil-works/* dependency minimum (>=0.
|
|
96
|
+
// Runtime version check — must match the @earendil-works/* dependency minimum (>=0.99.0).
|
|
96
97
|
// The floor was raised to 0.84.0 because setRuntimeApiKey() lost its `allowNetwork` option
|
|
97
98
|
// there and only 0.84.0+ hardcodes the equivalent guard internally (see pi-version.ts), then
|
|
98
99
|
// to 0.85.0 to track the pi extension package line, and to 0.87.0 for the 0.86
|
|
99
|
-
// TranscriptContext / 0.87 SessionEntry-union surface
|
|
100
|
+
// TranscriptContext / 0.87 SessionEntry-union surface, and to 0.99.0 for tool `exposure`
|
|
101
|
+
// and the ExtensionToolContext widening of ToolDefinition.execute() (see pi-version.ts). The
|
|
100
102
|
// APIs it rests on arrived in 0.80.8, which introduced ModelRuntime and removed
|
|
101
103
|
// AuthStorage/ModelRegistry.create(); buildModelRegistry (model-registry-factory.ts)
|
|
102
104
|
// unconditionally calls ModelRuntime.create(), and createAgentSession() is invoked
|
|
@@ -127,6 +129,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
127
129
|
// 1. REGISTER CRITICAL EVENT LISTENERS IMMEDIATELY
|
|
128
130
|
// This ensures we capture steering even if initialization takes time.
|
|
129
131
|
pi.on('input', async (event: any, ctx: ExtensionContext) => {
|
|
132
|
+
// A new prompt (not steering typed while the agent runs): re-align the
|
|
133
|
+
// knowledge-search tool's exposure with live config before pi builds this
|
|
134
|
+
// turn's prompt, so a /research-config Knowledge Mode change applies without
|
|
135
|
+
// a pi restart. Registration happens before this closure runs (see
|
|
136
|
+
// syncKnowledgeSearchExposure below).
|
|
137
|
+
if (!event.streamingBehavior) syncKnowledgeSearchExposure(ctx);
|
|
130
138
|
// Only intercept genuine interactive keystrokes. Programmatic sends originate
|
|
131
139
|
// from this extension (source 'extension') — e.g. the Alt+P pop handler forwards
|
|
132
140
|
// a popped message as 'steer'. Without this guard that forward would be captured
|
|
@@ -344,13 +352,34 @@ export default async function (pi: ExtensionAPI) {
|
|
|
344
352
|
|
|
345
353
|
// Create and register the Research Knowledge Search tool. It is native-free at
|
|
346
354
|
// construction (the vector/ML stack loads lazily on first execute) and self-guards when
|
|
347
|
-
// Knowledge Mode is 'none' (its execute returns a "store disabled" miss).
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
355
|
+
// Knowledge Mode is 'none' (its execute returns a "store disabled" miss).
|
|
356
|
+
//
|
|
357
|
+
// pi 0.99 exposes tool `exposure`, which is how this tool is keyed to LIVE
|
|
358
|
+
// Knowledge Mode: while the mode is 'none' it is re-registered as `hidden`, so
|
|
359
|
+
// it is neither declared to the model nor listed in the prompt, and a mode
|
|
360
|
+
// change via /research-config takes effect on the next prompt without a pi
|
|
361
|
+
// restart (pi has no unregisterTool; re-registering with `exposure: 'hidden'`
|
|
362
|
+
// is the documented withdrawal, and a later re-register back to `direct`
|
|
363
|
+
// activates the tool like a new one). The tool object itself is kept as the
|
|
364
|
+
// base so the /knowledge-store command below can still invoke it directly.
|
|
352
365
|
const researchKnowledgeSearchTool: ToolDefinition = createResearchKnowledgeSearchTool('pi');
|
|
353
|
-
|
|
366
|
+
let knowledgeSearchExposure: 'direct' | 'hidden' | undefined;
|
|
367
|
+
const syncKnowledgeSearchExposure = (ctx?: ExtensionContext): void => {
|
|
368
|
+
try {
|
|
369
|
+
const mode = getConfig(ctx?.cwd ?? (pi as any).cwd, 'pi').KNOWLEDGE_STORE_MODE;
|
|
370
|
+
const want: 'direct' | 'hidden' = mode === 'none' ? 'hidden' : 'direct';
|
|
371
|
+
if (want === knowledgeSearchExposure) return;
|
|
372
|
+
pi.registerTool({ ...researchKnowledgeSearchTool, exposure: want });
|
|
373
|
+
knowledgeSearchExposure = want;
|
|
374
|
+
logger.debug(`[pi-research] research_knowledge_search exposure → ${want}`);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
logger.debug('[pi-research] could not sync research_knowledge_search exposure:', err);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
syncKnowledgeSearchExposure();
|
|
380
|
+
pi.on('session_start', async (_event: any, ctx: ExtensionContext) => {
|
|
381
|
+
syncKnowledgeSearchExposure(ctx);
|
|
382
|
+
});
|
|
354
383
|
|
|
355
384
|
// Alt+P — Pop queued steering messages out of the research queue and steer pi with them.
|
|
356
385
|
pi.registerShortcut(Key.alt('p'), {
|
|
@@ -435,7 +464,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
435
464
|
{ query: text, depth: config.DEFAULT_RESEARCH_DEPTH },
|
|
436
465
|
ctx.signal,
|
|
437
466
|
undefined,
|
|
438
|
-
ctx,
|
|
467
|
+
asToolExecContext(ctx),
|
|
439
468
|
);
|
|
440
469
|
|
|
441
470
|
const output = extractResultText(result);
|
|
@@ -525,7 +554,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
525
554
|
{ queries: [text] },
|
|
526
555
|
ctx.signal,
|
|
527
556
|
undefined,
|
|
528
|
-
ctx,
|
|
557
|
+
asToolExecContext(ctx),
|
|
529
558
|
);
|
|
530
559
|
|
|
531
560
|
const output = extractResultText(result);
|
|
@@ -149,7 +149,7 @@ var init_user_agent = __esm({
|
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
// src/web-research/scraper-types.ts
|
|
152
|
-
var MAX_HTML_SIZE, MAX_PDF_SIZE, INTERNAL_NETWORK_PATTERNS, FILTERED_TAGS, BOT_PATTERNS, IMAGE_LINK_PATTERN, MARKDOWN_IMAGE_PATTERN;
|
|
152
|
+
var MAX_HTML_SIZE, MAX_PDF_SIZE, INTERNAL_NETWORK_PATTERNS, FILTERED_TAGS, BOT_PATTERNS, IMAGE_LINK_PATTERN, MARKDOWN_IMAGE_PATTERN, EMPTY_FRAGMENT_LINK_PATTERN;
|
|
153
153
|
var init_scraper_types = __esm({
|
|
154
154
|
"src/web-research/scraper-types.ts"() {
|
|
155
155
|
"use strict";
|
|
@@ -215,6 +215,7 @@ var init_scraper_types = __esm({
|
|
|
215
215
|
];
|
|
216
216
|
IMAGE_LINK_PATTERN = /\[([^\]]*)\]\((data:image\/[^)]+|[^)\s]+\.(?:svg|png|jpe?g|gif|webp|bmp|ico)(?:\?[^)]*)?)\)/gi;
|
|
217
217
|
MARKDOWN_IMAGE_PATTERN = /!\[[^\]]*]\((?:data:image\/[^)]+|[^)\s]+)\)/gi;
|
|
218
|
+
EMPTY_FRAGMENT_LINK_PATTERN = /\[\]\(#[^)\s]*(?:\s+"[^"\n]*")?\)/g;
|
|
218
219
|
}
|
|
219
220
|
});
|
|
220
221
|
|
|
@@ -1559,6 +1560,7 @@ var init_browser_error_utils = __esm({
|
|
|
1559
1560
|
// src/web-research/scraper-utils.ts
|
|
1560
1561
|
var scraper_utils_exports = {};
|
|
1561
1562
|
__export(scraper_utils_exports, {
|
|
1563
|
+
cleanConvertedMarkdown: () => cleanConvertedMarkdown,
|
|
1562
1564
|
createJsMarkdownConverter: () => createJsMarkdownConverter,
|
|
1563
1565
|
createNativeMarkdownConverter: () => createNativeMarkdownConverter,
|
|
1564
1566
|
disposeSsrfSafeFetcher: () => disposeSsrfSafeFetcher,
|
|
@@ -1569,7 +1571,9 @@ __export(scraper_utils_exports, {
|
|
|
1569
1571
|
isBenignScrapeFailure: () => isBenignScrapeFailure,
|
|
1570
1572
|
isSsrfBlockError: () => isSsrfBlockError,
|
|
1571
1573
|
ssrfSafeLookup: () => ssrfSafeLookup,
|
|
1574
|
+
stripEmptyFragmentLinks: () => stripEmptyFragmentLinks,
|
|
1572
1575
|
stripImageLinks: () => stripImageLinks,
|
|
1576
|
+
trimFrontMatter: () => trimFrontMatter,
|
|
1573
1577
|
validateContent: () => validateContent,
|
|
1574
1578
|
validateUrlForSSRF: () => validateUrlForSSRF,
|
|
1575
1579
|
validateUrlForSSRFSync: () => validateUrlForSSRFSync
|
|
@@ -1785,6 +1789,34 @@ function isMappedLoopback(ip) {
|
|
|
1785
1789
|
const mapped = extractMappedIpv4(ip.toLowerCase().replace(/^\[|\]$/g, ""));
|
|
1786
1790
|
return mapped !== null && /^127\./.test(mapped);
|
|
1787
1791
|
}
|
|
1792
|
+
function stripEmptyFragmentLinks(markdown) {
|
|
1793
|
+
return markdown.replace(EMPTY_FRAGMENT_LINK_PATTERN, "");
|
|
1794
|
+
}
|
|
1795
|
+
function trimFrontMatter(markdown) {
|
|
1796
|
+
if (!markdown.startsWith("---\n")) return markdown;
|
|
1797
|
+
const close = markdown.indexOf("\n---\n", 3);
|
|
1798
|
+
if (close === -1 || close > MAX_FRONT_MATTER_CHARS) return markdown;
|
|
1799
|
+
const entries = [];
|
|
1800
|
+
for (const line of markdown.slice(4, close).split("\n")) {
|
|
1801
|
+
const m = FRONT_MATTER_KEY.exec(line);
|
|
1802
|
+
if (m) entries.push({ key: m[1].toLowerCase(), lines: [line] });
|
|
1803
|
+
else if (entries.length) entries[entries.length - 1].lines.push(line);
|
|
1804
|
+
}
|
|
1805
|
+
const kept = [];
|
|
1806
|
+
for (const slot of FRONT_MATTER_SLOTS) {
|
|
1807
|
+
const entry = slot.map((key) => entries.find((e) => e.key === key)).find((e) => e !== void 0);
|
|
1808
|
+
if (entry) kept.push(...entry.lines);
|
|
1809
|
+
}
|
|
1810
|
+
const rest = markdown.slice(close + 5).replace(/^\n+/, "");
|
|
1811
|
+
return kept.length ? `---
|
|
1812
|
+
${kept.join("\n")}
|
|
1813
|
+
---
|
|
1814
|
+
|
|
1815
|
+
${rest}` : rest;
|
|
1816
|
+
}
|
|
1817
|
+
function cleanConvertedMarkdown(markdown) {
|
|
1818
|
+
return stripEmptyFragmentLinks(stripImageLinks(trimFrontMatter(markdown))).replace(/\n{3,}/g, "\n\n").trim();
|
|
1819
|
+
}
|
|
1788
1820
|
function stripImageLinks(markdown) {
|
|
1789
1821
|
return markdown.replace(MARKDOWN_IMAGE_PATTERN, "").replace(IMAGE_LINK_PATTERN, "$1").replace(/\n{3,}/g, "\n\n").trim();
|
|
1790
1822
|
}
|
|
@@ -1798,7 +1830,7 @@ function createNativeMarkdownConverter(nativeModule) {
|
|
|
1798
1830
|
codeBlockStyle: nativeModule.CodeBlockStyle.Backticks,
|
|
1799
1831
|
wrap: false
|
|
1800
1832
|
});
|
|
1801
|
-
return
|
|
1833
|
+
return cleanConvertedMarkdown(result.content ?? "");
|
|
1802
1834
|
};
|
|
1803
1835
|
}
|
|
1804
1836
|
function createJsMarkdownConverter() {
|
|
@@ -1809,7 +1841,7 @@ function createJsMarkdownConverter() {
|
|
|
1809
1841
|
});
|
|
1810
1842
|
return async (html) => {
|
|
1811
1843
|
const markdown = converter.translate(html);
|
|
1812
|
-
return
|
|
1844
|
+
return cleanConvertedMarkdown(markdown);
|
|
1813
1845
|
};
|
|
1814
1846
|
}
|
|
1815
1847
|
function validateContent(html, markdown, url) {
|
|
@@ -1848,7 +1880,7 @@ function validateContent(html, markdown, url) {
|
|
|
1848
1880
|
throw error;
|
|
1849
1881
|
}
|
|
1850
1882
|
}
|
|
1851
|
-
var cachedSsrfFetcher;
|
|
1883
|
+
var cachedSsrfFetcher, FRONT_MATTER_SLOTS, MAX_FRONT_MATTER_CHARS, FRONT_MATTER_KEY;
|
|
1852
1884
|
var init_scraper_utils = __esm({
|
|
1853
1885
|
"src/web-research/scraper-utils.ts"() {
|
|
1854
1886
|
"use strict";
|
|
@@ -1858,6 +1890,23 @@ var init_scraper_utils = __esm({
|
|
|
1858
1890
|
init_scraper_types();
|
|
1859
1891
|
init_browser_error_utils();
|
|
1860
1892
|
cachedSsrfFetcher = void 0;
|
|
1893
|
+
FRONT_MATTER_SLOTS = [
|
|
1894
|
+
["title", "meta-og:title", "meta-citation_title"],
|
|
1895
|
+
["meta-description", "description", "meta-og:description", "meta-twitter:description"],
|
|
1896
|
+
["meta-author", "meta-article:author", "meta-citation_author", "meta-dc.creator", "meta-dcterms.creator"],
|
|
1897
|
+
[
|
|
1898
|
+
"meta-article:published_time",
|
|
1899
|
+
"meta-citation_publication_date",
|
|
1900
|
+
"meta-citation_date",
|
|
1901
|
+
"meta-dcterms.date",
|
|
1902
|
+
"meta-dc.date",
|
|
1903
|
+
"meta-date",
|
|
1904
|
+
"meta-dcterms.created"
|
|
1905
|
+
],
|
|
1906
|
+
["meta-article:modified_time", "meta-dcterms.modified"]
|
|
1907
|
+
];
|
|
1908
|
+
MAX_FRONT_MATTER_CHARS = 2e4;
|
|
1909
|
+
FRONT_MATTER_KEY = /^([\w.:-]+):(?:\s|$)/;
|
|
1861
1910
|
}
|
|
1862
1911
|
});
|
|
1863
1912
|
|
|
@@ -2745,12 +2794,19 @@ var ConfigSchema = Type.Object({
|
|
|
2745
2794
|
* (default: 5; 1 = every query). Configured via env/config file; not surfaced
|
|
2746
2795
|
* in the config TUI. */
|
|
2747
2796
|
YOUTUBE_QUERY_EVERY_N: Type.Number({ minimum: 1, maximum: 100, default: 5 }),
|
|
2748
|
-
/** Max queries per `search` call in quick (depth 0) research (default:
|
|
2797
|
+
/** Max queries per `search` call in quick (depth 0) research (default: 3, range: 1-10).
|
|
2749
2798
|
* Applies to every quick run — the pi extension tool, the CLI, and the SDK's
|
|
2750
2799
|
* `--depth 0` — since all three share the same researcher session factory. The
|
|
2751
2800
|
* ceiling is 10 so quick mode can never exceed the old 5–10 prompt guidance.
|
|
2752
2801
|
* Configured via env/config file; not surfaced in the config TUI. */
|
|
2753
|
-
QUICK_MAX_QUERIES: Type.Number({ minimum: 1, maximum: 10, default:
|
|
2802
|
+
QUICK_MAX_QUERIES: Type.Number({ minimum: 1, maximum: 10, default: 3 }),
|
|
2803
|
+
/** Max URLs scraped per `scrape` call in quick (depth 0) research (default: 5,
|
|
2804
|
+
* range: 1-20). Quick mode is for single-fact lookups, where the deep default
|
|
2805
|
+
* (MAX_SCRAPE_URLS, 8) pulls in pages the question does not need; its per-batch
|
|
2806
|
+
* budget is therefore separate. Batches are still capped by MAX_SCRAPE_BATCHES.
|
|
2807
|
+
* Applies to every quick run (pi extension tool, CLI, SDK `--depth 0`).
|
|
2808
|
+
* Configured via env/config file; not surfaced in the config TUI. */
|
|
2809
|
+
QUICK_MAX_SCRAPE_URLS: Type.Number({ minimum: 1, maximum: 20, default: 5 }),
|
|
2754
2810
|
/** Health check timeout in milliseconds (default: 10000ms) */
|
|
2755
2811
|
HEALTH_CHECK_TIMEOUT_MS: Type.Number({ minimum: 2e3, maximum: 12e4, default: 1e4 }),
|
|
2756
2812
|
/** Default timeout for browser page operations like search (default: 45000ms) */
|
|
@@ -142,7 +142,14 @@ export class QuickResearchOrchestrator {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
// Quick mode has its own, much shorter system prompt. The deep researcher
|
|
146
|
+
// prompt is written for a multi-round, sibling-coordinated, "exhaustive
|
|
147
|
+
// detail" report; reusing it here (with two placeholders blanked) fought
|
|
148
|
+
// every one of quick mode's design goals: it asked for a report where the
|
|
149
|
+
// deliverable is an answer, it advertised scrape rounds 2+ and a Session URL
|
|
150
|
+
// Pool quick runs cannot have, and "omitting information is a failure"
|
|
151
|
+
// pushed volume on a single-fact lookup.
|
|
152
|
+
const researcherPromptTemplate = loadPrompt('quick-researcher');
|
|
146
153
|
// Resolve the model here (same deterministic inputs createResearcherSession
|
|
147
154
|
// below will resolve again) purely to learn whether prompt caching will be
|
|
148
155
|
// active, so the batch count told to the model in the prompt below matches
|
|
@@ -155,6 +162,10 @@ export class QuickResearchOrchestrator {
|
|
|
155
162
|
const maxScrapeBatchesDisplay = maxScrapeBatches > 99 ? 'unlimited' : maxScrapeBatches.toString();
|
|
156
163
|
|
|
157
164
|
const maxQueries = this.config.QUICK_MAX_QUERIES;
|
|
165
|
+
// The quick per-batch URL budget: the same value createResearcherSession
|
|
166
|
+
// hands the scrape tool below, so the number the model reads is the number
|
|
167
|
+
// the tool enforces.
|
|
168
|
+
const maxScrapeUrls = this.config.QUICK_MAX_SCRAPE_URLS;
|
|
158
169
|
const quickEvidenceSection =
|
|
159
170
|
'## Search\n' +
|
|
160
171
|
`You have access to the \`search\` tool. You get EXACTLY ONE search call — make it count.\n` +
|
|
@@ -163,8 +174,8 @@ export class QuickResearchOrchestrator {
|
|
|
163
174
|
`For roughly one in ${this.config.YOUTUBE_QUERY_EVERY_N} of your queries, append the word 'youtube' (e.g. "<topic> explained youtube") — DuckDuckGo rarely surfaces YouTube otherwise, and YouTube links let you read video transcripts.\n` +
|
|
164
175
|
'Your goal is to gather a focused, high-quality pool of initial links.\n\n' +
|
|
165
176
|
'## Scrape\n' +
|
|
166
|
-
`After searching, scrape the
|
|
167
|
-
'Prioritize primary sources and authoritative data.';
|
|
177
|
+
`After searching, scrape the sources that can answer the question using the \`scrape\` tool (up to ${maxScrapeBatchesDisplay} batches, up to ${maxScrapeUrls} URLs each).\n` +
|
|
178
|
+
'Prioritize primary sources and authoritative data. Stop once the answer is sourced.';
|
|
168
179
|
|
|
169
180
|
// Consume queued steering messages before starting
|
|
170
181
|
consumeQueuedMessages(this.options.sessionId);
|
|
@@ -187,14 +198,11 @@ export class QuickResearchOrchestrator {
|
|
|
187
198
|
// prefix providers key on, so back-to-back quick runs (and every follow-up
|
|
188
199
|
// turn within this session) reuse it via prompt-cache reads.
|
|
189
200
|
const prompt = injectCurrentDate(researcherPromptTemplate, 'researcher')
|
|
190
|
-
.replace('{{extra_tool_guidelines}}', '- `search`:
|
|
201
|
+
.replace('{{extra_tool_guidelines}}', '- `search`: one web search call, executed up front.')
|
|
191
202
|
// Config-driven, mirroring researcher-executor: the prompt's per-batch
|
|
192
|
-
//
|
|
203
|
+
// cap can never drift from the cap the scrape tool enforces.
|
|
193
204
|
// replaceAll — the template states the budget more than once.
|
|
194
|
-
.replaceAll('{{max_scrape_urls}}', String(
|
|
195
|
-
// Quick research has no research lead and no next round — see
|
|
196
|
-
// RESEARCHER_DIGEST_SECTION. Its report IS the deliverable.
|
|
197
|
-
.replace('{{digest_section}}', '')
|
|
205
|
+
.replaceAll('{{max_scrape_urls}}', String(maxScrapeUrls))
|
|
198
206
|
.trim();
|
|
199
207
|
|
|
200
208
|
// `$`-bearing dynamic content (the user query, store descriptions, steering)
|
|
@@ -219,6 +227,7 @@ export class QuickResearchOrchestrator {
|
|
|
219
227
|
excludeTools: resolveExcludedTools(this.options.excludeTools),
|
|
220
228
|
config: this.config,
|
|
221
229
|
maxSearchQueries: this.config.QUICK_MAX_QUERIES,
|
|
230
|
+
maxScrapeUrls: this.config.QUICK_MAX_SCRAPE_URLS,
|
|
222
231
|
getGlobalState: (): SystemResearchState => ({
|
|
223
232
|
version: 1,
|
|
224
233
|
researchId: this.options.researchId,
|
|
@@ -45,6 +45,8 @@ export interface CreateResearcherSessionOptions {
|
|
|
45
45
|
config?: Config;
|
|
46
46
|
/** Overrides the search tool's query cap (default 30). Quick research passes QUICK_MAX_QUERIES. */
|
|
47
47
|
maxSearchQueries?: number;
|
|
48
|
+
/** Overrides the scrape tool's per-call URL cap (default MAX_SCRAPE_URLS). Quick research passes QUICK_MAX_SCRAPE_URLS. */
|
|
49
|
+
maxScrapeUrls?: number;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export interface ResolvedResearcherSession {
|
|
@@ -68,6 +70,7 @@ export async function createResearcherSession(options: CreateResearcherSessionOp
|
|
|
68
70
|
excludeTools = [],
|
|
69
71
|
config,
|
|
70
72
|
maxSearchQueries,
|
|
73
|
+
maxScrapeUrls,
|
|
71
74
|
} = options;
|
|
72
75
|
|
|
73
76
|
if (!systemPrompt || typeof systemPrompt !== 'string') {
|
|
@@ -104,6 +107,7 @@ export async function createResearcherSession(options: CreateResearcherSessionOp
|
|
|
104
107
|
onSearchProgress: onSearchProgress,
|
|
105
108
|
onUrlScrapeResult: onUrlScrapeResult,
|
|
106
109
|
maxSearchQueries,
|
|
110
|
+
maxScrapeUrls,
|
|
107
111
|
getTokensUsed: () => {
|
|
108
112
|
if (!sessionRef.session) return 0;
|
|
109
113
|
|