@lincoln504/pi-research 1.0.1 → 1.0.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 +1 -1
- package/dist/cli.mjs +69 -22
- package/docs/AGENT-SKILL.md +16 -3
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CONFIGURATION.md +3 -3
- package/docs/SDK.md +8 -1
- package/package.json +1 -1
- package/skills/pi-research/README.md +2 -2
- package/skills/pi-research/SKILL.md +6 -3
- package/skills/pi-research/references/configuration.md +7 -4
- package/skills/pi-research/scripts/run.mjs +3 -2
- package/src/cli.ts +48 -13
- package/src/orchestration/quick-research-orchestrator.ts +5 -1
- package/src/orchestration/researcher-executor.ts +23 -4
- package/src/research-config.ts +14 -1
- package/src/sdk.ts +35 -13
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ pi install npm:@lincoln504/pi-research
|
|
|
63
63
|
|
|
64
64
|
See [Agent skill](docs/AGENT-SKILL.md) or [SDK](docs/SDK.md) for standalone (no pi extension) installation.
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
The pi extension works out of the box — it runs on your pi session's model and pi's own configuration. Standalone use (agent skill / CLI / SDK) requires a configured model: set `PI_RESEARCH_MODEL` (e.g. `openai/gpt-4o`) as an environment variable or in `~/.pi/research/config.env`, plus `PI_RESEARCH_API_KEY` if you don't use pi. See [Configuration](docs/CONFIGURATION.md).
|
|
67
67
|
|
|
68
68
|
The first install pulls the stealth browser engine, which takes a few minutes.
|
|
69
69
|
|
package/dist/cli.mjs
CHANGED
|
@@ -20166,7 +20166,8 @@ ${prompt}`);
|
|
|
20166
20166
|
const msg2 = `Researcher ${id} (${researcherConfig.name}) timed out after ${config.RESEARCHER_TIMEOUT_MS}ms`;
|
|
20167
20167
|
session.abort().catch((err) => {
|
|
20168
20168
|
logger.warn("[ResearcherExecutor] Failed to abort timed-out researcher session:", err);
|
|
20169
|
-
})
|
|
20169
|
+
});
|
|
20170
|
+
reject(new Error(msg2));
|
|
20170
20171
|
}, config.RESEARCHER_TIMEOUT_MS);
|
|
20171
20172
|
});
|
|
20172
20173
|
let abortCleanup;
|
|
@@ -20248,9 +20249,18 @@ ${responseText}`);
|
|
|
20248
20249
|
}
|
|
20249
20250
|
} finally {
|
|
20250
20251
|
subscription();
|
|
20251
|
-
await
|
|
20252
|
-
|
|
20253
|
-
|
|
20252
|
+
await Promise.race([
|
|
20253
|
+
session.abort().catch((err) => {
|
|
20254
|
+
logger.warn(`[ResearcherExecutor] Failed to abort researcher session ${id}:`, err);
|
|
20255
|
+
}),
|
|
20256
|
+
new Promise((resolve8) => {
|
|
20257
|
+
const t = setTimeout(() => {
|
|
20258
|
+
logger.warn(`[ResearcherExecutor] Researcher ${id} session abort did not settle within 10s; continuing cleanup`);
|
|
20259
|
+
resolve8();
|
|
20260
|
+
}, 1e4);
|
|
20261
|
+
t.unref?.();
|
|
20262
|
+
})
|
|
20263
|
+
]);
|
|
20254
20264
|
sessionService?.unregisterSession(researchId, id);
|
|
20255
20265
|
if (ctx.mode !== "tui" && ctx.hasUI && typeof ctx.ui.setHiddenThinkingLabel === "function") {
|
|
20256
20266
|
ctx.ui.setHiddenThinkingLabel();
|
|
@@ -20526,7 +20536,8 @@ ${prompt}`);
|
|
|
20526
20536
|
const msg2 = `Quick research timed out after ${this.config.RESEARCHER_TIMEOUT_MS}ms`;
|
|
20527
20537
|
session.abort().catch((err) => {
|
|
20528
20538
|
logger.warn("[QuickOrchestrator] Failed to abort timed-out session:", err);
|
|
20529
|
-
})
|
|
20539
|
+
});
|
|
20540
|
+
reject(new Error(msg2));
|
|
20530
20541
|
}, this.config.RESEARCHER_TIMEOUT_MS);
|
|
20531
20542
|
});
|
|
20532
20543
|
let abortCleanup;
|
|
@@ -21989,14 +22000,15 @@ async function _doInit(options = {}) {
|
|
|
21989
22000
|
}
|
|
21990
22001
|
globalApiKey = options.apiKey || process.env["PI_RESEARCH_API_KEY"];
|
|
21991
22002
|
let parsedProvider = options.provider || process.env["PI_RESEARCH_PROVIDER"];
|
|
21992
|
-
|
|
21993
|
-
|
|
21994
|
-
|
|
22003
|
+
const effectiveModel = options.model ?? globalConfig.RESEARCH_MODEL;
|
|
22004
|
+
if (!parsedProvider && effectiveModel) {
|
|
22005
|
+
if (typeof effectiveModel === "string") {
|
|
22006
|
+
const slashIdx = effectiveModel.indexOf("/");
|
|
21995
22007
|
if (slashIdx > 0) {
|
|
21996
|
-
parsedProvider =
|
|
22008
|
+
parsedProvider = effectiveModel.slice(0, slashIdx);
|
|
21997
22009
|
}
|
|
21998
|
-
} else if (
|
|
21999
|
-
parsedProvider =
|
|
22010
|
+
} else if (effectiveModel.provider) {
|
|
22011
|
+
parsedProvider = effectiveModel.provider;
|
|
22000
22012
|
}
|
|
22001
22013
|
}
|
|
22002
22014
|
if (globalApiKey && !parsedProvider) {
|
|
@@ -22004,7 +22016,16 @@ async function _doInit(options = {}) {
|
|
|
22004
22016
|
}
|
|
22005
22017
|
globalRegistry = buildModelRegistry(globalApiKey, parsedProvider);
|
|
22006
22018
|
try {
|
|
22007
|
-
|
|
22019
|
+
if (effectiveModel && typeof effectiveModel === "object" && effectiveModel.id) {
|
|
22020
|
+
globalModel = effectiveModel;
|
|
22021
|
+
} else {
|
|
22022
|
+
globalModel = resolveModel(
|
|
22023
|
+
globalRegistry,
|
|
22024
|
+
typeof effectiveModel === "string" ? effectiveModel : void 0,
|
|
22025
|
+
parsedProvider,
|
|
22026
|
+
globalApiKey
|
|
22027
|
+
);
|
|
22028
|
+
}
|
|
22008
22029
|
globalContainer = getServiceContainer();
|
|
22009
22030
|
registerCoreServices(globalContainer);
|
|
22010
22031
|
registerInfrastructureServices(globalContainer);
|
|
@@ -22619,6 +22640,18 @@ function detectCredentials() {
|
|
|
22619
22640
|
};
|
|
22620
22641
|
}
|
|
22621
22642
|
if (piAuthPresent) {
|
|
22643
|
+
if (!model) {
|
|
22644
|
+
return {
|
|
22645
|
+
source: "pi-config",
|
|
22646
|
+
apiKeyConfigured: true,
|
|
22647
|
+
provider,
|
|
22648
|
+
model,
|
|
22649
|
+
modelFrom,
|
|
22650
|
+
piAuthPresent,
|
|
22651
|
+
piModelsPresent,
|
|
22652
|
+
problem: 'pi credentials were found (auth.json), but no research model is configured. The standalone CLI / agent skill run only on an explicitly configured model \u2014 they do not follow the model selected inside the pi extension. Set PI_RESEARCH_MODEL to a "provider/model-id" (in the environment, or in config.env / cli.env below).'
|
|
22653
|
+
};
|
|
22654
|
+
}
|
|
22622
22655
|
return {
|
|
22623
22656
|
source: "pi-config",
|
|
22624
22657
|
apiKeyConfigured: true,
|
|
@@ -22648,14 +22681,14 @@ function configBlock(det, extraNote) {
|
|
|
22648
22681
|
lines.push(` \u2022 base config file: ${paths.configEnv}`);
|
|
22649
22682
|
lines.push(` \u2022 cli overlay: ${paths.cliIfaceEnv} (optional; overrides base for this CLI / agent skill)`);
|
|
22650
22683
|
lines.push(` \u2022 env vars: PI_RESEARCH_API_KEY / PI_RESEARCH_PROVIDER / PI_RESEARCH_MODEL`);
|
|
22651
|
-
lines.push(` \u2022 pi
|
|
22652
|
-
lines.push(` \u2022 pi models:
|
|
22684
|
+
lines.push(` \u2022 pi config (keys): ${paths.piAuth}`);
|
|
22685
|
+
lines.push(` \u2022 pi config (models): ${paths.piModels}`);
|
|
22653
22686
|
lines.push("");
|
|
22654
22687
|
lines.push("Detected:");
|
|
22655
22688
|
lines.push(` \u2022 credential source: ${det.source}`);
|
|
22656
22689
|
lines.push(` \u2022 api key configured: ${det.apiKeyConfigured}`);
|
|
22657
|
-
lines.push(` \u2022 provider: ${det.provider ?? "(unset)"}`);
|
|
22658
|
-
lines.push(` \u2022 model: ${det.model
|
|
22690
|
+
lines.push(` \u2022 provider: ${det.provider ?? "(unset \u2014 inferred from a provider/model-id model)"}`);
|
|
22691
|
+
lines.push(` \u2022 model: ${det.model ? `${det.model} [from: ${det.modelFrom}]` : "(not set \u2014 set PI_RESEARCH_MODEL, required)"}`);
|
|
22659
22692
|
lines.push(` \u2022 pi auth.json: ${det.piAuthPresent ? "present" : "absent"}`);
|
|
22660
22693
|
lines.push(` \u2022 pi models.json: ${det.piModelsPresent ? "present" : "absent"}`);
|
|
22661
22694
|
if (extraNote) lines.push(` \u2022 note: ${extraNote}`);
|
|
@@ -22912,8 +22945,9 @@ uninstall: ${BINARY_NAME} skill uninstall
|
|
|
22912
22945
|
} catch (err) {
|
|
22913
22946
|
return reportError(err, "skill install", s.json);
|
|
22914
22947
|
}
|
|
22948
|
+
const skillModel = process.env["PI_RESEARCH_MODEL"] ?? getConfig(process.cwd(), "cli").RESEARCH_MODEL;
|
|
22915
22949
|
if (s.json) {
|
|
22916
|
-
toStdout(pretty({ command: "skill", action: "install", dryRun: !!s.dryRun, results: results2 }));
|
|
22950
|
+
toStdout(pretty({ command: "skill", action: "install", dryRun: !!s.dryRun, results: results2, modelConfigured: Boolean(skillModel) }));
|
|
22917
22951
|
} else {
|
|
22918
22952
|
toStdout(`pi-research agent skill \u2014 ${s.dryRun ? "install (dry run)" : "install"}
|
|
22919
22953
|
|
|
@@ -22923,6 +22957,15 @@ uninstall: ${BINARY_NAME} skill uninstall
|
|
|
22923
22957
|
toStdout(`
|
|
22924
22958
|
The skill activates automatically \u2014 ask the agent to research something.
|
|
22925
22959
|
`);
|
|
22960
|
+
if (!skillModel) {
|
|
22961
|
+
toStdout(
|
|
22962
|
+
`
|
|
22963
|
+
Required: set PI_RESEARCH_MODEL=provider/model-id in ${resolvedConfigPaths().configEnv}
|
|
22964
|
+
(or as an env var) \u2014 the skill runs only on this configured model, not your pi
|
|
22965
|
+
session model. Verify with: ${BINARY_NAME} status
|
|
22966
|
+
`
|
|
22967
|
+
);
|
|
22968
|
+
}
|
|
22926
22969
|
}
|
|
22927
22970
|
return results2.some((r) => r.status === "error") ? EXIT.SOFTWARE : EXIT.OK;
|
|
22928
22971
|
}
|
|
@@ -23080,11 +23123,15 @@ COMMANDS
|
|
|
23080
23123
|
CONFIGURE
|
|
23081
23124
|
Credentials are resolved in this order (first match wins):
|
|
23082
23125
|
|
|
23083
|
-
env vars:
|
|
23084
|
-
cli overlay:
|
|
23085
|
-
base config:
|
|
23086
|
-
pi
|
|
23087
|
-
pi models:
|
|
23126
|
+
env vars: PI_RESEARCH_API_KEY PI_RESEARCH_PROVIDER PI_RESEARCH_MODEL
|
|
23127
|
+
cli overlay: ${p.cliIfaceEnv} (optional; this CLI / agent skill only)
|
|
23128
|
+
base config: ${p.configEnv}
|
|
23129
|
+
pi config (keys): ${p.piAuth}
|
|
23130
|
+
pi config (models): ${p.piModels}
|
|
23131
|
+
|
|
23132
|
+
A model is REQUIRED: set PI_RESEARCH_MODEL to a "provider/model-id". This CLI and
|
|
23133
|
+
the agent skill run only on that configured model \u2014 they do not follow the model
|
|
23134
|
+
selected inside the pi extension.
|
|
23088
23135
|
|
|
23089
23136
|
The pi extension has its own optional overlay file:
|
|
23090
23137
|
pi extension: ${p.piIfaceEnv}
|
package/docs/AGENT-SKILL.md
CHANGED
|
@@ -10,7 +10,8 @@ pi-research software.
|
|
|
10
10
|
Already running the `pi` extension (`pi install npm:@lincoln504/pi-research`)? You
|
|
11
11
|
already have the engine — install the skill into your other agents with
|
|
12
12
|
`/research-config` → Install in External Agents (see [Installation flow](#installation-flow))
|
|
13
|
-
and skip the
|
|
13
|
+
and skip the install commands below. The model step still applies to you: the
|
|
14
|
+
skill runs on its own configured `PI_RESEARCH_MODEL`, not your pi session's model.
|
|
14
15
|
|
|
15
16
|
For standalone use without pi, install the engine globally, then link the skill into
|
|
16
17
|
every coding agent detected on this machine:
|
|
@@ -24,6 +25,18 @@ pi-research skill install # link the skill into Claude, Codex, Op
|
|
|
24
25
|
different skill in the slot, and records what it created so `pi-research skill uninstall`
|
|
25
26
|
removes exactly that. Run `pi-research skill status` to see where it is installed.
|
|
26
27
|
|
|
28
|
+
Then configure the model research runs on — the skill and standalone CLI use only
|
|
29
|
+
this explicitly configured model (they never follow the model selected inside the
|
|
30
|
+
pi extension) and refuse to start without one:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
# ~/.pi/research/config.env (or export as an env var)
|
|
34
|
+
PI_RESEARCH_MODEL=provider/model-id
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If you use `pi`, the API key comes from your pi configuration (`~/.pi/agent/auth.json`) automatically; otherwise
|
|
38
|
+
also set `PI_RESEARCH_API_KEY` (same file or env var). See [Configuration](CONFIGURATION.md).
|
|
39
|
+
|
|
27
40
|
On Windows, run `pi-research` from `cmd` or use `pi-research.cmd`: PowerShell's default
|
|
28
41
|
execution policy (`Restricted`) blocks npm's `.ps1` shims ("running scripts is
|
|
29
42
|
disabled"); or run `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once.
|
|
@@ -114,8 +127,8 @@ Manual. Symlink the directory into any agent's skills folder yourself:
|
|
|
114
127
|
### Prerequisites
|
|
115
128
|
|
|
116
129
|
- Node.js >= 22.19.0
|
|
117
|
-
- `pi-research` installed where the launcher can find it, plus a model
|
|
118
|
-
See [Configuration](CONFIGURATION.md).
|
|
130
|
+
- `pi-research` installed where the launcher can find it, plus a configured model
|
|
131
|
+
(`PI_RESEARCH_MODEL`) + API key. See [Configuration](CONFIGURATION.md).
|
|
119
132
|
|
|
120
133
|
```bash
|
|
121
134
|
npm install -g @lincoln504/pi-research
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -301,7 +301,7 @@ Browser & scraping
|
|
|
301
301
|
|
|
302
302
|
- [Camoufox](https://camoufox.com) — stealth Firefox (driven via [Playwright](https://playwright.dev)) for undetected search and scraping
|
|
303
303
|
- [poolifier](https://github.com/poolifier/poolifier) — the worker-process pool behind the browser workers
|
|
304
|
-
- [html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown)
|
|
304
|
+
- [html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown) — converts scraped HTML to Markdown (node-html-markdown serves as the pure-JS fallback)
|
|
305
305
|
- `pdf-oxide-wasm` — PDF text extraction (Rust/WASM)
|
|
306
306
|
|
|
307
307
|
Knowledge store & embeddings
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -75,7 +75,7 @@ Research
|
|
|
75
75
|
| `PI_RESEARCH_RETRY_DELAY_MS` | `2000` | 100–10000 | Base delay between retries. |
|
|
76
76
|
| `PI_RESEARCH_WORKER_THREADS` | `4` | 1–10 | Browser worker processes. Higher = more throughput, more CPU/RAM. |
|
|
77
77
|
| `PI_RESEARCH_WORKER_CONCURRENCY` | `2` | 1–10 | Tasks per worker process. |
|
|
78
|
-
| `PI_RESEARCH_MODEL` | _(session model)_ | — |
|
|
78
|
+
| `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. 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 evaluator keep using the session model. Accepts `provider/id` or a bare model id. |
|
|
79
79
|
| `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/evaluator prompt. |
|
|
80
80
|
| `PI_RESEARCH_REPORT_EXPORT_ENABLED` (TUI) | `false` | — | Front-ends write a Markdown report to disk and surface its path. |
|
|
81
81
|
| `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. |
|
|
@@ -128,11 +128,11 @@ See the [knowledge store doc](KNOWLEDGE-STORE.md) for what each value does.
|
|
|
128
128
|
| `PI_RESEARCH_EMBEDDING_MODEL_INIT_TIMEOUT_MS` | `300000` | 10000–600000 | Embedding-model initialization timeout (first-time download can be slow). |
|
|
129
129
|
| `PI_RESEARCH_WEBGPU_REPROBE` | _(unset)_ | — | Set `1` to discard the cached WebGPU-viability verdict and probe again on next use. |
|
|
130
130
|
|
|
131
|
-
API keys
|
|
131
|
+
API keys
|
|
132
132
|
|
|
133
133
|
| Variable | Description |
|
|
134
134
|
|----------|-------------|
|
|
135
|
-
| `PI_RESEARCH_API_KEY` / `PI_RESEARCH_PROVIDER` | Explicit LLM credentials for SDK / CLI mode (not needed when
|
|
135
|
+
| `PI_RESEARCH_API_KEY` / `PI_RESEARCH_PROVIDER` | Explicit LLM credentials for SDK / CLI mode (not needed when pi's configuration — `~/.pi/agent/auth.json` — supplies the key). On the CLI / agent skill both can also live in `config.env` / `cli.env`. The provider is required alongside the key, or inferred from a `provider/model-id` `PI_RESEARCH_MODEL`. |
|
|
136
136
|
| `STACKEXCHANGE_API_KEY` | Raises the Stack Exchange tool's limit from 300/day to 10,000/day. Obtain at <https://stackapps.com/apps/oauth>. |
|
|
137
137
|
| `GITHUB_TOKEN` | Raises the security tool's GitHub Advisory limit from 60/hr to 5000/hr (any default-scope token). |
|
|
138
138
|
| `NVD_API_KEY` | Raises the security tool's NVD limit ~10× and tightens request spacing. Request at <https://nvd.nist.gov/developers/request-an-api-key>. Recommended when using severity-filtered security searches: those issue a second (CVSS v2) NVD query to catch v2-only CVEs, which roughly doubles request time against the 6 s/request unauthenticated throttle. |
|
package/docs/SDK.md
CHANGED
|
@@ -13,6 +13,13 @@ 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
|
+
Then pick the model: pass `model` to `initResearchSDK`, or set `PI_RESEARCH_MODEL`
|
|
17
|
+
(env or `~/.pi/research/config.env`). The SDK never follows the model selected
|
|
18
|
+
inside the pi extension; only when neither is set does it fall back to the first
|
|
19
|
+
available model in your pi registry. The API key comes from your pi
|
|
20
|
+
configuration (`~/.pi/agent/auth.json`) automatically, or from the `apiKey`
|
|
21
|
+
option / `PI_RESEARCH_API_KEY` env var.
|
|
22
|
+
|
|
16
23
|
`src/sdk.ts` is a library for scripts, CI, and custom tooling. It is configured
|
|
17
24
|
from code, not from a global overlay file — there is no `sdk.env`. It reads the
|
|
18
25
|
base `~/.pi/research/config.env` as a baseline, and everything is
|
|
@@ -83,7 +90,7 @@ extension and the CLI / agent skill do it when `PI_RESEARCH_REPORT_EXPORT_ENABLE
|
|
|
83
90
|
|
|
84
91
|
| Option | Description |
|
|
85
92
|
|--------|-------------|
|
|
86
|
-
| `model` | `"provider/id"` string or a `Model` object. Omit to use the first available pi model. |
|
|
93
|
+
| `model` | `"provider/id"` string or a `Model` object. Omit to use the configured `PI_RESEARCH_MODEL`; only when neither is set does the SDK fall back to the first available pi model. |
|
|
87
94
|
| `apiKey` / `provider` | Explicit credentials (provider required with apiKey). |
|
|
88
95
|
| `config` | `Partial<Config>` overrides, applied over the base/defaults. |
|
|
89
96
|
| `ignoreGlobalConfig` | Skip `config.env` entirely — defaults + `process.env` + `config` only. |
|
package/package.json
CHANGED
|
@@ -39,8 +39,8 @@ model/key is missing.
|
|
|
39
39
|
### Prerequisites
|
|
40
40
|
|
|
41
41
|
- Node.js >= 22.19.0
|
|
42
|
-
- `pi-research` installed somewhere the launcher can find it, and a
|
|
43
|
-
API key
|
|
42
|
+
- `pi-research` installed somewhere the launcher can find it, and a configured
|
|
43
|
+
model (`PI_RESEARCH_MODEL`) + API key. See [`references/configuration.md`](references/configuration.md).
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
npm install -g @lincoln504/pi-research
|
|
@@ -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.0.
|
|
5
|
+
metadata: { "author": "Lincoln Deen", "version": "1.0.3", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
|
|
6
6
|
allowed-tools: Bash(node:*)
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -91,10 +91,13 @@ On exit 78 the engine prints exact configure locations to stderr — relay verba
|
|
|
91
91
|
```
|
|
92
92
|
config file: ~/.pi/research/config.env
|
|
93
93
|
env vars: PI_RESEARCH_API_KEY PI_RESEARCH_PROVIDER PI_RESEARCH_MODEL
|
|
94
|
-
pi
|
|
95
|
-
pi models:
|
|
94
|
+
pi config (keys): ~/.pi/agent/auth.json (supplies the API key only)
|
|
95
|
+
pi config (models): ~/.pi/agent/models.json
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
`PI_RESEARCH_MODEL` (a `provider/model-id`) is always required — pi's configuration
|
|
99
|
+
supplies only the key, and the skill never uses the model selected inside the pi extension.
|
|
100
|
+
|
|
98
101
|
Run `status` for the resolved paths on this machine. Full reference:
|
|
99
102
|
[`references/configuration.md`](references/configuration.md).
|
|
100
103
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
## Configuration
|
|
2
2
|
|
|
3
|
-
The skill drives the **pi-research** engine, which needs
|
|
3
|
+
The skill drives the **pi-research** engine, which needs an explicitly configured
|
|
4
|
+
model (`PI_RESEARCH_MODEL`) + API key — it runs only on that configured model and
|
|
5
|
+
never follows the model selected inside the pi extension.
|
|
4
6
|
Run `node "<SKILL_DIR>/scripts/run.mjs" status` to print what's detected and where
|
|
5
7
|
to configure (no research runs).
|
|
6
8
|
|
|
@@ -13,7 +15,8 @@ to configure (no research runs).
|
|
|
13
15
|
export PI_RESEARCH_MODEL=openai/gpt-4o # provider/model-id
|
|
14
16
|
```
|
|
15
17
|
2. **Global config** `~/.pi/research/config.env` (same KEY=VALUE keys; read at startup).
|
|
16
|
-
3. **pi
|
|
18
|
+
3. **pi configuration** (if you use `pi`): the API key comes from `~/.pi/agent/auth.json`
|
|
19
|
+
automatically — `PI_RESEARCH_MODEL` must still be set (env or `config.env`).
|
|
17
20
|
|
|
18
21
|
Real env vars always win. Optional CLI-only overlay `~/.pi/research/cli.env` layers
|
|
19
22
|
over `config.env` for the CLI only. Precedence:
|
|
@@ -23,7 +26,7 @@ over `config.env` for the CLI only. Precedence:
|
|
|
23
26
|
|
|
24
27
|
| Variable | Default | Description |
|
|
25
28
|
|----------|---------|-------------|
|
|
26
|
-
| `PI_RESEARCH_MODEL` |
|
|
29
|
+
| `PI_RESEARCH_MODEL` | _(required)_ | The `provider/model-id` all research runs on. |
|
|
27
30
|
| `PI_RESEARCH_DEFAULT_RESEARCH_DEPTH` | `1` | Default depth (1–3) when `--depth` omitted. |
|
|
28
31
|
| `PI_RESEARCH_MAX_RESEARCHERS` | `3` | Parallel researchers (1–5). |
|
|
29
32
|
| `PI_RESEARCH_TIMEOUT_MS` | `300000` | Per-researcher timeout (180000–1800000). |
|
|
@@ -70,7 +73,7 @@ model/key; message lists the fix) · `70` runtime error (network/provider/intern
|
|
|
70
73
|
`export PI_RESEARCH_BIN=/path/to/pi-research/dist/cli.mjs` (exact CLI entry;
|
|
71
74
|
checked first — also the escape hatch for unusual Windows shim layouts).
|
|
72
75
|
- **"No model or API key" (78 from engine):** set the three env vars (or config /
|
|
73
|
-
pi
|
|
76
|
+
pi's configuration), then re-run `status`.
|
|
74
77
|
- **Rate limit (70):** provider returned 429 — wait and retry.
|
|
75
78
|
- **GPU/embedding errors:** set `PI_RESEARCH_EMBEDDING_DEVICE=cpu` (headless/no-GPU).
|
|
76
79
|
|
|
@@ -139,10 +139,11 @@ function notInstalled() {
|
|
|
139
139
|
" # or, with pi: pi install npm:@lincoln504/pi-research",
|
|
140
140
|
" # or point at a copy: export PI_RESEARCH_PATH=/path/to/pi-research",
|
|
141
141
|
"",
|
|
142
|
-
"After installing, configure
|
|
142
|
+
"After installing, configure the model (PI_RESEARCH_MODEL is required) + API key.",
|
|
143
|
+
"Locations:",
|
|
143
144
|
` \u2022 env vars: PI_RESEARCH_API_KEY / PI_RESEARCH_PROVIDER / PI_RESEARCH_MODEL`,
|
|
144
145
|
` \u2022 global config file: ${join(home, ".pi", "research", "config.env")}`,
|
|
145
|
-
` \u2022 pi
|
|
146
|
+
` \u2022 pi config (keys): ${join(home, ".pi", "agent", "auth.json")} (supplies the API key only)`,
|
|
146
147
|
"",
|
|
147
148
|
"Run `status` once installed to verify detection.",
|
|
148
149
|
""
|
package/src/cli.ts
CHANGED
|
@@ -196,7 +196,7 @@ export interface ResolvedConfigPaths {
|
|
|
196
196
|
cliIfaceEnv: string;
|
|
197
197
|
/** Per-interface overlay for the pi extension (~/.pi/research/pi.env). */
|
|
198
198
|
piIfaceEnv: string;
|
|
199
|
-
/** pi
|
|
199
|
+
/** pi's key file (~/.pi/agent/auth.json), part of pi's configuration. */
|
|
200
200
|
piAuth: string;
|
|
201
201
|
/** pi model definitions (~/.pi/agent/models.json). */
|
|
202
202
|
piModels: string;
|
|
@@ -300,9 +300,29 @@ function detectCredentials(): CredentialDetection {
|
|
|
300
300
|
};
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
// --- pi
|
|
303
|
+
// --- pi configuration path (key from auth.json) -----------------------------
|
|
304
304
|
if (piAuthPresent) {
|
|
305
|
-
//
|
|
305
|
+
// Keys come from auth.json, but the MODEL must be configured explicitly:
|
|
306
|
+
// the standalone CLI / agent skill run only on the configured model — they
|
|
307
|
+
// never follow the model selected inside the pi extension, and silently
|
|
308
|
+
// falling back to the registry's first authed entry picks a model nobody
|
|
309
|
+
// chose. Fail fast here (pre-flight, before any SDK init).
|
|
310
|
+
if (!model) {
|
|
311
|
+
return {
|
|
312
|
+
source: 'pi-config',
|
|
313
|
+
apiKeyConfigured: true,
|
|
314
|
+
provider,
|
|
315
|
+
model,
|
|
316
|
+
modelFrom,
|
|
317
|
+
piAuthPresent,
|
|
318
|
+
piModelsPresent,
|
|
319
|
+
problem:
|
|
320
|
+
'pi credentials were found (auth.json), but no research model is configured. ' +
|
|
321
|
+
'The standalone CLI / agent skill run only on an explicitly configured model — they do not ' +
|
|
322
|
+
'follow the model selected inside the pi extension. Set PI_RESEARCH_MODEL to a ' +
|
|
323
|
+
'"provider/model-id" (in the environment, or in config.env / cli.env below).',
|
|
324
|
+
};
|
|
325
|
+
}
|
|
306
326
|
return {
|
|
307
327
|
source: 'pi-config',
|
|
308
328
|
apiKeyConfigured: true, // key lives in auth.json; SDK reads it
|
|
@@ -342,14 +362,14 @@ function configBlock(det: CredentialDetection, extraNote?: string): string {
|
|
|
342
362
|
lines.push(` • base config file: ${paths.configEnv}`);
|
|
343
363
|
lines.push(` • cli overlay: ${paths.cliIfaceEnv} (optional; overrides base for this CLI / agent skill)`);
|
|
344
364
|
lines.push(` • env vars: PI_RESEARCH_API_KEY / PI_RESEARCH_PROVIDER / PI_RESEARCH_MODEL`);
|
|
345
|
-
lines.push(` • pi
|
|
346
|
-
lines.push(` • pi models:
|
|
365
|
+
lines.push(` • pi config (keys): ${paths.piAuth}`);
|
|
366
|
+
lines.push(` • pi config (models): ${paths.piModels}`);
|
|
347
367
|
lines.push('');
|
|
348
368
|
lines.push('Detected:');
|
|
349
369
|
lines.push(` • credential source: ${det.source}`);
|
|
350
370
|
lines.push(` • api key configured: ${det.apiKeyConfigured}`);
|
|
351
|
-
lines.push(` • provider: ${det.provider ?? '(unset)'}`);
|
|
352
|
-
lines.push(` • model: ${det.model
|
|
371
|
+
lines.push(` • provider: ${det.provider ?? '(unset — inferred from a provider/model-id model)'}`);
|
|
372
|
+
lines.push(` • model: ${det.model ? `${det.model} [from: ${det.modelFrom}]` : '(not set — set PI_RESEARCH_MODEL, required)'}`);
|
|
353
373
|
lines.push(` • pi auth.json: ${det.piAuthPresent ? 'present' : 'absent'}`);
|
|
354
374
|
lines.push(` • pi models.json: ${det.piModelsPresent ? 'present' : 'absent'}`);
|
|
355
375
|
if (extraNote) lines.push(` • note: ${extraNote}`);
|
|
@@ -657,12 +677,23 @@ async function cmdSkill(s: NonNullable<ParsedArgs['skill']>): Promise<number> {
|
|
|
657
677
|
} catch (err) {
|
|
658
678
|
return reportError(err, 'skill install', s.json);
|
|
659
679
|
}
|
|
680
|
+
// The skill runs only on an explicitly configured model — if none is set yet,
|
|
681
|
+
// say so right here, at the moment of install (mirrors the TUI install action).
|
|
682
|
+
const skillModel =
|
|
683
|
+
process.env['PI_RESEARCH_MODEL'] ?? getConfig(process.cwd(), 'cli').RESEARCH_MODEL;
|
|
660
684
|
if (s.json) {
|
|
661
|
-
toStdout(pretty({ command: 'skill', action: 'install', dryRun: !!s.dryRun, results }));
|
|
685
|
+
toStdout(pretty({ command: 'skill', action: 'install', dryRun: !!s.dryRun, results, modelConfigured: Boolean(skillModel) }));
|
|
662
686
|
} else {
|
|
663
687
|
toStdout(`pi-research agent skill — ${s.dryRun ? 'install (dry run)' : 'install'}\n\n`);
|
|
664
688
|
for (const r of results) toStdout(` ${r.tool.padEnd(10)} ${describeInstall(r)}\n`);
|
|
665
689
|
toStdout(`\nThe skill activates automatically — ask the agent to research something.\n`);
|
|
690
|
+
if (!skillModel) {
|
|
691
|
+
toStdout(
|
|
692
|
+
`\nRequired: set PI_RESEARCH_MODEL=provider/model-id in ${resolvedConfigPaths().configEnv}\n` +
|
|
693
|
+
`(or as an env var) — the skill runs only on this configured model, not your pi\n` +
|
|
694
|
+
`session model. Verify with: ${BINARY_NAME} status\n`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
666
697
|
}
|
|
667
698
|
return results.some((r) => r.status === 'error') ? EXIT.SOFTWARE : EXIT.OK;
|
|
668
699
|
}
|
|
@@ -842,11 +873,15 @@ COMMANDS
|
|
|
842
873
|
CONFIGURE
|
|
843
874
|
Credentials are resolved in this order (first match wins):
|
|
844
875
|
|
|
845
|
-
env vars:
|
|
846
|
-
cli overlay:
|
|
847
|
-
base config:
|
|
848
|
-
pi
|
|
849
|
-
pi models:
|
|
876
|
+
env vars: PI_RESEARCH_API_KEY PI_RESEARCH_PROVIDER PI_RESEARCH_MODEL
|
|
877
|
+
cli overlay: ${p.cliIfaceEnv} (optional; this CLI / agent skill only)
|
|
878
|
+
base config: ${p.configEnv}
|
|
879
|
+
pi config (keys): ${p.piAuth}
|
|
880
|
+
pi config (models): ${p.piModels}
|
|
881
|
+
|
|
882
|
+
A model is REQUIRED: set PI_RESEARCH_MODEL to a "provider/model-id". This CLI and
|
|
883
|
+
the agent skill run only on that configured model — they do not follow the model
|
|
884
|
+
selected inside the pi extension.
|
|
850
885
|
|
|
851
886
|
The pi extension has its own optional overlay file:
|
|
852
887
|
pi extension: ${p.piIfaceEnv}
|
|
@@ -269,9 +269,13 @@ export class QuickResearchOrchestrator {
|
|
|
269
269
|
const timeoutPromise = new Promise<void>((_, reject) => {
|
|
270
270
|
timeoutId = setTimeout(() => {
|
|
271
271
|
const msg = `Quick research timed out after ${this.config.RESEARCHER_TIMEOUT_MS}ms`;
|
|
272
|
+
// Reject FIRST, abort as fire-and-forget cleanup — see researcher-executor:
|
|
273
|
+
// abort() awaits the stuck in-flight request, so gating the rejection on it
|
|
274
|
+
// settling disables the timeout exactly when it is needed.
|
|
272
275
|
session.abort().catch((err) => {
|
|
273
276
|
logger.warn('[QuickOrchestrator] Failed to abort timed-out session:', err);
|
|
274
|
-
})
|
|
277
|
+
});
|
|
278
|
+
reject(new Error(msg));
|
|
275
279
|
}, this.config.RESEARCHER_TIMEOUT_MS);
|
|
276
280
|
});
|
|
277
281
|
|
|
@@ -264,9 +264,14 @@ export async function runResearcher(options: RunResearcherOptions): Promise<void
|
|
|
264
264
|
const timeoutPromise = new Promise<void>((_, reject) => {
|
|
265
265
|
timeoutId = setTimeout(() => {
|
|
266
266
|
const msg = `Researcher ${id} (${researcherConfig.name}) timed out after ${config.RESEARCHER_TIMEOUT_MS}ms`;
|
|
267
|
+
// Reject FIRST, abort as fire-and-forget cleanup: session.abort() awaits the
|
|
268
|
+
// in-flight request, which is the very thing that is stuck (e.g. a provider
|
|
269
|
+
// retry loop against an exhausted quota) — gating the rejection on it settling
|
|
270
|
+
// makes the timeout ineffective exactly when it is needed.
|
|
267
271
|
session.abort().catch((err) => {
|
|
268
272
|
logger.warn('[ResearcherExecutor] Failed to abort timed-out researcher session:', err);
|
|
269
|
-
})
|
|
273
|
+
});
|
|
274
|
+
reject(new Error(msg));
|
|
270
275
|
}, config.RESEARCHER_TIMEOUT_MS);
|
|
271
276
|
});
|
|
272
277
|
|
|
@@ -375,9 +380,23 @@ export async function runResearcher(options: RunResearcherOptions): Promise<void
|
|
|
375
380
|
}
|
|
376
381
|
} finally {
|
|
377
382
|
subscription();
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
383
|
+
// Bounded: after a timeout, session.abort() awaits the very in-flight request
|
|
384
|
+
// that is stuck (e.g. a provider retry loop against an exhausted quota) and may
|
|
385
|
+
// never settle — never let per-attempt cleanup hang the retry loop on it. The
|
|
386
|
+
// retry builds a fresh session; the abandoned abort keeps draining in the
|
|
387
|
+
// background and its session dies with the run's teardown.
|
|
388
|
+
await Promise.race([
|
|
389
|
+
session.abort().catch((err) => {
|
|
390
|
+
logger.warn(`[ResearcherExecutor] Failed to abort researcher session ${id}:`, err);
|
|
391
|
+
}),
|
|
392
|
+
new Promise<void>((resolve) => {
|
|
393
|
+
const t = setTimeout(() => {
|
|
394
|
+
logger.warn(`[ResearcherExecutor] Researcher ${id} session abort did not settle within 10s; continuing cleanup`);
|
|
395
|
+
resolve();
|
|
396
|
+
}, 10_000);
|
|
397
|
+
t.unref?.();
|
|
398
|
+
}),
|
|
399
|
+
]);
|
|
381
400
|
sessionService?.unregisterSession(researchId, id);
|
|
382
401
|
|
|
383
402
|
// Restore the default thinking label now that the researcher is done.
|
package/src/research-config.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from '@earendil-works/pi-tui';
|
|
24
24
|
import { setInteractiveTuiActive, initGlobalTuiController } from './tui/tui-controller.ts';
|
|
25
25
|
import { normalizeSessionId } from './orchestration/session-state.ts';
|
|
26
|
-
import { getConfig, saveConfig, resetConfig, getDbDir } from './config.ts';
|
|
26
|
+
import { getConfig, saveConfig, resetConfig, getDbDir, getGlobalEnvFilePath } from './config.ts';
|
|
27
27
|
import { healthRegistry } from './healthcheck/index.ts';
|
|
28
28
|
import { getService, clearService, tryGetServiceContainerFromCtx } from './core/service-registry.ts';
|
|
29
29
|
import { ServiceNames, IKnowledgeStoreService } from './core/service-interfaces.ts';
|
|
@@ -648,6 +648,19 @@ async function installSkillAction(ctx: ExtensionContext, pi: ExtensionAPI): Prom
|
|
|
648
648
|
}
|
|
649
649
|
lines.push('', 'The skill activates automatically — just ask the agent to pi-research something.');
|
|
650
650
|
|
|
651
|
+
// The skill runs only on an explicitly configured model (it never follows the
|
|
652
|
+
// pi session model) — if none is configured yet, say so right here, at the
|
|
653
|
+
// moment of install, mirroring the CLI's `skill install` output.
|
|
654
|
+
const skillModel = process.env['PI_RESEARCH_MODEL'] ?? getConfig(ctx.cwd, 'cli').RESEARCH_MODEL;
|
|
655
|
+
if (!skillModel) {
|
|
656
|
+
lines.push(
|
|
657
|
+
'',
|
|
658
|
+
'**Required:** set `PI_RESEARCH_MODEL=provider/model-id` in ' +
|
|
659
|
+
`\`${getGlobalEnvFilePath()}\` (or as an env var) — the skill runs only on ` +
|
|
660
|
+
'this configured model, not your pi session model.'
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
651
664
|
pi.sendMessage({ customType: 'skill-install-result', content: lines.join('\n'), display: true });
|
|
652
665
|
if (ctx.hasUI) {
|
|
653
666
|
if (ok > 0) ctx.ui.notify(`Skill installed for ${ok} coding agent${ok === 1 ? '' : 's'}.`, 'info');
|
package/src/sdk.ts
CHANGED
|
@@ -144,17 +144,21 @@ function _removeSignalHandlers(): void {
|
|
|
144
144
|
* Options for initializing the Research SDK
|
|
145
145
|
*/
|
|
146
146
|
export interface ResearchSDKOptions {
|
|
147
|
-
/**
|
|
147
|
+
/**
|
|
148
148
|
* The LLM model to use for coordination, planning, and evaluation.
|
|
149
|
-
* If a string, it must match "provider/id" (e.g. "openai/gpt-4o")
|
|
150
|
-
* If omitted, the
|
|
149
|
+
* If a string, it must match "provider/id" (e.g. "openai/gpt-4o"); a Model
|
|
150
|
+
* object is used as-is. If omitted, the configured RESEARCH_MODEL
|
|
151
|
+
* (PI_RESEARCH_MODEL / config.env, or the `config` option) is used; only
|
|
152
|
+
* when neither is set does the SDK fall back to the first available model
|
|
153
|
+
* in pi's configuration. The SDK never follows the model selected
|
|
154
|
+
* interactively inside the pi extension.
|
|
151
155
|
*/
|
|
152
156
|
model?: string | Model<any>;
|
|
153
157
|
|
|
154
158
|
/**
|
|
155
159
|
* Optional explicit API key for the model.
|
|
156
160
|
* If provided, `provider` must also be set.
|
|
157
|
-
* If omitted, pi's
|
|
161
|
+
* If omitted, the key from pi's configuration (~/.pi/agent/auth.json) is used.
|
|
158
162
|
*/
|
|
159
163
|
apiKey?: string;
|
|
160
164
|
|
|
@@ -232,15 +236,18 @@ async function _doInit(options: ResearchSDKOptions = {}): Promise<void> {
|
|
|
232
236
|
globalApiKey = options.apiKey || process.env['PI_RESEARCH_API_KEY'];
|
|
233
237
|
let parsedProvider = options.provider || process.env['PI_RESEARCH_PROVIDER'];
|
|
234
238
|
|
|
235
|
-
// Infer provider from model if not explicitly provided
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
// Infer provider from the effective model if not explicitly provided. The
|
|
240
|
+
// effective model is the explicit option first, else the configured
|
|
241
|
+
// RESEARCH_MODEL — the same precedence the resolve below uses.
|
|
242
|
+
const effectiveModel = options.model ?? globalConfig.RESEARCH_MODEL;
|
|
243
|
+
if (!parsedProvider && effectiveModel) {
|
|
244
|
+
if (typeof effectiveModel === 'string') {
|
|
245
|
+
const slashIdx = effectiveModel.indexOf('/');
|
|
239
246
|
if (slashIdx > 0) {
|
|
240
|
-
parsedProvider =
|
|
247
|
+
parsedProvider = effectiveModel.slice(0, slashIdx);
|
|
241
248
|
}
|
|
242
|
-
} else if ((
|
|
243
|
-
parsedProvider = (
|
|
249
|
+
} else if ((effectiveModel as any).provider) {
|
|
250
|
+
parsedProvider = (effectiveModel as any).provider;
|
|
244
251
|
}
|
|
245
252
|
}
|
|
246
253
|
|
|
@@ -252,8 +259,23 @@ async function _doInit(options: ResearchSDKOptions = {}): Promise<void> {
|
|
|
252
259
|
globalRegistry = sharedBuildModelRegistry(globalApiKey, parsedProvider);
|
|
253
260
|
|
|
254
261
|
try {
|
|
255
|
-
// Resolve model
|
|
256
|
-
|
|
262
|
+
// Resolve the model. Precedence: explicit option (a Model object is used
|
|
263
|
+
// as-is; it need not exist in the registry) → configured RESEARCH_MODEL →
|
|
264
|
+
// provider default → registry fallback. Seeding the resolver with the
|
|
265
|
+
// configured model is what keeps the coordinator (which runs on ctx.model)
|
|
266
|
+
// on the SAME model as the researchers/synthesis (which read
|
|
267
|
+
// RESEARCH_MODEL themselves) — without it a configured model would govern
|
|
268
|
+
// only the sub-agents and planning would silently run on the fallback.
|
|
269
|
+
if (effectiveModel && typeof effectiveModel === 'object' && (effectiveModel as any).id) {
|
|
270
|
+
globalModel = effectiveModel as Model<any>;
|
|
271
|
+
} else {
|
|
272
|
+
globalModel = resolveModel(
|
|
273
|
+
globalRegistry,
|
|
274
|
+
typeof effectiveModel === 'string' ? effectiveModel : undefined,
|
|
275
|
+
parsedProvider,
|
|
276
|
+
globalApiKey
|
|
277
|
+
);
|
|
278
|
+
}
|
|
257
279
|
|
|
258
280
|
// Use the global container to ensure internal services can resolve dependencies
|
|
259
281
|
globalContainer = getServiceContainer();
|