@lincoln504/pi-research 1.3.11 → 1.4.1
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 -2
- package/dist/cli.mjs +5558 -4596
- package/dist/prompts/research-tool-usage.md +1 -1
- package/dist/prompts/researcher.md +1 -0
- package/dist/prompts/system-coordinator.md +1 -1
- package/dist/prompts/system-lead-router.md +122 -0
- package/dist/prompts/system-lead-synthesizer.md +46 -0
- package/dist/thread-worker.mjs +123 -30
- package/docs/ARCHITECTURE.md +69 -24
- package/docs/CHANGELOG.md +215 -0
- package/docs/CONFIGURATION.md +108 -8
- package/package.json +19 -11
- package/scripts/build.cjs +8 -1
- package/scripts/cleanup.cjs +26 -5
- package/scripts/setup.cjs +25 -1
- package/skills/pi-research/SKILL.md +1 -1
- package/src/cli.ts +41 -2
- package/src/config.ts +12 -8
- package/src/constants.ts +1 -1
- package/src/core/interfaces/observer-interfaces.ts +3 -3
- package/src/core/interfaces/orchestration-interfaces.ts +2 -0
- package/src/core/interfaces/planning-interfaces.ts +7 -1
- package/src/core/interfaces/scheduler-interfaces.ts +18 -2
- package/src/core/llm/agentic-repair.ts +7 -2
- package/src/core/llm/inject-date.ts +4 -1
- package/src/core/llm/llm-utils.ts +7 -0
- package/src/core/planning-service.ts +719 -91
- package/src/core/planning-utils.ts +21 -5
- package/src/core/service-initialization.ts +19 -3
- package/src/core/service-registry.ts +19 -3
- package/src/healthcheck/index.ts +11 -2
- package/src/index.ts +4 -3
- package/src/infrastructure/browser/browser-client.ts +68 -34
- package/src/infrastructure/browser/browser-task-scheduler.ts +220 -34
- package/src/infrastructure/browser/config.ts +77 -6
- package/src/infrastructure/browser/priority-task-queue.ts +55 -1
- package/src/infrastructure/browser/scheduler-factory.ts +12 -1
- package/src/infrastructure/browser/task-execution-service.ts +17 -7
- package/src/infrastructure/browser/thread-worker-browser.ts +66 -20
- package/src/infrastructure/browser/thread-worker-messaging.ts +38 -6
- package/src/infrastructure/browser/thread-worker.mjs +123 -30
- package/src/infrastructure/browser/thread-worker.ts +2 -2
- package/src/infrastructure/browser/worker-pool-manager.ts +127 -12
- package/src/infrastructure/embedding/embedding-client.ts +14 -4
- package/src/infrastructure/embedding/embedding-factory.ts +83 -15
- package/src/infrastructure/file-lock-service.ts +214 -19
- package/src/infrastructure/gpu-resource-service.ts +15 -1
- package/src/infrastructure/knowledge-store-service.ts +14 -0
- package/src/infrastructure/research-run-semaphore.ts +1 -1
- package/src/infrastructure/service-initialization.ts +7 -0
- package/src/knowledge/embedder-init.ts +60 -18
- package/src/knowledge/store.ts +11 -5
- package/src/logger.ts +11 -0
- package/src/orchestration/deep-research-orchestrator.ts +141 -87
- package/src/orchestration/quick-research-orchestrator.ts +11 -2
- package/src/orchestration/research-orchestration-service.ts +30 -1
- package/src/orchestration/research-synthesis-service.ts +53 -1
- package/src/orchestration/researcher-executor.ts +53 -2
- package/src/orchestration/researcher.ts +15 -2
- package/src/prompts/research-tool-usage.md +1 -1
- package/src/prompts/researcher.md +1 -0
- package/src/prompts/system-coordinator.md +1 -1
- package/src/prompts/system-lead-router.md +122 -0
- package/src/prompts/system-lead-synthesizer.md +46 -0
- package/src/sdk.ts +10 -1
- package/src/security/nvd-client.ts +55 -4
- package/src/security/osv-client.ts +13 -1
- package/src/tools/research-knowledge-search.ts +29 -4
- package/src/tools/scrape.ts +6 -0
- package/src/tools/search.ts +7 -0
- package/src/tools/security.ts +27 -9
- package/src/tools/stackexchange.ts +13 -7
- package/src/tools/youtube-transcript.ts +26 -12
- package/src/utils/cancellation.ts +36 -0
- package/src/utils/coverage-digest.ts +204 -0
- package/src/utils/llm-usage.ts +33 -6
- package/src/utils/log-utils.ts +16 -1
- package/src/utils/metrics-summary.ts +20 -4
- package/src/utils/metrics.ts +27 -1
- package/src/web-research/browser-search.ts +93 -20
- package/src/web-research/scraper-utils.ts +55 -14
- package/dist/prompts/system-lead-evaluator.md +0 -118
- package/src/prompts/system-lead-evaluator.md +0 -118
package/docs/CONFIGURATION.md
CHANGED
|
@@ -74,12 +74,12 @@ Research
|
|
|
74
74
|
| `PI_RESEARCH_MAX_RETRIES` | `2` | 0–5 | Retries per researcher request. |
|
|
75
75
|
| `PI_RESEARCH_RETRY_DELAY_MS` | `2000` | 100–10000 | Base delay between retries. |
|
|
76
76
|
| `PI_RESEARCH_MAX_FAILED_RESEARCHERS` | `2` | 1–10 | Unique researcher failures that abort the whole run. Raise to let slower, still-in-flight researchers finish before giving up. |
|
|
77
|
-
| `PI_RESEARCH_WORKER_THREADS` | `4` | 1–10 | Browser worker processes. Higher = more throughput, more CPU/RAM. |
|
|
77
|
+
| `PI_RESEARCH_WORKER_THREADS` | `4` | 1–10 | Browser worker processes. Higher = more throughput, more CPU/RAM. It sets how *fast* a search burst drains, not how large a burst may be — a burst larger than the pool waits its turn rather than being trimmed or timing out. |
|
|
78
78
|
| `PI_RESEARCH_WORKER_CONCURRENCY` | `2` | 1–10 | Tasks per worker process. |
|
|
79
79
|
| `PI_RESEARCH_MAX_CONCURRENT_RUNS` | `3` | ≥1 | Machine-wide cap on research runs executing at the same time, across **every** process (CLI, agent skill, pi extension, SDK). Runs beyond the cap queue rather than fail. All concurrent runs share one leader-elected browser/embedding pool, so oversubscribing it degrades every run at once. |
|
|
80
80
|
| `PI_RESEARCH_RUN_ACQUIRE_TIMEOUT_MS` | `600000` | ≥0 | How long a run queues for a free slot before failing with "maximum concurrent research runs reached" (CLI exit `75`). `0` = fail immediately instead of queueing. |
|
|
81
|
-
| `PI_RESEARCH_MODEL` | _(pi: session model; CLI/skill: required)_ | — | The model research runs on. **Required for the standalone CLI / agent skill** — they use only this configured model (never the model selected inside the pi extension) and refuse to start without one (the CLI's per-run `--model` flag also satisfies this). On the SDK it selects the session model when no `model` option is given. In the pi extension it overrides researcher sub-agents and knowledge synthesis, while the coordinator and
|
|
82
|
-
| `PI_RESEARCH_DISABLED_TOOLS` | _(none)_ | — | Comma-separated research tools to disable for a run (`search`, `scrape`, `security_search`, `stackexchange`, `youtube_transcript`, `grep`). Removed from every researcher's toolset and named in the coordinator
|
|
81
|
+
| `PI_RESEARCH_MODEL` | _(pi: session model; CLI/skill: required)_ | — | The model research runs on. **Required for the standalone CLI / agent skill** — they use only this configured model (never the model selected inside the pi extension) and refuse to start without one (the CLI's per-run `--model` flag also satisfies this). On the SDK it selects the session model when no `model` option is given. In the pi extension it overrides researcher sub-agents and knowledge synthesis, while the coordinator and research lead keep using the session model. Accepts `provider/id` or a bare model id. |
|
|
82
|
+
| `PI_RESEARCH_DISABLED_TOOLS` | _(none)_ | — | Comma-separated research tools to disable for a run (`search`, `scrape`, `security_search`, `stackexchange`, `youtube_transcript`, `grep`, `read`). Removed from every researcher's toolset and named in the coordinator and research-lead prompts. Strictly additive — it can only remove capabilities, never grant one, and it stacks on top of the default exclusions rather than replacing them. An unrecognized name excludes nothing and is warned about rather than failing the run. |
|
|
83
83
|
| `PI_RESEARCH_REPORT_EXPORT_ENABLED` (TUI) | `false` | — | Front-ends write a Markdown report to disk and surface its path. |
|
|
84
84
|
| `PI_RESEARCH_REPORT_EXPORT_DIR` | _(smart cwd)_ | — | Pin exported reports to a fixed directory, bypassing the cwd-relative resolution. Useful for the agent skill, which runs from the host agent's arbitrary directory. |
|
|
85
85
|
| `PI_RESEARCH_MAX_SCRAPE_TOKEN_FRACTION_FOR_SCRAPING` | `0.15` | 0.05–1.0 | Max fraction of the context window used for initial scrape context. |
|
|
@@ -99,10 +99,10 @@ Timeouts
|
|
|
99
99
|
|
|
100
100
|
| Variable | Default | Range | Description |
|
|
101
101
|
|----------|---------|-------|-------------|
|
|
102
|
-
| `PI_RESEARCH_LLM_TIMEOUT_MS` | `300000` | 60000–1800000 | Coordinator /
|
|
102
|
+
| `PI_RESEARCH_LLM_TIMEOUT_MS` | `300000` | 60000–1800000 | Coordinator / research-lead / repair / knowledge LLM call timeout. |
|
|
103
103
|
| `PI_RESEARCH_SCRAPE_TIMEOUT_MS` | `15000` | 5000–120000 | Per-page scrape (page-load) timeout. |
|
|
104
104
|
| `PI_RESEARCH_SEARCH_TIMEOUT_MS` | `45000` | 5000–120000 | Browser search page timeout. |
|
|
105
|
-
| `PI_RESEARCH_BROWSER_TASK_TIMEOUT_MS` | `10000` | 2000–120000 |
|
|
105
|
+
| `PI_RESEARCH_BROWSER_TASK_TIMEOUT_MS` | `10000` | 2000–120000 | Overhead margin added to each browser op's own timeout (a search task ceiling is `SEARCH_TIMEOUT_MS` + this; a scrape is `SCRAPE_TIMEOUT_MS` + this). These ceilings bound **execution**: a task's clock starts when a worker picks it up, so time spent queued behind other work is not charged against it and raising this to cover a busy pool is neither needed nor effective. |
|
|
106
106
|
| `PI_RESEARCH_HEALTH_CHECK_TIMEOUT_MS` | `10000` | 2000–120000 | Pre-flight health-check timeout. |
|
|
107
107
|
|
|
108
108
|
LLM output & reasoning
|
|
@@ -111,8 +111,8 @@ These are advanced, env-only knobs (not in the TUI).
|
|
|
111
111
|
|
|
112
112
|
| Variable | Default | Range | Description |
|
|
113
113
|
|----------|---------|-------|-------------|
|
|
114
|
-
| `PI_RESEARCH_LLM_THINKING_LEVEL` | `off` | off · minimal · low · medium · high | Chain-of-thought level for all engine LLM work (coordinator,
|
|
115
|
-
| `PI_RESEARCH_PLANNING_MAX_TOKENS` | `16384` | 1024–131072 | Max output tokens for the plan
|
|
114
|
+
| `PI_RESEARCH_LLM_THINKING_LEVEL` | `off` | off · minimal · low · medium · high | Chain-of-thought level for all engine LLM work (coordinator, router, synthesizer, JSON-repair, knowledge extraction, and researcher sub-agents). Off by default — these calls emit structured JSON / cited reports, so a thinking block only consumes the output budget and can truncate the answer. Clamped per model by pi. |
|
|
115
|
+
| `PI_RESEARCH_PLANNING_MAX_TOKENS` | `16384` | 1024–131072 | Max output tokens for the coordinator's plan. The router's decision has its own, smaller ceiling; the final synthesis uses `PI_RESEARCH_SYNTHESIS_MAX_TOKENS`. Clamped to the model's real ceiling. |
|
|
116
116
|
| `PI_RESEARCH_SYNTHESIS_MAX_TOKENS` | `32768` | 1024–131072 | Max output tokens for the final synthesized report. Clamped to the model's real ceiling. |
|
|
117
117
|
|
|
118
118
|
Knowledge store
|
|
@@ -156,7 +156,8 @@ Diagnostics & platform
|
|
|
156
156
|
| `PI_RESEARCH_SKILL_DIR` | _(auto)_ | Override the bundled research-skill source directory used by the skill installer. |
|
|
157
157
|
| `PI_RESEARCH_PURGE_BROWSERS` | _(unset)_ | Read by the bundled `scripts/cleanup.cjs`: set `1` to also delete the shared camoufox browser cache (kept by default because other installs may use it). Note npm ≥7 does not run `preuninstall`, so that script does not fire on `npm uninstall` — see [AGENT-SKILL.md](AGENT-SKILL.md). |
|
|
158
158
|
| `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` | _(unset)_ | Set `1` during `npm install` to skip the camoufox browser download (fetched lazily on first use instead; standard Playwright convention). |
|
|
159
|
-
| `
|
|
159
|
+
| `CAMOUFOX_INSTALL_DIR` | _(user cache)_ | camoufox-js's own variable, and the only one that can relocate the browser. Sets where pi-research looks the binary up, and is exported to the postinstall fetch and to browser workers. **Not effective on the currently pinned camoufox-js (0.10.x), which hardcodes its cache directory and honours no variable** — so relocating the browser is unsupported until that pin can advance (see ARCHITECTURE.md). |
|
|
160
|
+
| `PLAYWRIGHT_BROWSERS_PATH` | _(user cache)_ | Alias for the above, accepted for compatibility and exported onward as `CAMOUFOX_INSTALL_DIR`. Same caveat: on the pinned camoufox-js it moves only where pi-research looks, not where the download lands, so setting it reports the browser missing from the custom path after a successful install. |
|
|
160
161
|
| `XDG_CACHE_HOME` | `~/.cache` | Standard XDG variable. When set, every `~/.cache/pi-research/...` path below is rooted at `$XDG_CACHE_HOME/pi-research/...` instead. |
|
|
161
162
|
| `PI_RESEARCH_BIN` (alias `PI_RESEARCH_PATH`) | _(auto)_ | Agent-skill launcher only: explicit path to the pi-research engine binary when auto-resolution (PATH → local install → npx) should be bypassed. See [skills/pi-research/references/configuration.md](../skills/pi-research/references/configuration.md). |
|
|
162
163
|
| `PLAYWRIGHT_INSTALL_DEPS` | _(unset)_ | Linux only. Set `true` during `npm install` to also install system libraries via `npx playwright install-deps` (same as `npm run install:system-deps`). |
|
|
@@ -238,6 +239,105 @@ just export the variable for that process.
|
|
|
238
239
|
> `/research-config` TUI (which writes it), export variables in your shell, or use a
|
|
239
240
|
> loader such as direnv. `.env.example` is a reference, not an active config file.
|
|
240
241
|
|
|
242
|
+
### Prompt caching
|
|
243
|
+
|
|
244
|
+
A research run re-sends a large, mostly-unchanged prompt many times: a researcher
|
|
245
|
+
re-sends its whole conversation (including every scraped page) on each tool turn, and
|
|
246
|
+
the research lead used to re-send the accumulated findings on every round. Every provider will
|
|
247
|
+
serve that repetition from a prompt cache at a fraction of the input price — but only
|
|
248
|
+
for an **exact prefix** of the request, and only if nothing that varies sits in front
|
|
249
|
+
of it.
|
|
250
|
+
|
|
251
|
+
What pi-research does about it. The research lead is split into two roles so that the
|
|
252
|
+
repetition is removed rather than merely discounted. The **router** decides each round
|
|
253
|
+
whether to continue, reading each report in full on the one round it arrives and only its
|
|
254
|
+
short coverage digest thereafter, instead of re-reading every report collected so far. The **synthesizer** runs once, at the end, and is the only
|
|
255
|
+
call that reads the reports in full. Before the split, one call did both jobs and re-sent
|
|
256
|
+
the whole corpus every round, so its input grew with the square of the round count. The
|
|
257
|
+
synthesizer's corpus is also budgeted against the model's context window: over budget, the
|
|
258
|
+
reports are reduced in partial passes and merged rather than truncated or rejected.
|
|
259
|
+
|
|
260
|
+
How often the router runs depends on the round budget. Depth 2 and 3 route on every round
|
|
261
|
+
but the last. Depth 1's base budget is two rounds and the last one is skipped, so a plain
|
|
262
|
+
depth-1 run never routes — but steering raises the budget (up to two extra rounds), and a
|
|
263
|
+
steered depth-1 run does route. That is also the depth-1 case carrying the most findings,
|
|
264
|
+
so it is where routing on digests rather than on the full corpus matters most.
|
|
265
|
+
|
|
266
|
+
On top of that, both lead prompts are laid out stable-part-first: they interpolate only
|
|
267
|
+
values fixed for the whole run (complexity, team size, query budget, disabled tools), and
|
|
268
|
+
everything that changes between rounds — root query, round number, agenda, executed
|
|
269
|
+
queries, steering, round-phase guidance — is appended afterwards as a `RUN CONTEXT`
|
|
270
|
+
block. Unit tests hold the layout in place; if you edit
|
|
271
|
+
`src/prompts/system-lead-router.md` or `src/prompts/system-lead-synthesizer.md`, keep
|
|
272
|
+
run-varying text out of them.
|
|
273
|
+
|
|
274
|
+
What your provider does about it depends on which API pi talks to (the provider's `api`
|
|
275
|
+
field in `~/.pi/agent/models.json`) and, on OpenAI-compatible endpoints, on one `compat`
|
|
276
|
+
key:
|
|
277
|
+
|
|
278
|
+
| Provider API | Behaviour |
|
|
279
|
+
|--------------|-----------|
|
|
280
|
+
| `anthropic-messages` | pi inserts `cache_control` breakpoints automatically — on the system prompt, the last tool definition, and the last user/assistant/tool-result block. Nothing to configure. |
|
|
281
|
+
| `openai-completions` | pi inserts the **same** Anthropic-style breakpoints into the OpenAI-shaped payload whenever `compat.cacheControlFormat` is `"anthropic"`. Auto-detection sets that key for exactly one case — OpenRouter routing to an `anthropic/*` model — so by default no markers are sent and the provider's own implicit prefix caching is relied on. That default is correct for OpenAI, DeepSeek, Gemini and GLM, which all cache implicitly. |
|
|
282
|
+
| `openai-responses` | No markers at any setting; implicit caching only. |
|
|
283
|
+
|
|
284
|
+
Note that the marker behaviour is gated on `cacheControlFormat`, **not** on the `api`
|
|
285
|
+
field — setting the key makes an `openai-completions` provider emit explicit breakpoints
|
|
286
|
+
exactly like an `anthropic-messages` one. That matters when reading the counters below:
|
|
287
|
+
on a provider you have configured this way, a zero cache read is not evidence that
|
|
288
|
+
markers are missing.
|
|
289
|
+
|
|
290
|
+
The gap is providers that need explicit markers but are reached over an OpenAI-compatible
|
|
291
|
+
endpoint and are not the one auto-detected case. Anything in that category (Qwen through
|
|
292
|
+
OpenRouter, or any gateway fronting Claude) caches **nothing at all**, silently and with
|
|
293
|
+
no error. Fix it per provider with a `compat` block:
|
|
294
|
+
|
|
295
|
+
```json
|
|
296
|
+
{
|
|
297
|
+
"providers": {
|
|
298
|
+
"openrouter": {
|
|
299
|
+
"api": "openai-completions",
|
|
300
|
+
"compat": {
|
|
301
|
+
"cacheControlFormat": "anthropic",
|
|
302
|
+
"sessionAffinityFormat": "openrouter",
|
|
303
|
+
"sendSessionAffinityHeaders": true
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`cacheControlFormat: "anthropic"` forces the markers on. It is safe to set for a whole
|
|
311
|
+
provider: models that do not use markers ignore the extra field. The two session-affinity
|
|
312
|
+
keys ask OpenRouter to keep a conversation on the replica that warmed its cache; without
|
|
313
|
+
them OpenRouter falls back to hashing the first messages, which an agentic loop mutates.
|
|
314
|
+
|
|
315
|
+
`PI_CACHE_RETENTION=long` (a pi variable, not a pi-research one) requests 1-hour
|
|
316
|
+
retention where the provider supports it. It raises the cache-write multiplier from
|
|
317
|
+
1.25x to 2x, so it only pays off when a run's gaps between calls exceed the provider's
|
|
318
|
+
default window — nominally 5 minutes.
|
|
319
|
+
|
|
320
|
+
Measure before enabling it. On a direct probe against GLM over `anthropic-messages`, an
|
|
321
|
+
entry was still read back unchanged after **seven minutes at the default retention**, and
|
|
322
|
+
`long` produced an identical number — so on that route the doubled write multiplier buys
|
|
323
|
+
nothing. The nominal 5-minute figure is a floor providers are free to exceed, not a
|
|
324
|
+
deadline you can plan around.
|
|
325
|
+
|
|
326
|
+
Verifying it works. Set `PI_RESEARCH_DEBUG=true` and read the run log: every LLM call
|
|
327
|
+
records `llm_cache_read_tokens_total` and `llm_cache_write_tokens_total` alongside
|
|
328
|
+
`llm_tokens_total`, labelled by component (`coordinator` / `router` / `synthesizer` /
|
|
329
|
+
`researcher`).
|
|
330
|
+
Cache reads that stay at zero on a multi-turn researcher mean the prefix is not being
|
|
331
|
+
matched — usually a provider with a minimum cacheable prefix (commonly 1024–4096 tokens)
|
|
332
|
+
that short prompts never reach, or an explicit-marker provider missing the `compat` block
|
|
333
|
+
above. A counter that is absent rather than zero means the provider does not report
|
|
334
|
+
caching at all.
|
|
335
|
+
|
|
336
|
+
Cache **writes** are the weaker signal of the two. Several providers report reads and
|
|
337
|
+
omit writes entirely — OpenRouter and Z.ai's Anthropic-compatible endpoint both return
|
|
338
|
+
non-zero `cache_read` alongside a flat zero `cache_write` — so a zero there is normal and
|
|
339
|
+
says nothing about whether caching is working. Judge it by the read counter.
|
|
340
|
+
|
|
241
341
|
### Where files live
|
|
242
342
|
|
|
243
343
|
All pi-research state lives under its own namespace, `~/.pi/research/`:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lincoln504/pi-research",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.4.1",
|
|
4
|
+
"description": "Web research for your agents with smart and safe tooling + knowledge store",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"main": "./src/index.ts",
|
|
@@ -53,19 +53,27 @@
|
|
|
53
53
|
"pi-package",
|
|
54
54
|
"pi-extension",
|
|
55
55
|
"pi-coding-agent",
|
|
56
|
+
"claude-code",
|
|
56
57
|
"openclaw",
|
|
58
|
+
"codex",
|
|
59
|
+
"agent-skills",
|
|
57
60
|
"agent-skill",
|
|
58
61
|
"skills",
|
|
59
|
-
"
|
|
62
|
+
"web-search",
|
|
60
63
|
"web-research",
|
|
61
64
|
"deep-research",
|
|
62
|
-
"
|
|
63
|
-
"
|
|
65
|
+
"ai-research",
|
|
66
|
+
"research",
|
|
67
|
+
"web-scraping",
|
|
64
68
|
"scraping",
|
|
65
|
-
"
|
|
66
|
-
"duckduckgo",
|
|
69
|
+
"browser-automation",
|
|
67
70
|
"stealth-browser",
|
|
68
|
-
"camoufox"
|
|
71
|
+
"camoufox",
|
|
72
|
+
"duckduckgo",
|
|
73
|
+
"knowledge-store",
|
|
74
|
+
"llm-tools",
|
|
75
|
+
"web-access",
|
|
76
|
+
"agent"
|
|
69
77
|
],
|
|
70
78
|
"author": "Lincoln Deen",
|
|
71
79
|
"contributors": [
|
|
@@ -120,7 +128,7 @@
|
|
|
120
128
|
"@earendil-works/pi-tui": ">=0.84.0 <1",
|
|
121
129
|
"@huggingface/transformers": "4.2.0",
|
|
122
130
|
"@kreuzberg/html-to-markdown-node": "^3.5.5",
|
|
123
|
-
"@lancedb/lancedb": "0.
|
|
131
|
+
"@lancedb/lancedb": "0.37.1",
|
|
124
132
|
"apache-arrow": "21.1.0",
|
|
125
133
|
"bgutils-js": "^3.2.0",
|
|
126
134
|
"camoufox-js": "^0.10.2",
|
|
@@ -133,7 +141,7 @@
|
|
|
133
141
|
"poolifier": "^5.3.2",
|
|
134
142
|
"typebox": "1.3.7",
|
|
135
143
|
"undici": "^8.9.0",
|
|
136
|
-
"youtubei.js": "^
|
|
144
|
+
"youtubei.js": "^18.0.0"
|
|
137
145
|
},
|
|
138
146
|
"optionalDependencies": {
|
|
139
147
|
"webgpu": "^0.4.0"
|
|
@@ -154,7 +162,7 @@
|
|
|
154
162
|
},
|
|
155
163
|
"devDependencies": {
|
|
156
164
|
"@eslint/js": "^10.0.1",
|
|
157
|
-
"@types/jsdom": "^
|
|
165
|
+
"@types/jsdom": "^30.0.0",
|
|
158
166
|
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
|
159
167
|
"@typescript-eslint/parser": "^8.65.0",
|
|
160
168
|
"@vitest/coverage-v8": "^4.1.10",
|
package/scripts/build.cjs
CHANGED
|
@@ -87,9 +87,16 @@ function copyThreadWorker() {
|
|
|
87
87
|
// 'prompts')), so the templates must sit next to the bundle.
|
|
88
88
|
function copyPrompts() {
|
|
89
89
|
fs.mkdirSync(p('dist', 'prompts'), { recursive: true });
|
|
90
|
-
|
|
90
|
+
const sources = fs.readdirSync(p('src', 'prompts'));
|
|
91
|
+
for (const f of sources) {
|
|
91
92
|
fs.copyFileSync(p('src', 'prompts', f), p('dist', 'prompts', f));
|
|
92
93
|
}
|
|
94
|
+
// Prune prompts that no longer exist in src. Copying alone leaves a deleted or renamed
|
|
95
|
+
// prompt behind in an incremental dist forever; verify-package rejects the orphan at
|
|
96
|
+
// publish time, which surfaces as a confusing failure long after the rename.
|
|
97
|
+
for (const f of fs.readdirSync(p('dist', 'prompts'))) {
|
|
98
|
+
if (!sources.includes(f)) fs.rmSync(p('dist', 'prompts', f));
|
|
99
|
+
}
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
const TARGETS = {
|
package/scripts/cleanup.cjs
CHANGED
|
@@ -197,13 +197,34 @@ const dirName = rawDirName && !rawDirName.includes('/') && !rawDirName.includes(
|
|
|
197
197
|
* several tools' state by name); a substring match would treat that shared
|
|
198
198
|
* folder as exclusively ours and wholesale-delete it. Only the exact default
|
|
199
199
|
* layout is something WE constructed and can therefore prove is ours.
|
|
200
|
+
*
|
|
201
|
+
* Segment-adjacency is NOT that proof either, and used to be what this did:
|
|
202
|
+
* it tested whether `<dirName>/research` appeared ANYWHERE in the path, with no
|
|
203
|
+
* constraint on what followed. That also matched:
|
|
204
|
+
* - the research ROOT itself (`~/.pi/research`) — which holds config.env and
|
|
205
|
+
* knowledge_db/, the two things this script's header promises to preserve;
|
|
206
|
+
* - any unrelated directory beneath it (`/mnt/backup/.pi/research/mystate`);
|
|
207
|
+
* - traversal forms (`~/.pi/research/../../..`) that resolve far outside our
|
|
208
|
+
* tree entirely.
|
|
209
|
+
*
|
|
210
|
+
* The real proof is the full default *shape*: the runtime only ever builds
|
|
211
|
+
* `<root>/<dirName>/research/state`. Requiring those three trailing segments on
|
|
212
|
+
* the RESOLVED path keeps a relocated-but-default layout reclaimable while
|
|
213
|
+
* refusing the research root, sibling directories, and traversal escapes.
|
|
200
214
|
*/
|
|
215
|
+
const DEFAULT_STATE_TAIL = [dirName, 'research', 'state'];
|
|
216
|
+
|
|
201
217
|
function isPiResearchNamespaced(p) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
218
|
+
// Resolve first so `..` cannot smuggle the tail past this check.
|
|
219
|
+
const resolved = path.resolve(p);
|
|
220
|
+
// Windows and macOS default to case-insensitive filesystems, so a path
|
|
221
|
+
// differing only in case still names the directory we built.
|
|
222
|
+
const fold = (s) =>
|
|
223
|
+
process.platform === 'win32' || process.platform === 'darwin' ? s.toLowerCase() : s;
|
|
224
|
+
const segs = resolved.split(/[\\/]+/).filter(Boolean);
|
|
225
|
+
if (segs.length < DEFAULT_STATE_TAIL.length) return false;
|
|
226
|
+
const tail = segs.slice(-DEFAULT_STATE_TAIL.length);
|
|
227
|
+
return DEFAULT_STATE_TAIL.every((want, i) => fold(tail[i]) === fold(want));
|
|
207
228
|
}
|
|
208
229
|
|
|
209
230
|
/**
|
package/scripts/setup.cjs
CHANGED
|
@@ -47,8 +47,25 @@ function resolveCamoufoxBin() {
|
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The custom camoufox location, if the user set one. Mirrors
|
|
52
|
+
* getCustomCamoufoxDir() in src/infrastructure/browser/config.ts — the two must
|
|
53
|
+
* agree or install and lookup land in different directories.
|
|
54
|
+
*
|
|
55
|
+
* CAMOUFOX_INSTALL_DIR first: it is camoufox-js's own variable and the only one
|
|
56
|
+
* that can relocate the install (camoufox-js <0.12.0 hardcodes
|
|
57
|
+
* userCacheDir("camoufox") and honours nothing). PLAYWRIGHT_BROWSERS_PATH is
|
|
58
|
+
* kept as the documented alias and is exported BELOW as CAMOUFOX_INSTALL_DIR.
|
|
59
|
+
* NOTE: inert on the pinned camoufox-js 0.10.x, which reads neither — this is
|
|
60
|
+
* forward-compatible plumbing, not a live fix. See src/infrastructure/browser/
|
|
61
|
+
* config.ts and thread-worker-browser.ts.
|
|
62
|
+
*/
|
|
63
|
+
function customCamoufoxDir() {
|
|
64
|
+
return process.env.CAMOUFOX_INSTALL_DIR || process.env.PLAYWRIGHT_BROWSERS_PATH || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
function camoufoxCachePath() {
|
|
51
|
-
const customPath =
|
|
68
|
+
const customPath = customCamoufoxDir();
|
|
52
69
|
if (customPath) return customPath;
|
|
53
70
|
|
|
54
71
|
if (isWindows) {
|
|
@@ -68,6 +85,13 @@ if (process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === '1') {
|
|
|
68
85
|
console.log('pi-research: skipping browser download (PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1)');
|
|
69
86
|
} else {
|
|
70
87
|
const env = { ...process.env };
|
|
88
|
+
// Make the fetch honour the custom location. Without this the download always
|
|
89
|
+
// went to camoufox's default cache while camoufoxCachePath() reported the
|
|
90
|
+
// custom one: the "already installed" probe never matched, every install
|
|
91
|
+
// re-fetched, and at runtime the browser was reported missing despite having
|
|
92
|
+
// downloaded successfully.
|
|
93
|
+
const customDir = customCamoufoxDir();
|
|
94
|
+
if (customDir) env.CAMOUFOX_INSTALL_DIR = customDir;
|
|
71
95
|
|
|
72
96
|
const installDeps = process.argv.includes('--system-deps') || process.env.PLAYWRIGHT_INSTALL_DEPS === 'true';
|
|
73
97
|
if (installDeps && isLinux) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: pi-research
|
|
3
3
|
description: "Multi-agent web research for any coding agent. Use for anything requiring current information from the internet — news, trends, product/service/company facts, statistics, people, places, events, APIs, library behavior, or 'what is X / how does X work' questions. Also use to search a local knowledge store of previously researched findings before doing live research. Powered by the pi-research engine (search, stealth scraping, security databases, Stack Exchange). Not for reading local files, running project commands, or analyzing this repository's own source."
|
|
4
4
|
license: MIT
|
|
5
|
-
metadata: { "author": "Lincoln Deen", "version": "1.
|
|
5
|
+
metadata: { "author": "Lincoln Deen", "version": "1.4.1", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
|
|
6
6
|
allowed-tools: Bash(node:*)
|
|
7
7
|
---
|
|
8
8
|
|
package/src/cli.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { getConfig, getGlobalConfigDir, getGlobalEnvFilePath, getInterfaceEnvFil
|
|
|
45
45
|
import { getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
46
46
|
import { buildModelRegistry, safeGetAvailable } from './core/llm/model-registry-factory.ts';
|
|
47
47
|
import { RESEARCH_TOOL_NAMES } from './constants.ts';
|
|
48
|
+
import { isBrowserAvailable } from './infrastructure/browser/config.ts';
|
|
48
49
|
|
|
49
50
|
// ---------------------------------------------------------------------------
|
|
50
51
|
// Constants
|
|
@@ -167,7 +168,11 @@ function makeProgressObserver(): HeadlessObserverOptions {
|
|
|
167
168
|
toStderr(` • round ${data?.round}\n`);
|
|
168
169
|
break;
|
|
169
170
|
case 'search_start':
|
|
170
|
-
|
|
171
|
+
// "N queries", not "N query/s" — the old `query/${n===1?'':'s'}` form rendered a
|
|
172
|
+
// COUNT as what reads unmistakably as a RATE ("45 query/s"), so a burst that was
|
|
173
|
+
// in fact 45 queries looked like 45 per second and then appeared frozen. It also
|
|
174
|
+
// emitted a bare trailing slash ("1 query/") in the singular.
|
|
175
|
+
toStderr(` • search burst: ${data?.queries?.length ?? 0} ${(data?.queries?.length ?? 0) === 1 ? 'query' : 'queries'}\n`);
|
|
171
176
|
break;
|
|
172
177
|
case 'researcher_start':
|
|
173
178
|
toStderr(` • researcher ${data?.id}: ${data?.name}\n`);
|
|
@@ -770,6 +775,18 @@ async function cmdStatus(json?: boolean): Promise<number> {
|
|
|
770
775
|
const det = await detectCredentials();
|
|
771
776
|
const paths = resolvedConfigPaths();
|
|
772
777
|
const cfg = getConfig(process.cwd(), 'cli');
|
|
778
|
+
// The stealth browser is installed by this package's `postinstall`
|
|
779
|
+
// (scripts/setup.cjs), which npm 11.19+ BLOCKS BY DEFAULT — it now warns
|
|
780
|
+
// "packages have install scripts not yet covered by allowScripts" and carries
|
|
781
|
+
// on, so the browser is simply absent on an otherwise successful install.
|
|
782
|
+
// Every other native artifact survives that (onnxruntime, sharp, esbuild and
|
|
783
|
+
// webgpu all ship prebuilt platform packages), which is exactly what makes
|
|
784
|
+
// this one worth reporting: nothing else fails, and without this the first
|
|
785
|
+
// symptom is a scrape falling back mid-run. A missing browser degrades
|
|
786
|
+
// capability rather than breaking the tool — the fetch path still serves most
|
|
787
|
+
// pages — so it is reported alongside `ready` rather than folded into it.
|
|
788
|
+
// isBrowserAvailable() is a resolve + existsSync; it never launches anything.
|
|
789
|
+
const browserInstalled = isBrowserAvailable();
|
|
773
790
|
const summary = {
|
|
774
791
|
package: '@lincoln504/pi-research',
|
|
775
792
|
version: PKG_VERSION,
|
|
@@ -783,6 +800,14 @@ async function cmdStatus(json?: boolean): Promise<number> {
|
|
|
783
800
|
modelFrom: det.modelFrom,
|
|
784
801
|
problem: det.problem ?? null,
|
|
785
802
|
},
|
|
803
|
+
browser: {
|
|
804
|
+
installed: browserInstalled,
|
|
805
|
+
// Same remedy the health check prints, so both surfaces name one command.
|
|
806
|
+
fix: browserInstalled ? null : 'npx camoufox-js fetch',
|
|
807
|
+
note: browserInstalled
|
|
808
|
+
? null
|
|
809
|
+
: 'Stealth browser not installed — scraping falls back to plain fetch, which some sites block. npm 11.19+ blocks install scripts by default, which skips this package\'s postinstall.',
|
|
810
|
+
},
|
|
786
811
|
knowledgeStoreMode: cfg.KNOWLEDGE_STORE_MODE,
|
|
787
812
|
defaultDepth: cfg.DEFAULT_RESEARCH_DEPTH,
|
|
788
813
|
paths,
|
|
@@ -796,6 +821,10 @@ async function cmdStatus(json?: boolean): Promise<number> {
|
|
|
796
821
|
toStdout(`pi-research ${PKG_VERSION}\n`);
|
|
797
822
|
toStdout(`ready: ${summary.ready ? 'yes' : 'no'}\n\n`);
|
|
798
823
|
toStdout(configBlock(det, summary.ready ? undefined : '— fix the problem above, then re-run.') + '\n');
|
|
824
|
+
if (!summary.browser.installed) {
|
|
825
|
+
toStdout(`\nstealth browser: NOT INSTALLED — run: ${summary.browser.fix}\n`);
|
|
826
|
+
toStdout(` ${summary.browser.note}\n`);
|
|
827
|
+
}
|
|
799
828
|
toStdout(`\nknowledge store mode: ${cfg.KNOWLEDGE_STORE_MODE} (default depth: ${cfg.DEFAULT_RESEARCH_DEPTH})\n`);
|
|
800
829
|
return EXIT.OK;
|
|
801
830
|
}
|
|
@@ -1064,7 +1093,17 @@ export async function reportError(err: unknown, what: string, json?: boolean): P
|
|
|
1064
1093
|
// --json: emit a structured error on stdout so a machine consumer never has to
|
|
1065
1094
|
// special-case plain-text stderr. Mirrors the { ok: true, ... } success shape.
|
|
1066
1095
|
if (json) {
|
|
1067
|
-
|
|
1096
|
+
// A cancellation reports itself as a cancellation, not as whatever component
|
|
1097
|
+
// happened to notice the abort first. The internal message is genuinely
|
|
1098
|
+
// misleading here — a Ctrl-C unwinding through the planner surfaces as
|
|
1099
|
+
// "Coordinator failed: Request was aborted", so a consumer relaying `error`
|
|
1100
|
+
// verbatim (which SKILL.md instructs agents to do) tells the user a component
|
|
1101
|
+
// broke when they simply stopped the run. The plain-text branch below already
|
|
1102
|
+
// refuses to say "failed" for a cancel; JSON said it anyway. The original
|
|
1103
|
+
// wording is preserved under `cause` so nothing diagnostic is lost.
|
|
1104
|
+
const payload: Record<string, unknown> = isCancellation
|
|
1105
|
+
? { ok: false, error: `pi-research ${what} cancelled.`, cause: msg, exitCode }
|
|
1106
|
+
: { ok: false, error: msg, exitCode };
|
|
1068
1107
|
// Machine-readable "this will succeed later" flag, so a consumer can back off
|
|
1069
1108
|
// and retry without string-matching the human message. Gated on cancellation:
|
|
1070
1109
|
// a Ctrl-C landing in the same window a queue-wait expires must never emit
|
package/src/config.ts
CHANGED
|
@@ -88,7 +88,7 @@ export const ConfigSchema = Type.Object({
|
|
|
88
88
|
* never killed before it has used its full nav budget plus time waiting in the queue.
|
|
89
89
|
* (default: 10000) */
|
|
90
90
|
BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2000, maximum: 120000, default: 10000 }),
|
|
91
|
-
/** Timeout for coordinator/
|
|
91
|
+
/** Timeout for coordinator/research-lead/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
|
|
92
92
|
* Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
|
|
93
93
|
* Ceiling raised to 30min (matching RESEARCHER_TIMEOUT_MS): a slow model on a
|
|
94
94
|
* large-context synthesis/plan call can legitimately run >10min, and the old
|
|
@@ -96,7 +96,7 @@ export const ConfigSchema = Type.Object({
|
|
|
96
96
|
* (e.g. 900000), causing those calls to time out mid-generation. */
|
|
97
97
|
LLM_TIMEOUT_MS: Type.Number({ minimum: 60000, maximum: 1800000, default: 300000 }),
|
|
98
98
|
/** Chain-of-thought "thinking" level for the engine's own LLM calls (coordinator,
|
|
99
|
-
*
|
|
99
|
+
* router, synthesizer, JSON-repair, knowledge extraction) AND the researcher
|
|
100
100
|
* sub-agents. Default 'off': these calls emit structured JSON / cited reports, not
|
|
101
101
|
* open-ended reasoning, so thinking only burns the output-token budget (often
|
|
102
102
|
* truncating before the JSON/report block) and adds latency for at most a marginal
|
|
@@ -111,22 +111,26 @@ export const ConfigSchema = Type.Object({
|
|
|
111
111
|
Type.Literal('medium'),
|
|
112
112
|
Type.Literal('high'),
|
|
113
113
|
], { default: 'off' }),
|
|
114
|
-
/** Max output tokens for the coordinator plan
|
|
115
|
-
*
|
|
114
|
+
/** Max output tokens for the coordinator's round-1 plan. Default 16384. The routing
|
|
115
|
+
* decision has its OWN, much smaller ceiling (it emits a decision, never a report,
|
|
116
|
+
* and cannot synthesize mid-round under the split protocol); the final report uses
|
|
117
|
+
* SYNTHESIS_MAX_TOKENS below. The blanket
|
|
116
118
|
* 4096 cap previously throttled every call and could be exhausted by a thinking block
|
|
117
119
|
* before any text was emitted. Capped at the model's real ceiling at call time.
|
|
118
120
|
* Not exposed in TUI — controlled via PI_RESEARCH_PLANNING_MAX_TOKENS env var. */
|
|
119
121
|
PLANNING_MAX_TOKENS: Type.Number({ minimum: 1024, maximum: 131072, default: 16384 }),
|
|
120
|
-
/** Max output tokens for the final synthesized research report (the
|
|
121
|
-
*
|
|
122
|
+
/** Max output tokens for the final synthesized research report (the synthesizer call,
|
|
123
|
+
* and each pass of a reduce when the corpus does not fit in one). Default 32768 so a
|
|
124
|
+
* full cited report is never truncated. It is also the reserve subtracted from the
|
|
125
|
+
* model's context window when budgeting that corpus. Capped at
|
|
122
126
|
* the model's real maxTokens at call time.
|
|
123
127
|
* Not exposed in TUI — controlled via PI_RESEARCH_SYNTHESIS_MAX_TOKENS env var. */
|
|
124
128
|
SYNTHESIS_MAX_TOKENS: Type.Number({ minimum: 1024, maximum: 131072, default: 32768 }),
|
|
125
129
|
/** LLM Model override for researcher sub-agents and knowledge synthesis.
|
|
126
130
|
* Format: provider/model-id (e.g. google/gemini-2.0-flash-001) or just model-id.
|
|
127
131
|
* When set, this overrides ctx.model for researcher sub-agents (both deep and quick)
|
|
128
|
-
* and the knowledge synthesis background LLM. The coordinator and
|
|
129
|
-
* use the caller's model (ctx.model).
|
|
132
|
+
* and the knowledge synthesis background LLM. The coordinator and the research lead
|
|
133
|
+
* always use the caller's model (ctx.model).
|
|
130
134
|
*/
|
|
131
135
|
RESEARCH_MODEL: Type.Optional(Type.String()),
|
|
132
136
|
/** Explicit directory for the knowledge store database (overrides default) */
|
package/src/constants.ts
CHANGED
|
@@ -69,7 +69,7 @@ export function getUnitsPerResearcher(config?: Config): number {
|
|
|
69
69
|
return 1 + Math.min(batches, 4);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/** Progress bar weight per lead
|
|
72
|
+
/** Progress bar weight per research-lead round */
|
|
73
73
|
export const LEAD_EVAL_UNITS = 2;
|
|
74
74
|
|
|
75
75
|
|
|
@@ -58,9 +58,9 @@ export interface ResearchObserver {
|
|
|
58
58
|
// Synthesis/Completion
|
|
59
59
|
/**
|
|
60
60
|
* Fired when the run enters FINAL synthesis / stops consuming steering:
|
|
61
|
-
* - deep mode: right before the
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* - deep mode: right before the synthesizer call, and on the path where the
|
|
62
|
+
* router chose to finish early (idempotent there — the flag is already off
|
|
63
|
+
* via onEvaluationDecision('synthesize')).
|
|
64
64
|
* - quick mode: as soon as the researcher session settles, i.e. the point
|
|
65
65
|
* the 500ms steering-consume poller stops running.
|
|
66
66
|
* TUI consequence: steeringAcceptable flips false, so a late steer takes the
|
|
@@ -59,6 +59,8 @@ export interface RunResearchersOptions {
|
|
|
59
59
|
export interface IResearchSynthesisService extends IService {
|
|
60
60
|
storeReport(researchId: string, researcherId: string, report: string): void;
|
|
61
61
|
getAllReports(researchId: string): Map<string, string>;
|
|
62
|
+
/** Coverage digest per stored report, derived when a researcher emitted none. */
|
|
63
|
+
getAllDigests(researchId: string): Map<string, string>;
|
|
62
64
|
getReport(researchId: string, id: string): string | undefined;
|
|
63
65
|
getReportsForRound(researchId: string, round: number): Map<string, string>;
|
|
64
66
|
getReportCount(researchId: string): number;
|
|
@@ -56,6 +56,12 @@ export interface GenerateQueriesOptions {
|
|
|
56
56
|
export interface UpdatePlanOptions {
|
|
57
57
|
sessionId: string;
|
|
58
58
|
reports: Map<string, string>;
|
|
59
|
+
/** Coverage digest per report id. The router reads the FRESH round's reports in full
|
|
60
|
+
* (evidence-based routing) and falls back to these digests only for PRIOR rounds — it
|
|
61
|
+
* does not read digests instead of the current round's reports. Optional: when omitted,
|
|
62
|
+
* digests are derived from the report bodies so a caller that doesn't track them still
|
|
63
|
+
* routes on coverage rather than on nothing. */
|
|
64
|
+
digests?: Map<string, string>;
|
|
59
65
|
round: number;
|
|
60
66
|
query: string;
|
|
61
67
|
complexity: 1 | 2 | 3;
|
|
@@ -98,7 +104,7 @@ export interface IPlanningService extends IService {
|
|
|
98
104
|
getTeamSize(complexity: 1 | 2 | 3): number;
|
|
99
105
|
getQueryBudget(complexity: 1 | 2 | 3): number;
|
|
100
106
|
getComplexityGuidance(complexity: 1 | 2 | 3, maxTeamSize: number, queryBudget: number): string;
|
|
101
|
-
getEvaluatorComplexityGuidance(complexity: 1 | 2 | 3): string;
|
|
107
|
+
getEvaluatorComplexityGuidance(complexity: 1 | 2 | 3, researchRounds: number): string;
|
|
102
108
|
getRoundPhaseGuidance(currentRound: number, maxRounds: number, complexity: 1 | 2 | 3, maxTeamSize: number): string;
|
|
103
109
|
capResearcherQueries(plan: ResearchPlan, complexity: 1 | 2 | 3, serviceName: string): ResearchPlan;
|
|
104
110
|
parseJsonPlan(text: string): ResearchPlan;
|
|
@@ -70,9 +70,25 @@ export interface ScrapeResult {
|
|
|
70
70
|
/**
|
|
71
71
|
* Base scheduler interface
|
|
72
72
|
*/
|
|
73
|
+
/**
|
|
74
|
+
* Reports when a task's EXECUTION starts and stops.
|
|
75
|
+
*
|
|
76
|
+
* Callers that keep their own guard over a scheduler call arm it on 'dispatched'
|
|
77
|
+
* rather than at call time, so queue wait never counts against the work's budget,
|
|
78
|
+
* and disarm it on 'settled' so the guard is not left running over a period in
|
|
79
|
+
* which nothing is executing — between a failed attempt and its retry, that
|
|
80
|
+
* period includes a backoff sleep, a re-enqueue and a fresh queue wait, and a
|
|
81
|
+
* timer left armed across it kills the retry for the predecessor's overrun.
|
|
82
|
+
*
|
|
83
|
+
* An implementation that cannot observe dispatch (BrowserClient, which only sees
|
|
84
|
+
* the leader's final reply) never calls it at all, and the caller's guard stays
|
|
85
|
+
* disarmed — that implementation bounds the call itself.
|
|
86
|
+
*/
|
|
87
|
+
export type TaskDispatchListener = (event: 'dispatched' | 'settled') => void;
|
|
88
|
+
|
|
73
89
|
export interface IScheduler {
|
|
74
|
-
runSearch(query: string, config?: Config, signal?: AbortSignal): Promise<SearchResult[]>;
|
|
75
|
-
runScrape(url: string, config?: Config, signal?: AbortSignal): Promise<ScrapeResult>;
|
|
90
|
+
runSearch(query: string, config?: Config, signal?: AbortSignal, onTaskEvent?: TaskDispatchListener): Promise<SearchResult[]>;
|
|
91
|
+
runScrape(url: string, config?: Config, signal?: AbortSignal, onTaskEvent?: TaskDispatchListener): Promise<ScrapeResult>;
|
|
76
92
|
runHealthCheck(config?: Config, signal?: AbortSignal): Promise<{ success: boolean }>;
|
|
77
93
|
shutdown(): Promise<void>;
|
|
78
94
|
resetIdleTimerOnActivity?(): void;
|
|
@@ -34,6 +34,10 @@ export interface JsonRepairOptions {
|
|
|
34
34
|
maxTokens?: number;
|
|
35
35
|
/** Thinking level for the repair pass (default 'off' — repair emits JSON, not reasoning). */
|
|
36
36
|
thinkingLevel?: ModelThinkingLevel;
|
|
37
|
+
/** Research session id, forwarded to the provider as its prompt-cache / session-affinity
|
|
38
|
+
* key (see llm-utils.buildSafeOptions). Keeps the repair pass on the same upstream
|
|
39
|
+
* replica as the call it is repairing. */
|
|
40
|
+
sessionId?: string;
|
|
37
41
|
/** Usage sink invoked once per billed LLM attempt with the raw usage object from
|
|
38
42
|
* the response. Lets callers attribute the repair pass's tokens/cost to the run
|
|
39
43
|
* (metrics + observer) exactly as they do for the primary call. Every attempt that
|
|
@@ -70,7 +74,7 @@ export async function repairJsonWithLlm<T = any>(
|
|
|
70
74
|
auth: { apiKey: string; headers?: Record<string, string | null> },
|
|
71
75
|
options: JsonRepairOptions
|
|
72
76
|
): Promise<T | null> {
|
|
73
|
-
const { model, context, schema, serviceName = 'RepairService', signal, maxTokens = 16384, thinkingLevel = 'off' } = options;
|
|
77
|
+
const { model, context, schema, serviceName = 'RepairService', signal, maxTokens = 16384, thinkingLevel = 'off', sessionId } = options;
|
|
74
78
|
|
|
75
79
|
logger.warn(`[${serviceName}] JSON parse failed; attempting agentic salvage`);
|
|
76
80
|
|
|
@@ -118,7 +122,8 @@ Return ONLY the valid JSON object. No prose before or after.`;
|
|
|
118
122
|
],
|
|
119
123
|
}, buildSafeOptions(model, {
|
|
120
124
|
...auth,
|
|
121
|
-
signal
|
|
125
|
+
signal,
|
|
126
|
+
...(sessionId ? { sessionId } : {}),
|
|
122
127
|
}, maxTokens, thinkingLevel)),
|
|
123
128
|
llmTimeout, `agentic-repair-${serviceName}`,
|
|
124
129
|
);
|
|
@@ -35,7 +35,10 @@ function getCurrentDateString(): string {
|
|
|
35
35
|
* training-cutoff defaults). The full date follows for precision. Applies to
|
|
36
36
|
* coordinator, researcher, and evaluator across all modes.
|
|
37
37
|
*/
|
|
38
|
-
export function injectCurrentDate(
|
|
38
|
+
export function injectCurrentDate(
|
|
39
|
+
prompt: string,
|
|
40
|
+
_agentType: 'coordinator' | 'researcher' | 'router' | 'synthesizer',
|
|
41
|
+
): string {
|
|
39
42
|
const monthYear = getCurrentMonthYearString();
|
|
40
43
|
const fullDate = getCurrentDateString();
|
|
41
44
|
// Derive the stale-year examples from the current year so the guidance stays correct in any
|
|
@@ -22,6 +22,13 @@ import { logger } from '../../logger.ts';
|
|
|
22
22
|
* thinking block only consumes the output-token budget (often truncating before the
|
|
23
23
|
* text block is emitted) for at most a marginal quality gain that does not justify the cost.
|
|
24
24
|
*
|
|
25
|
+
* Callers should also pass the research `sessionId` in `options`. pi-ai forwards it as the
|
|
26
|
+
* provider's prompt-cache / session-affinity key — OpenAI's `prompt_cache_key`, OpenRouter's
|
|
27
|
+
* `x-session-id` — which keeps a run's calls on the replica that warmed their cache. Without
|
|
28
|
+
* it OpenRouter falls back to hashing the first system and user messages, which every round
|
|
29
|
+
* of a research run mutates, so it re-routes and re-warms. Providers that do not implement
|
|
30
|
+
* session affinity ignore the field, so passing it is unconditionally safe.
|
|
31
|
+
*
|
|
25
32
|
* @param model - The model being called
|
|
26
33
|
* @param options - Caller-provided options (an explicit `reasoning` here wins)
|
|
27
34
|
* @param defaultCap - Default maxTokens cap (clamped to model.maxTokens)
|