@lincoln504/pi-research 1.0.8 → 1.0.9
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/dist/cli.mjs +55 -68
- package/dist/thread-worker.mjs +7 -3
- package/docs/CHANGELOG.md +30 -9
- package/docs/CONFIGURATION.md +1 -1
- package/package.json +16 -15
- package/skills/pi-research/SKILL.md +1 -1
- package/src/cli.ts +10 -10
- package/src/config.ts +8 -4
- package/src/core/llm/model-registry-factory.ts +77 -25
- package/src/index.ts +9 -5
- package/src/infrastructure/browser/thread-worker.mjs +7 -3
- package/src/orchestration/researcher.ts +12 -1
- package/src/sdk.ts +1 -1
- package/src/utils/make-resource-loader.ts +13 -23
- package/src/web-research/browser-search.ts +17 -5
package/dist/cli.mjs
CHANGED
|
@@ -2485,7 +2485,7 @@ function createConfig(env, processEnv) {
|
|
|
2485
2485
|
SEARCH_TIMEOUT_MS: parseEnvNumber(e, "PI_RESEARCH_SEARCH_TIMEOUT_MS", DEFAULTS.SEARCH_TIMEOUT_MS, 5e3, 12e4),
|
|
2486
2486
|
TUI_REFRESH_DEBOUNCE_MS: parseEnvNumber(e, "PI_RESEARCH_TUI_REFRESH_DEBOUNCE_MS", DEFAULTS.TUI_REFRESH_DEBOUNCE_MS, 0, 1e3),
|
|
2487
2487
|
BROWSER_TASK_TIMEOUT_MS: parseEnvNumber(e, "PI_RESEARCH_BROWSER_TASK_TIMEOUT_MS", DEFAULTS.BROWSER_TASK_TIMEOUT_MS, 2e3, 12e4),
|
|
2488
|
-
LLM_TIMEOUT_MS: parseEnvNumber(e, "PI_RESEARCH_LLM_TIMEOUT_MS", DEFAULTS.LLM_TIMEOUT_MS, 6e4,
|
|
2488
|
+
LLM_TIMEOUT_MS: parseEnvNumber(e, "PI_RESEARCH_LLM_TIMEOUT_MS", DEFAULTS.LLM_TIMEOUT_MS, 6e4, 18e5),
|
|
2489
2489
|
LLM_THINKING_LEVEL: parseEnvEnum(e, "PI_RESEARCH_LLM_THINKING_LEVEL", ["off", "minimal", "low", "medium", "high"], DEFAULTS.LLM_THINKING_LEVEL),
|
|
2490
2490
|
PLANNING_MAX_TOKENS: parseEnvNumber(e, "PI_RESEARCH_PLANNING_MAX_TOKENS", DEFAULTS.PLANNING_MAX_TOKENS, 1024, 131072),
|
|
2491
2491
|
SYNTHESIS_MAX_TOKENS: parseEnvNumber(e, "PI_RESEARCH_SYNTHESIS_MAX_TOKENS", DEFAULTS.SYNTHESIS_MAX_TOKENS, 1024, 131072),
|
|
@@ -2641,9 +2641,13 @@ var init_config = __esm({
|
|
|
2641
2641
|
* never killed before it has used its full nav budget plus time waiting in the queue.
|
|
2642
2642
|
* (default: 10000) */
|
|
2643
2643
|
BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2e3, maximum: 12e4, default: 1e4 }),
|
|
2644
|
-
/** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-
|
|
2645
|
-
* Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
|
|
2646
|
-
|
|
2644
|
+
/** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
|
|
2645
|
+
* Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
|
|
2646
|
+
* Ceiling raised to 30min (matching RESEARCHER_TIMEOUT_MS): a slow model on a
|
|
2647
|
+
* large-context synthesis/plan call can legitimately run >10min, and the old
|
|
2648
|
+
* 600000 ceiling silently clamped an explicit PI_RESEARCH_LLM_TIMEOUT_MS
|
|
2649
|
+
* (e.g. 900000), causing those calls to time out mid-generation. */
|
|
2650
|
+
LLM_TIMEOUT_MS: Type.Number({ minimum: 6e4, maximum: 18e5, default: 3e5 }),
|
|
2647
2651
|
/** Chain-of-thought "thinking" level for the engine's own LLM calls (coordinator,
|
|
2648
2652
|
* evaluator, synthesis, JSON-repair, knowledge extraction) AND the researcher
|
|
2649
2653
|
* sub-agents. Default 'off': these calls emit structured JSON / cited reports, not
|
|
@@ -3201,7 +3205,8 @@ var init_agentic_repair = __esm({
|
|
|
3201
3205
|
});
|
|
3202
3206
|
|
|
3203
3207
|
// src/core/llm/model-registry-factory.ts
|
|
3204
|
-
import {
|
|
3208
|
+
import { ModelRegistry, ModelRuntime, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3209
|
+
import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
|
|
3205
3210
|
import * as path6 from "node:path";
|
|
3206
3211
|
import * as fs5 from "node:fs";
|
|
3207
3212
|
function safeGetAll(registry) {
|
|
@@ -3243,27 +3248,27 @@ function buildHeaders(provider) {
|
|
|
3243
3248
|
if (provider === "openrouter") return { "HTTP-Referer": "https://pi.ai", "X-Title": "pi-research" };
|
|
3244
3249
|
return {};
|
|
3245
3250
|
}
|
|
3246
|
-
function buildModelRegistry(apiKey, provider) {
|
|
3251
|
+
async function buildModelRegistry(apiKey, provider) {
|
|
3247
3252
|
const agentDir = getAgentDir();
|
|
3248
3253
|
const modelsJsonPath = path6.join(agentDir, "models.json");
|
|
3249
3254
|
const authPath = path6.join(agentDir, "auth.json");
|
|
3255
|
+
const hasModels = fs5.existsSync(modelsJsonPath);
|
|
3256
|
+
const hasAuth = fs5.existsSync(authPath);
|
|
3257
|
+
const runtime = await ModelRuntime.create({
|
|
3258
|
+
authPath: hasAuth ? authPath : void 0,
|
|
3259
|
+
credentials: hasAuth ? void 0 : new InMemoryCredentialStore(),
|
|
3260
|
+
modelsPath: hasModels ? modelsJsonPath : null,
|
|
3261
|
+
allowModelNetwork: false
|
|
3262
|
+
});
|
|
3250
3263
|
if (apiKey && provider) {
|
|
3251
|
-
|
|
3252
|
-
[provider]: { type: "api_key", key: apiKey }
|
|
3253
|
-
});
|
|
3254
|
-
return ModelRegistry.create(
|
|
3255
|
-
authStorage,
|
|
3256
|
-
fs5.existsSync(modelsJsonPath) ? modelsJsonPath : void 0
|
|
3257
|
-
);
|
|
3258
|
-
}
|
|
3259
|
-
if (fs5.existsSync(authPath) || fs5.existsSync(modelsJsonPath)) {
|
|
3260
|
-
const authStorage = AuthStorage.create(authPath);
|
|
3261
|
-
return ModelRegistry.create(
|
|
3262
|
-
authStorage,
|
|
3263
|
-
fs5.existsSync(modelsJsonPath) ? modelsJsonPath : void 0
|
|
3264
|
-
);
|
|
3264
|
+
await runtime.setRuntimeApiKey(provider, apiKey, { allowNetwork: false });
|
|
3265
3265
|
}
|
|
3266
|
-
|
|
3266
|
+
const registry = new ModelRegistry(runtime);
|
|
3267
|
+
registryRuntimes.set(registry, runtime);
|
|
3268
|
+
return registry;
|
|
3269
|
+
}
|
|
3270
|
+
function getRuntimeForRegistry(registry) {
|
|
3271
|
+
return registry instanceof Object ? registryRuntimes.get(registry) : void 0;
|
|
3267
3272
|
}
|
|
3268
3273
|
function constructMinimalModel(provider, modelId, _apiKey) {
|
|
3269
3274
|
return {
|
|
@@ -3355,10 +3360,12 @@ function pickPreferredAvailable(available, providerOrder) {
|
|
|
3355
3360
|
}
|
|
3356
3361
|
return available[0];
|
|
3357
3362
|
}
|
|
3363
|
+
var registryRuntimes;
|
|
3358
3364
|
var init_model_registry_factory = __esm({
|
|
3359
3365
|
"src/core/llm/model-registry-factory.ts"() {
|
|
3360
3366
|
"use strict";
|
|
3361
3367
|
init_logger();
|
|
3368
|
+
registryRuntimes = /* @__PURE__ */ new WeakMap();
|
|
3362
3369
|
}
|
|
3363
3370
|
});
|
|
3364
3371
|
|
|
@@ -14770,7 +14777,7 @@ async function performSearch(queries, config, signal, onProgress, container = ge
|
|
|
14770
14777
|
metrics.increment("browser_search_orchestrations_total", 1);
|
|
14771
14778
|
logger.log(`[Search] Orchestrating ${queries.length} queries across ${maxWorkers} worker processes...`);
|
|
14772
14779
|
if (onProgress) onProgress(0);
|
|
14773
|
-
const QUERY_TIMEOUT_MS = config?.SEARCH_TIMEOUT_MS ?? 45e3;
|
|
14780
|
+
const QUERY_TIMEOUT_MS = (config?.SEARCH_TIMEOUT_MS ?? 45e3) + (config?.BROWSER_TASK_TIMEOUT_MS ?? 1e4);
|
|
14774
14781
|
const filteredQueries = queries.filter((q) => q.trim());
|
|
14775
14782
|
let timeoutCount = 0;
|
|
14776
14783
|
let errorCount = 0;
|
|
@@ -14815,7 +14822,7 @@ async function performSearch(queries, config, signal, onProgress, container = ge
|
|
|
14815
14822
|
metrics.observe("browser_search_query_duration_ms", queryDuration, { status });
|
|
14816
14823
|
metrics.increment("browser_search_queries_total", 1, { status });
|
|
14817
14824
|
if (isTimeout) {
|
|
14818
|
-
logger.warn(`[Search] Query timed out after ${QUERY_TIMEOUT_MS}ms
|
|
14825
|
+
logger.warn(`[Search] Query timed out after ${QUERY_TIMEOUT_MS}ms \u2014 likely starved waiting for a free browser worker under concurrent load, or a slow/blocked search provider: "${query}"`);
|
|
14819
14826
|
} else {
|
|
14820
14827
|
const msg2 = error instanceof Error ? error.message : String(error);
|
|
14821
14828
|
if (msg2 !== "Aborted") {
|
|
@@ -19635,40 +19642,9 @@ function createResearchTools(options) {
|
|
|
19635
19642
|
}
|
|
19636
19643
|
|
|
19637
19644
|
// src/utils/make-resource-loader.ts
|
|
19645
|
+
import { createExtensionRuntime } from "@earendil-works/pi-coding-agent";
|
|
19638
19646
|
function makeResourceLoader(systemPromptText) {
|
|
19639
|
-
const mockRuntime =
|
|
19640
|
-
flagValues: /* @__PURE__ */ new Map(),
|
|
19641
|
-
pendingProviderRegistrations: [],
|
|
19642
|
-
registerProvider: () => {
|
|
19643
|
-
},
|
|
19644
|
-
unregisterProvider: () => {
|
|
19645
|
-
},
|
|
19646
|
-
sendMessage: async () => {
|
|
19647
|
-
},
|
|
19648
|
-
sendUserMessage: async () => {
|
|
19649
|
-
},
|
|
19650
|
-
appendEntry: async () => {
|
|
19651
|
-
},
|
|
19652
|
-
setSessionName: () => void 0,
|
|
19653
|
-
getSessionName: () => void 0,
|
|
19654
|
-
setLabel: async () => {
|
|
19655
|
-
},
|
|
19656
|
-
getActiveTools: () => [],
|
|
19657
|
-
getAllTools: () => [],
|
|
19658
|
-
setActiveTools: () => {
|
|
19659
|
-
},
|
|
19660
|
-
refreshTools: () => {
|
|
19661
|
-
},
|
|
19662
|
-
getCommands: () => [],
|
|
19663
|
-
setModel: async () => true,
|
|
19664
|
-
getThinkingLevel: () => "off",
|
|
19665
|
-
setThinkingLevel: () => {
|
|
19666
|
-
},
|
|
19667
|
-
assertActive: () => {
|
|
19668
|
-
},
|
|
19669
|
-
invalidate: () => {
|
|
19670
|
-
}
|
|
19671
|
-
};
|
|
19647
|
+
const mockRuntime = createExtensionRuntime();
|
|
19672
19648
|
return {
|
|
19673
19649
|
getExtensions: () => ({ extensions: [], errors: [], runtime: mockRuntime }),
|
|
19674
19650
|
getSkills: () => ({ skills: [], diagnostics: [] }),
|
|
@@ -19822,6 +19798,7 @@ function createDefaultToolLimits(config) {
|
|
|
19822
19798
|
// src/orchestration/researcher.ts
|
|
19823
19799
|
init_logger();
|
|
19824
19800
|
init_research_model_resolver();
|
|
19801
|
+
init_model_registry_factory();
|
|
19825
19802
|
async function createResearcherSession(options) {
|
|
19826
19803
|
const {
|
|
19827
19804
|
cwd,
|
|
@@ -19889,7 +19866,17 @@ async function createResearcherSession(options) {
|
|
|
19889
19866
|
// Each researcher gets its own isolated session
|
|
19890
19867
|
settingsManager: researcherSettings,
|
|
19891
19868
|
model: modelToUse,
|
|
19892
|
-
modelRegistry
|
|
19869
|
+
// pi 0.80.8 replaced the SDK's `modelRegistry`/`authStorage` session options
|
|
19870
|
+
// with the async `modelRuntime`. Pass the runtime backing OUR registry when
|
|
19871
|
+
// this run built one (standalone CLI/SDK): it carries any explicit API key
|
|
19872
|
+
// seeded via setRuntimeApiKey, which the session's own default runtime
|
|
19873
|
+
// (auto-built from ~/.pi/agent/{auth,models}.json) would never see — without
|
|
19874
|
+
// this, standalone PI_RESEARCH_API_KEY users fail auth on every researcher
|
|
19875
|
+
// LLM call despite passing pre-flight. For registries we did NOT build
|
|
19876
|
+
// (the pi host's ExtensionContext.modelRegistry), this resolves to
|
|
19877
|
+
// undefined and the host auto-builds from its own agent dir — correct
|
|
19878
|
+
// in-host, where auth lives in the host's config.
|
|
19879
|
+
modelRuntime: getRuntimeForRegistry(modelRegistry),
|
|
19893
19880
|
resourceLoader: makeResourceLoader(systemPrompt),
|
|
19894
19881
|
// Thinking/reasoning is disabled for researcher agents: they do retrieval and
|
|
19895
19882
|
// synthesis from scraped pages, not open-ended reasoning. Keeping it off reduces
|
|
@@ -22332,7 +22319,7 @@ async function _doInit(options = {}) {
|
|
|
22332
22319
|
if (globalApiKey && !parsedProvider) {
|
|
22333
22320
|
throw new Error("Provider must be specified when using an explicit API key (set provider option or PI_RESEARCH_PROVIDER).");
|
|
22334
22321
|
}
|
|
22335
|
-
globalRegistry = buildModelRegistry(globalApiKey, parsedProvider);
|
|
22322
|
+
globalRegistry = await buildModelRegistry(globalApiKey, parsedProvider);
|
|
22336
22323
|
try {
|
|
22337
22324
|
if (effectiveModel && typeof effectiveModel === "object" && effectiveModel.id) {
|
|
22338
22325
|
globalModel = effectiveModel;
|
|
@@ -22940,16 +22927,16 @@ function fileExists(p) {
|
|
|
22940
22927
|
return false;
|
|
22941
22928
|
}
|
|
22942
22929
|
}
|
|
22943
|
-
function piKeysAvailable(piAuthPresent, provider) {
|
|
22930
|
+
async function piKeysAvailable(piAuthPresent, provider) {
|
|
22944
22931
|
try {
|
|
22945
|
-
const available = safeGetAvailable(buildModelRegistry(void 0, void 0));
|
|
22932
|
+
const available = safeGetAvailable(await buildModelRegistry(void 0, void 0));
|
|
22946
22933
|
if (!provider) return available.length > 0;
|
|
22947
22934
|
return available.some((m) => m.provider === provider);
|
|
22948
22935
|
} catch {
|
|
22949
22936
|
return piAuthPresent;
|
|
22950
22937
|
}
|
|
22951
22938
|
}
|
|
22952
|
-
function detectCredentials(explicitModel) {
|
|
22939
|
+
async function detectCredentials(explicitModel) {
|
|
22953
22940
|
const paths = resolvedConfigPaths();
|
|
22954
22941
|
const piAuthPresent = fileExists(paths.piAuth);
|
|
22955
22942
|
const piModelsPresent = fileExists(paths.piModels);
|
|
@@ -22960,7 +22947,7 @@ function detectCredentials(explicitModel) {
|
|
|
22960
22947
|
const model = explicitModel ?? modelEnv ?? modelConfig;
|
|
22961
22948
|
const modelFrom = explicitModel ? "--model" : modelEnv ? "PI_RESEARCH_MODEL" : modelConfig ? "config.env" : "unset";
|
|
22962
22949
|
const provider = providerEnv ?? (model && model.includes("/") ? model.slice(0, model.indexOf("/")) : void 0);
|
|
22963
|
-
const piKeysUsable = piKeysAvailable(piAuthPresent, provider);
|
|
22950
|
+
const piKeysUsable = await piKeysAvailable(piAuthPresent, provider);
|
|
22964
22951
|
if (apiKey) {
|
|
22965
22952
|
if (!provider) {
|
|
22966
22953
|
return {
|
|
@@ -23052,7 +23039,7 @@ function configBlock(det, extraNote) {
|
|
|
23052
23039
|
return lines.join("\n");
|
|
23053
23040
|
}
|
|
23054
23041
|
async function cmdResearch(args) {
|
|
23055
|
-
const det = detectCredentials(args.model);
|
|
23042
|
+
const det = await detectCredentials(args.model);
|
|
23056
23043
|
if (det.problem) {
|
|
23057
23044
|
if (args.json) toStdout(pretty({ ok: false, error: det.problem, exitCode: EXIT.CONFIG }));
|
|
23058
23045
|
else toStderr(`
|
|
@@ -23125,7 +23112,7 @@ Error: ${vmsg}
|
|
|
23125
23112
|
toStdout(report.endsWith("\n") ? report : report + "\n");
|
|
23126
23113
|
}
|
|
23127
23114
|
} catch (err) {
|
|
23128
|
-
exit = reportError(err, "research", args.json);
|
|
23115
|
+
exit = await reportError(err, "research", args.json);
|
|
23129
23116
|
} finally {
|
|
23130
23117
|
if (activeResearchAbortController === abortController) activeResearchAbortController = null;
|
|
23131
23118
|
await safeShutdown();
|
|
@@ -23133,7 +23120,7 @@ Error: ${vmsg}
|
|
|
23133
23120
|
return exit;
|
|
23134
23121
|
}
|
|
23135
23122
|
async function cmdKnowledge(queries, json) {
|
|
23136
|
-
const det = detectCredentials();
|
|
23123
|
+
const det = await detectCredentials();
|
|
23137
23124
|
if (det.problem) {
|
|
23138
23125
|
if (json) {
|
|
23139
23126
|
toStdout(pretty({ ok: false, error: det.problem, exitCode: EXIT.CONFIG }));
|
|
@@ -23187,7 +23174,7 @@ Configure in:
|
|
|
23187
23174
|
return exit;
|
|
23188
23175
|
}
|
|
23189
23176
|
async function cmdStatus(json) {
|
|
23190
|
-
const det = detectCredentials();
|
|
23177
|
+
const det = await detectCredentials();
|
|
23191
23178
|
const paths = resolvedConfigPaths();
|
|
23192
23179
|
const cfg = getConfig(process.cwd(), "cli");
|
|
23193
23180
|
const summary = {
|
|
@@ -23401,7 +23388,7 @@ function describeUninstall(r) {
|
|
|
23401
23388
|
return r.status;
|
|
23402
23389
|
}
|
|
23403
23390
|
}
|
|
23404
|
-
function reportError(err, what, json) {
|
|
23391
|
+
async function reportError(err, what, json) {
|
|
23405
23392
|
const msg2 = err instanceof Error ? err.message : String(err);
|
|
23406
23393
|
const lower = msg2.toLowerCase();
|
|
23407
23394
|
const isTaggedResearchStop = err?.code === RESEARCH_STOPPED_ERROR_CODE;
|
|
@@ -23427,7 +23414,7 @@ Error: pi-research ${what} halted: provider rate limit reached. Wait a moment an
|
|
|
23427
23414
|
toStderr(`
|
|
23428
23415
|
Error: pi-research ${what} failed: ${msg2}
|
|
23429
23416
|
|
|
23430
|
-
${configBlock(detectCredentials())}
|
|
23417
|
+
${configBlock(await detectCredentials())}
|
|
23431
23418
|
`);
|
|
23432
23419
|
return exitCode;
|
|
23433
23420
|
}
|
package/dist/thread-worker.mjs
CHANGED
|
@@ -2278,9 +2278,13 @@ var ConfigSchema = Type.Object({
|
|
|
2278
2278
|
* never killed before it has used its full nav budget plus time waiting in the queue.
|
|
2279
2279
|
* (default: 10000) */
|
|
2280
2280
|
BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2e3, maximum: 12e4, default: 1e4 }),
|
|
2281
|
-
/** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-
|
|
2282
|
-
* Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
|
|
2283
|
-
|
|
2281
|
+
/** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
|
|
2282
|
+
* Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
|
|
2283
|
+
* Ceiling raised to 30min (matching RESEARCHER_TIMEOUT_MS): a slow model on a
|
|
2284
|
+
* large-context synthesis/plan call can legitimately run >10min, and the old
|
|
2285
|
+
* 600000 ceiling silently clamped an explicit PI_RESEARCH_LLM_TIMEOUT_MS
|
|
2286
|
+
* (e.g. 900000), causing those calls to time out mid-generation. */
|
|
2287
|
+
LLM_TIMEOUT_MS: Type.Number({ minimum: 6e4, maximum: 18e5, default: 3e5 }),
|
|
2284
2288
|
/** Chain-of-thought "thinking" level for the engine's own LLM calls (coordinator,
|
|
2285
2289
|
* evaluator, synthesis, JSON-repair, knowledge extraction) AND the researcher
|
|
2286
2290
|
* sub-agents. Default 'off': these calls emit structured JSON / cited reports, not
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,28 @@ 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.0.
|
|
8
|
+
## [1.0.9] - 2026-07-28
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **pi ≥ 0.81.0 researcher crash** — every researcher session died with `pendingNativeProviderRegistrations is not iterable` under pi 0.81.0+ ("no source material was collected"). The hand-maintained `ExtensionRuntime` mock in the resource loader was frozen at a pre-0.81 contract; it is replaced with pi's own `createExtensionRuntime()`, which always satisfies the running host's contract and cannot drift again.
|
|
12
|
+
- **pi 0.80.8+ ModelRuntime migration** — `buildModelRegistry` migrated off the removed `AuthStorage`/`ModelRegistry.create()` APIs to `ModelRuntime.create()` + the synchronous `ModelRegistry` facade. The `@earendil-works/*` dependency floor and the in-host version self-check are both raised to 0.80.8 to match.
|
|
13
|
+
- **Explicit-API-key auth now reaches researcher LLM calls** — researcher sessions are created with the `modelRuntime` backing our own registry (pi 0.80.8 removed the old `modelRegistry` session option). Without this, standalone `PI_RESEARCH_API_KEY` users with no `~/.pi/agent/auth.json` passed pre-flight and then failed auth on every researcher call.
|
|
14
|
+
- **Explicit-API-key pre-flight hang** — seeding a runtime API key triggered a model-catalog refresh whose default options open live network connections with no timeout; in network-restricted environments the CLI pre-flight hung indefinitely. The refresh is now pinned offline (`allowNetwork: false`).
|
|
15
|
+
- **No filesystem side effects on read-only commands** — the migrated runtime's default file-backed credential store created `~/.pi/agent/auth.json` at construction, so every CLI invocation (including `status`) silently created a pi config dir on machines without pi and hard-failed where `$HOME` is read-only. An in-memory credential store is used when no `auth.json` exists; env-var provider keys are unaffected.
|
|
16
|
+
- **Search-timeout cascade under concurrent load** — the per-query hard cap in browser search started at enqueue with only the nav budget, so a query stuck in a saturated worker queue burned its whole budget waiting and aborted with zero work ("Query timed out … likely blocked or slow startup"). The cap now includes the queue-wait margin, matching the scheduler's own ceiling.
|
|
17
|
+
- **`PI_RESEARCH_LLM_TIMEOUT_MS` ceiling raised 10 → 30 min** — an explicit higher value was silently clamped to 600000, timing out large-context synthesis calls mid-generation.
|
|
18
|
+
- **CHANGELOG dates corrected** — release dates in this file wrongly said 2024; corrected to the actual git release dates (2026).
|
|
19
|
+
|
|
20
|
+
### Security
|
|
21
|
+
- **protobufjs override 7.6.4 → 7.6.5** — clears GHSA-j3f2-48v5-ccww (DoS) and its whole moderate advisory chain through `@google/genai`/pi-ai.
|
|
22
|
+
- **adm-zip forced to 0.6.0 via override** — clears GHSA-xcpc-8h2w-3j85 (crafted-ZIP 4 GB allocation) in the camoufox-js subtree; extraction APIs camoufox uses verified compatible.
|
|
23
|
+
- **Audit gate hardened with a reviewed exception list** — CI and the release publish gate now run `scripts/audit-gate.cjs`: any moderate+ advisory in the shipped tree fails the build unless explicitly allowlisted in `config/tooling/audit-exceptions.json` with an upstream-blocked justification, a clears-when condition, and a review date. Current exceptions: `brace-expansion` (frozen inside pi-coding-agent's npm-shrinkwrap; fix released upstream) and `sharp` (unused image pipeline of the embeddings-only transformers dependency).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- **Dependencies**: pi ecosystem lockfile refreshed to 0.82.1 (matching the current pi host); `pdf-oxide-wasm` 0.3.77; `undici` ^8.9; devDependencies refreshed (eslint 10.8, typescript-eslint 8.65, vitest 4.1.10, dependency-cruiser 17.4.3). Deliberate pins unchanged (typebox 1.1.38, playwright-core 1.60.0, camoufox-js 0.10.x, lancedb 0.29, apache-arrow 21.1.0, transformers 4.2, impit 0.13).
|
|
27
|
+
- **Docs**: `PI_RESEARCH_LLM_TIMEOUT_MS` range updated in CONFIGURATION.md.
|
|
28
|
+
|
|
29
|
+
## [1.0.8] - 2026-07-17
|
|
9
30
|
|
|
10
31
|
### Added
|
|
11
32
|
- **CHANGELOG** — Added comprehensive CHANGELOG.md documenting all versions from 0.1.13 through 1.0.7
|
|
@@ -15,7 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
36
|
- **Topic consolidation guidance** — Emphasized that topics can likely be consolidated to fit the 4-call maximum, with strategies for grouping related subtopics
|
|
16
37
|
- **Package structure** — CHANGELOG.md now included in published package (docs/CHANGELOG.md)
|
|
17
38
|
|
|
18
|
-
## [1.0.7] -
|
|
39
|
+
## [1.0.7] - 2026-07-14
|
|
19
40
|
|
|
20
41
|
### Fixed
|
|
21
42
|
- **Browser-pool leader-election race** - Fixed a critical bug where a single client's transient socket error could force-clear shared leader state, causing a live, healthy leader to self-destruct via the leadership-miss watchdog. The fix reserves the "unreachable, force clear" path for cases where liveness probes actually confirm the registered leader PID or port is gone.
|
|
@@ -34,7 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
34
55
|
- Socket drain on Content-Length pre-screen throws
|
|
35
56
|
- Observer isolation improvements
|
|
36
57
|
|
|
37
|
-
## [1.0.6] -
|
|
58
|
+
## [1.0.6] - 2026-07-04
|
|
38
59
|
|
|
39
60
|
### Fixed
|
|
40
61
|
- **Observer isolation** - Fixed `makeSafeObserver` proxying the observer directly, which could cause state leakage.
|
|
@@ -43,7 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
43
64
|
- Corrected depth-0 availability documentation
|
|
44
65
|
- Fixed migration.ts attribution in architecture docs
|
|
45
66
|
|
|
46
|
-
## [1.0.5] -
|
|
67
|
+
## [1.0.5] - 2026-07-04
|
|
47
68
|
|
|
48
69
|
### Fixed
|
|
49
70
|
- **Content-Length pre-screen throws** - Added proper body draining to prevent resource leaks
|
|
@@ -52,13 +73,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
52
73
|
### Fixed
|
|
53
74
|
- **YouTube transcript provenance** - Fixed transcript provenance tracking in final report citations
|
|
54
75
|
|
|
55
|
-
## [1.0.4] -
|
|
76
|
+
## [1.0.4] - 2026-07-04
|
|
56
77
|
|
|
57
78
|
### Fixed
|
|
58
79
|
- **Credential detection** - Detect pi keys by pi's own semantics, not file presence; registry no longer gated on auth.json
|
|
59
80
|
- **Configuration clarity** - Updated docs to reflect that pi has configuration files, not a named 'auth' feature
|
|
60
81
|
|
|
61
|
-
## [1.0.3] -
|
|
82
|
+
## [1.0.3] - 2026-07-04
|
|
62
83
|
|
|
63
84
|
### Fixed
|
|
64
85
|
- **Model resolution** - Require a configured model on the standalone CLI/skill; align SDK session model with RESEARCH_MODEL
|
|
@@ -67,19 +88,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
67
88
|
### Documentation
|
|
68
89
|
- Updated installation and configuration documentation
|
|
69
90
|
|
|
70
|
-
## [1.0.2] -
|
|
91
|
+
## [1.0.2] - 2026-07-04
|
|
71
92
|
|
|
72
93
|
### Fixed
|
|
73
94
|
- **Timeout robustness** - Improved timeout handling for long-running operations
|
|
74
95
|
- De-emphasized the markdown-conversion fallback in favor of native HTML-to-markdown conversion
|
|
75
96
|
|
|
76
|
-
## [1.0.1] -
|
|
97
|
+
## [1.0.1] - 2026-07-04
|
|
77
98
|
|
|
78
99
|
### Fixed
|
|
79
100
|
- Initial stable release after rebuild against current pi APIs
|
|
80
101
|
- Stealth-browser stack integration
|
|
81
102
|
|
|
82
|
-
## [0.1.13] -
|
|
103
|
+
## [0.1.13] - 2026-04-15
|
|
83
104
|
|
|
84
105
|
### Added
|
|
85
106
|
- Initial research functionality
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -97,7 +97,7 @@ Timeouts
|
|
|
97
97
|
|
|
98
98
|
| Variable | Default | Range | Description |
|
|
99
99
|
|----------|---------|-------|-------------|
|
|
100
|
-
| `PI_RESEARCH_LLM_TIMEOUT_MS` | `300000` | 60000–
|
|
100
|
+
| `PI_RESEARCH_LLM_TIMEOUT_MS` | `300000` | 60000–1800000 | Coordinator / evaluator / repair / knowledge LLM call timeout. |
|
|
101
101
|
| `PI_RESEARCH_SCRAPE_TIMEOUT_MS` | `15000` | 5000–120000 | Per-page scrape (page-load) timeout. |
|
|
102
102
|
| `PI_RESEARCH_SEARCH_TIMEOUT_MS` | `45000` | 5000–120000 | Browser search page timeout. |
|
|
103
103
|
| `PI_RESEARCH_BROWSER_TASK_TIMEOUT_MS` | `10000` | 2000–120000 | Queue-wait / 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). |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lincoln504/pi-research",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "A set of web research tools (search, scrape, and others) and research agent system that uses them safely and efficiently.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -110,9 +110,9 @@
|
|
|
110
110
|
"image": "https://raw.githubusercontent.com/Lincoln504/pi-research/main/docs/media/hero.png"
|
|
111
111
|
},
|
|
112
112
|
"dependencies": {
|
|
113
|
-
"@earendil-works/pi-ai": ">=0.80.
|
|
114
|
-
"@earendil-works/pi-coding-agent": ">=0.80.
|
|
115
|
-
"@earendil-works/pi-tui": ">=0.80.
|
|
113
|
+
"@earendil-works/pi-ai": ">=0.80.8 <1",
|
|
114
|
+
"@earendil-works/pi-coding-agent": ">=0.80.8 <1",
|
|
115
|
+
"@earendil-works/pi-tui": ">=0.80.8 <1",
|
|
116
116
|
"@huggingface/transformers": "4.2.0",
|
|
117
117
|
"@kreuzberg/html-to-markdown-node": "^3.5.5",
|
|
118
118
|
"@lancedb/lancedb": "0.29.0",
|
|
@@ -123,11 +123,11 @@
|
|
|
123
123
|
"impit": "0.13.0",
|
|
124
124
|
"jsdom": "^29.1.1",
|
|
125
125
|
"node-html-markdown": "^2.0.0",
|
|
126
|
-
"pdf-oxide-wasm": "^0.3.
|
|
126
|
+
"pdf-oxide-wasm": "^0.3.75",
|
|
127
127
|
"playwright-core": "1.60.0",
|
|
128
128
|
"poolifier": "^5.3.2",
|
|
129
129
|
"typebox": "1.1.38",
|
|
130
|
-
"undici": "^8.
|
|
130
|
+
"undici": "^8.9.0",
|
|
131
131
|
"youtubei.js": "^17.2.0"
|
|
132
132
|
},
|
|
133
133
|
"optionalDependencies": {
|
|
@@ -140,20 +140,21 @@
|
|
|
140
140
|
"onnxruntime-web": "1.24.3",
|
|
141
141
|
"node-domexception": "npm:@nolyfill/domexception@^1.0.28",
|
|
142
142
|
"ua-parser-js": "^1.0.41",
|
|
143
|
-
"protobufjs": "7.6.
|
|
144
|
-
"vite": "^8.0.16"
|
|
143
|
+
"protobufjs": "7.6.5",
|
|
144
|
+
"vite": "^8.0.16",
|
|
145
|
+
"adm-zip": "^0.6.0"
|
|
145
146
|
},
|
|
146
147
|
"devDependencies": {
|
|
147
148
|
"@eslint/js": "^10.0.1",
|
|
148
149
|
"@types/jsdom": "^28.0.3",
|
|
149
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
150
|
-
"@typescript-eslint/parser": "^8.
|
|
151
|
-
"@vitest/coverage-v8": "^4.1.
|
|
152
|
-
"dependency-cruiser": "^17.4.
|
|
150
|
+
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
|
151
|
+
"@typescript-eslint/parser": "^8.65.0",
|
|
152
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
153
|
+
"dependency-cruiser": "^17.4.3",
|
|
153
154
|
"depngn": "^0.6.0",
|
|
154
|
-
"eslint": "^10.
|
|
155
|
-
"npm-check-updates": "^22.2.
|
|
155
|
+
"eslint": "^10.8.0",
|
|
156
|
+
"npm-check-updates": "^22.2.9",
|
|
156
157
|
"typescript": ">=6.0.3 <6.1.0",
|
|
157
|
-
"vitest": "^4.1.
|
|
158
|
+
"vitest": "^4.1.10"
|
|
158
159
|
}
|
|
159
160
|
}
|
|
@@ -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.9", "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
|
@@ -240,9 +240,9 @@ function fileExists(p: string): boolean {
|
|
|
240
240
|
* OPENAI_API_KEY. File presence alone is neither necessary (keys can live in
|
|
241
241
|
* models.json / env with no auth.json) nor sufficient (auth.json can be `{}`).
|
|
242
242
|
*/
|
|
243
|
-
function piKeysAvailable(piAuthPresent: boolean, provider?: string): boolean {
|
|
243
|
+
async function piKeysAvailable(piAuthPresent: boolean, provider?: string): Promise<boolean> {
|
|
244
244
|
try {
|
|
245
|
-
const available = safeGetAvailable(buildModelRegistry(undefined, undefined));
|
|
245
|
+
const available = safeGetAvailable(await buildModelRegistry(undefined, undefined));
|
|
246
246
|
if (!provider) return available.length > 0;
|
|
247
247
|
// Provider known (from the configured provider/model-id): keys must exist
|
|
248
248
|
// for THAT provider — a key for some other provider would pass pre-flight
|
|
@@ -282,7 +282,7 @@ export interface CredentialDetection {
|
|
|
282
282
|
* blocks a valid `--model` run). An explicit PI_RESEARCH_PROVIDER still wins:
|
|
283
283
|
* it names the provider the configured key belongs to.
|
|
284
284
|
*/
|
|
285
|
-
function detectCredentials(explicitModel?: string): CredentialDetection {
|
|
285
|
+
async function detectCredentials(explicitModel?: string): Promise<CredentialDetection> {
|
|
286
286
|
const paths = resolvedConfigPaths();
|
|
287
287
|
const piAuthPresent = fileExists(paths.piAuth);
|
|
288
288
|
const piModelsPresent = fileExists(paths.piModels);
|
|
@@ -299,7 +299,7 @@ function detectCredentials(explicitModel?: string): CredentialDetection {
|
|
|
299
299
|
providerEnv ?? (model && model.includes('/') ? model.slice(0, model.indexOf('/')) : undefined);
|
|
300
300
|
// Provider-aware when the configured model names one: a key for a DIFFERENT
|
|
301
301
|
// provider must not greenlight this run.
|
|
302
|
-
const piKeysUsable = piKeysAvailable(piAuthPresent, provider);
|
|
302
|
+
const piKeysUsable = await piKeysAvailable(piAuthPresent, provider);
|
|
303
303
|
|
|
304
304
|
// --- Explicit API-key path -------------------------------------------------
|
|
305
305
|
if (apiKey) {
|
|
@@ -440,7 +440,7 @@ interface ResearchArgs {
|
|
|
440
440
|
async function cmdResearch(args: ResearchArgs): Promise<number> {
|
|
441
441
|
// Pre-flight with the per-run --model flag folded in: it satisfies the
|
|
442
442
|
// model-required check and steers the provider-aware key detection.
|
|
443
|
-
const det = detectCredentials(args.model);
|
|
443
|
+
const det = await detectCredentials(args.model);
|
|
444
444
|
if (det.problem) {
|
|
445
445
|
if (args.json) toStdout(pretty({ ok: false, error: det.problem, exitCode: EXIT.CONFIG }));
|
|
446
446
|
else toStderr(`\nError: ${det.problem}\n\n${configBlock(det)}\n`);
|
|
@@ -531,7 +531,7 @@ async function cmdResearch(args: ResearchArgs): Promise<number> {
|
|
|
531
531
|
toStdout(report.endsWith('\n') ? report : report + '\n');
|
|
532
532
|
}
|
|
533
533
|
} catch (err) {
|
|
534
|
-
exit = reportError(err, 'research', args.json);
|
|
534
|
+
exit = await reportError(err, 'research', args.json);
|
|
535
535
|
} finally {
|
|
536
536
|
if (activeResearchAbortController === abortController) activeResearchAbortController = null;
|
|
537
537
|
await safeShutdown();
|
|
@@ -545,7 +545,7 @@ async function cmdResearch(args: ResearchArgs): Promise<number> {
|
|
|
545
545
|
* research.
|
|
546
546
|
*/
|
|
547
547
|
async function cmdKnowledge(queries: string[], json?: boolean): Promise<number> {
|
|
548
|
-
const det = detectCredentials();
|
|
548
|
+
const det = await detectCredentials();
|
|
549
549
|
if (det.problem) {
|
|
550
550
|
if (json) { toStdout(pretty({ ok: false, error: det.problem, exitCode: EXIT.CONFIG })); return EXIT.CONFIG; }
|
|
551
551
|
toStderr(`\nError: ${det.problem}\n\n${configBlock(det)}\n`);
|
|
@@ -595,7 +595,7 @@ async function cmdKnowledge(queries: string[], json?: boolean): Promise<number>
|
|
|
595
595
|
* run, without initializing the SDK. Machine-readable with --json.
|
|
596
596
|
*/
|
|
597
597
|
async function cmdStatus(json?: boolean): Promise<number> {
|
|
598
|
-
const det = detectCredentials();
|
|
598
|
+
const det = await detectCredentials();
|
|
599
599
|
const paths = resolvedConfigPaths();
|
|
600
600
|
const cfg = getConfig(process.cwd(), 'cli');
|
|
601
601
|
const summary = {
|
|
@@ -815,7 +815,7 @@ function describeUninstall(r: { status: string; path: string; message?: string }
|
|
|
815
815
|
// ---------------------------------------------------------------------------
|
|
816
816
|
|
|
817
817
|
/** Distinguish setup errors from runtime errors and print a clean, located message. */
|
|
818
|
-
export function reportError(err: unknown, what: string, json?: boolean): number {
|
|
818
|
+
export async function reportError(err: unknown, what: string, json?: boolean): Promise<number> {
|
|
819
819
|
const msg = err instanceof Error ? err.message : String(err);
|
|
820
820
|
const lower = msg.toLowerCase();
|
|
821
821
|
|
|
@@ -865,7 +865,7 @@ export function reportError(err: unknown, what: string, json?: boolean): number
|
|
|
865
865
|
return exitCode;
|
|
866
866
|
}
|
|
867
867
|
if (isConfigError) {
|
|
868
|
-
toStderr(`\nError: pi-research ${what} failed: ${msg}\n\n${configBlock(detectCredentials())}\n`);
|
|
868
|
+
toStderr(`\nError: pi-research ${what} failed: ${msg}\n\n${configBlock(await detectCredentials())}\n`);
|
|
869
869
|
return exitCode;
|
|
870
870
|
}
|
|
871
871
|
toStderr(`\nError: pi-research ${what} failed: ${msg}\n`);
|
package/src/config.ts
CHANGED
|
@@ -87,9 +87,13 @@ export const ConfigSchema = Type.Object({
|
|
|
87
87
|
* never killed before it has used its full nav budget plus time waiting in the queue.
|
|
88
88
|
* (default: 10000) */
|
|
89
89
|
BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2000, maximum: 120000, default: 10000 }),
|
|
90
|
-
/** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-
|
|
91
|
-
* Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
|
|
92
|
-
|
|
90
|
+
/** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
|
|
91
|
+
* Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
|
|
92
|
+
* Ceiling raised to 30min (matching RESEARCHER_TIMEOUT_MS): a slow model on a
|
|
93
|
+
* large-context synthesis/plan call can legitimately run >10min, and the old
|
|
94
|
+
* 600000 ceiling silently clamped an explicit PI_RESEARCH_LLM_TIMEOUT_MS
|
|
95
|
+
* (e.g. 900000), causing those calls to time out mid-generation. */
|
|
96
|
+
LLM_TIMEOUT_MS: Type.Number({ minimum: 60000, maximum: 1800000, default: 300000 }),
|
|
93
97
|
/** Chain-of-thought "thinking" level for the engine's own LLM calls (coordinator,
|
|
94
98
|
* evaluator, synthesis, JSON-repair, knowledge extraction) AND the researcher
|
|
95
99
|
* sub-agents. Default 'off': these calls emit structured JSON / cited reports, not
|
|
@@ -945,7 +949,7 @@ export function createConfig(env: Record<string, string | undefined>, processEnv
|
|
|
945
949
|
SEARCH_TIMEOUT_MS: parseEnvNumber(e, 'PI_RESEARCH_SEARCH_TIMEOUT_MS', DEFAULTS.SEARCH_TIMEOUT_MS, 5000, 120000),
|
|
946
950
|
TUI_REFRESH_DEBOUNCE_MS: parseEnvNumber(e, 'PI_RESEARCH_TUI_REFRESH_DEBOUNCE_MS', DEFAULTS.TUI_REFRESH_DEBOUNCE_MS, 0, 1000),
|
|
947
951
|
BROWSER_TASK_TIMEOUT_MS: parseEnvNumber(e, 'PI_RESEARCH_BROWSER_TASK_TIMEOUT_MS', DEFAULTS.BROWSER_TASK_TIMEOUT_MS, 2000, 120000),
|
|
948
|
-
LLM_TIMEOUT_MS: parseEnvNumber(e, 'PI_RESEARCH_LLM_TIMEOUT_MS', DEFAULTS.LLM_TIMEOUT_MS, 60000,
|
|
952
|
+
LLM_TIMEOUT_MS: parseEnvNumber(e, 'PI_RESEARCH_LLM_TIMEOUT_MS', DEFAULTS.LLM_TIMEOUT_MS, 60000, 1800000),
|
|
949
953
|
LLM_THINKING_LEVEL: parseEnvEnum(e, 'PI_RESEARCH_LLM_THINKING_LEVEL', ['off', 'minimal', 'low', 'medium', 'high'] as const, DEFAULTS.LLM_THINKING_LEVEL),
|
|
950
954
|
PLANNING_MAX_TOKENS: parseEnvNumber(e, 'PI_RESEARCH_PLANNING_MAX_TOKENS', DEFAULTS.PLANNING_MAX_TOKENS, 1024, 131072),
|
|
951
955
|
SYNTHESIS_MAX_TOKENS: parseEnvNumber(e, 'PI_RESEARCH_SYNTHESIS_MAX_TOKENS', DEFAULTS.SYNTHESIS_MAX_TOKENS, 1024, 131072),
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* Previously this logic was duplicated (#29) — any bug fix now updates both paths.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { ModelRegistry, ModelRuntime, getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
12
|
+
import { InMemoryCredentialStore } from '@earendil-works/pi-ai';
|
|
12
13
|
import type { Model } from '@earendil-works/pi-ai';
|
|
13
14
|
import * as path from 'node:path';
|
|
14
15
|
import * as fs from 'node:fs';
|
|
@@ -103,42 +104,93 @@ function buildHeaders(provider?: string): Record<string, string> {
|
|
|
103
104
|
/**
|
|
104
105
|
* Create a ModelRegistry with the given credentials.
|
|
105
106
|
*
|
|
107
|
+
* pi 0.80.8 replaced `AuthStorage`/`ModelRegistry.create()` with the async
|
|
108
|
+
* `ModelRuntime`: `ModelRegistry` is now a SYNCHRONOUS compatibility facade that
|
|
109
|
+
* wraps a `ModelRuntime` and still exposes `getAll`/`getAvailable`/`find`/
|
|
110
|
+
* `getApiKeyAndHeaders` — so every consumer of the registry (resolveModel,
|
|
111
|
+
* safeGet*, the research-model resolver) stays synchronous. Only this factory
|
|
112
|
+
* became async (constructing the ModelRuntime is inherently async) and its two
|
|
113
|
+
* callers await it. Mirrors how pi itself builds a registry
|
|
114
|
+
* (`sdk.js`: `await ModelRuntime.create({ authPath, modelsPath })`).
|
|
115
|
+
*
|
|
106
116
|
* @param apiKey - Optional explicit API key
|
|
107
117
|
* @param provider - Optional provider name to key the API key under
|
|
108
118
|
* @returns A configured ModelRegistry
|
|
109
119
|
*/
|
|
110
|
-
export function buildModelRegistry(apiKey?: string, provider?: string): ModelRegistry {
|
|
120
|
+
export async function buildModelRegistry(apiKey?: string, provider?: string): Promise<ModelRegistry> {
|
|
111
121
|
const agentDir = getAgentDir();
|
|
112
122
|
const modelsJsonPath = path.join(agentDir, 'models.json');
|
|
113
123
|
const authPath = path.join(agentDir, 'auth.json');
|
|
124
|
+
const hasModels = fs.existsSync(modelsJsonPath);
|
|
125
|
+
const hasAuth = fs.existsSync(authPath);
|
|
126
|
+
|
|
127
|
+
// Build the runtime from the user's pi config (models.json + auth.json) when
|
|
128
|
+
// EITHER file is present, otherwise a bare runtime that reads env-var keys and
|
|
129
|
+
// built-in providers. allowModelNetwork:false preserves the prior behavior of
|
|
130
|
+
// NOT doing a network model-catalog refresh at construction (the old
|
|
131
|
+
// ModelRegistry.create only read the local files). Env provider keys
|
|
132
|
+
// (e.g. OPENAI_API_KEY) are resolved via pi-ai's provider auth CONTEXT, not the
|
|
133
|
+
// credential store, so they are honored in every branch below; embedded
|
|
134
|
+
// models.json apiKeys likewise — auth.json presence must not gate models.json.
|
|
135
|
+
//
|
|
136
|
+
// When auth.json does NOT exist, an explicit in-memory credential store is
|
|
137
|
+
// passed instead of letting ModelRuntime default to the file at authPath:
|
|
138
|
+
// AuthStorage's file backend CREATES ~/.pi/agent/ and an empty auth.json as a
|
|
139
|
+
// construction side effect (mkdir+write). Without this, every CLI invocation —
|
|
140
|
+
// including read-only ones like `status` — would silently create a pi config
|
|
141
|
+
// dir on machines that never installed pi, and would hard-fail in read-only-
|
|
142
|
+
// $HOME environments (containers/CI) where the old in-memory path needed no
|
|
143
|
+
// filesystem write access at all.
|
|
144
|
+
const runtime = await ModelRuntime.create({
|
|
145
|
+
authPath: hasAuth ? authPath : undefined,
|
|
146
|
+
credentials: hasAuth ? undefined : new InMemoryCredentialStore(),
|
|
147
|
+
modelsPath: hasModels ? modelsJsonPath : null,
|
|
148
|
+
allowModelNetwork: false,
|
|
149
|
+
});
|
|
114
150
|
|
|
151
|
+
// Explicit key: seed it on the runtime after construction. NOTE: unlike the old
|
|
152
|
+
// AuthStorage.inMemory({ [provider]: ... }) seeding, this MERGES — the explicit
|
|
153
|
+
// key wins for its own provider (runtime overrides are checked first), but any
|
|
154
|
+
// providers already configured in auth.json remain visible in getAvailable().
|
|
155
|
+
// That broadening is acceptable because explicit-key flows always pair the key
|
|
156
|
+
// with an explicit model (enforced in cli.ts pre-flight), so resolution never
|
|
157
|
+
// falls back to "first available".
|
|
115
158
|
if (apiKey && provider) {
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
fs.existsSync(modelsJsonPath) ? modelsJsonPath : undefined,
|
|
123
|
-
);
|
|
159
|
+
// allowNetwork:false is REQUIRED here, not just consistent with create():
|
|
160
|
+
// setRuntimeApiKey triggers a model-catalog refresh for the newly configured
|
|
161
|
+
// provider, and its default options open live catalog connections — verified
|
|
162
|
+
// to hang indefinitely (no timeout at this layer) in network-restricted
|
|
163
|
+
// environments, which would wedge every explicit-API-key pre-flight.
|
|
164
|
+
await runtime.setRuntimeApiKey(provider, apiKey, { allowNetwork: false });
|
|
124
165
|
}
|
|
125
166
|
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const authStorage = AuthStorage.create(authPath);
|
|
134
|
-
return ModelRegistry.create(
|
|
135
|
-
authStorage,
|
|
136
|
-
fs.existsSync(modelsJsonPath) ? modelsJsonPath : undefined,
|
|
137
|
-
);
|
|
138
|
-
}
|
|
167
|
+
// The sync facade all our resolvers consume. getAvailable()/getApiKeyAndHeaders()
|
|
168
|
+
// read a cached snapshot populated by create(), so the sync reads below work
|
|
169
|
+
// without further awaits.
|
|
170
|
+
const registry = new ModelRegistry(runtime);
|
|
171
|
+
registryRuntimes.set(registry, runtime);
|
|
172
|
+
return registry;
|
|
173
|
+
}
|
|
139
174
|
|
|
140
|
-
|
|
141
|
-
|
|
175
|
+
/**
|
|
176
|
+
* The ModelRuntime backing each registry this factory built. Researcher sessions
|
|
177
|
+
* must be created with `modelRuntime` (pi 0.80.8+ replaced the old
|
|
178
|
+
* `modelRegistry` session option): without it, createAgentSession() builds its
|
|
179
|
+
* OWN runtime from the default ~/.pi/agent paths, which never sees an explicit
|
|
180
|
+
* API key seeded via setRuntimeApiKey above — standalone PI_RESEARCH_API_KEY
|
|
181
|
+
* users with no (or a different-provider) auth.json would pass pre-flight and
|
|
182
|
+
* then fail auth on every researcher LLM call.
|
|
183
|
+
*/
|
|
184
|
+
const registryRuntimes = new WeakMap<ModelRegistry, ModelRuntime>();
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Return the ModelRuntime behind a registry built by {@link buildModelRegistry},
|
|
188
|
+
* or undefined for registries built elsewhere (e.g. the pi host's own
|
|
189
|
+
* ExtensionContext.modelRegistry — for those, createAgentSession()'s default
|
|
190
|
+
* runtime construction from the host's agent dir is exactly right).
|
|
191
|
+
*/
|
|
192
|
+
export function getRuntimeForRegistry(registry: unknown): ModelRuntime | undefined {
|
|
193
|
+
return registry instanceof Object ? registryRuntimes.get(registry as ModelRegistry) : undefined;
|
|
142
194
|
}
|
|
143
195
|
|
|
144
196
|
/**
|
package/src/index.ts
CHANGED
|
@@ -90,10 +90,14 @@ function extractResultText(result: AgentToolResult<unknown>): string {
|
|
|
90
90
|
* Pi Research Extension
|
|
91
91
|
*/
|
|
92
92
|
export default async function (pi: ExtensionAPI) {
|
|
93
|
-
// Runtime version check — must match the @earendil-works/* dependency minimum (>=0.80.
|
|
94
|
-
// 0.80.
|
|
95
|
-
//
|
|
96
|
-
//
|
|
93
|
+
// Runtime version check — must match the @earendil-works/* dependency minimum (>=0.80.8).
|
|
94
|
+
// 0.80.8 is the floor because pi 0.80.8 introduced ModelRuntime and removed
|
|
95
|
+
// AuthStorage/ModelRegistry.create(); buildModelRegistry (model-registry-factory.ts)
|
|
96
|
+
// unconditionally calls ModelRuntime.create(), and createAgentSession() is invoked
|
|
97
|
+
// without the removed `modelRegistry` option (researcher.ts), relying on the
|
|
98
|
+
// 0.80.8+ host auto-building its ModelRuntime. This in-host check is the real
|
|
99
|
+
// enforcement point: when running as an extension the host's version is NOT
|
|
100
|
+
// constrained by npm's resolution of our own package.json range.
|
|
97
101
|
const versionParts = PI_VERSION.split('.').map(Number);
|
|
98
102
|
const major = versionParts[0] ?? 0;
|
|
99
103
|
const minor = versionParts[1] ?? 0;
|
|
@@ -104,7 +108,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
104
108
|
`Please ensure pi-coding-agent is installed correctly.`,
|
|
105
109
|
);
|
|
106
110
|
}
|
|
107
|
-
const minMajor = 0, minMinor = 80, minPatch =
|
|
111
|
+
const minMajor = 0, minMinor = 80, minPatch = 8;
|
|
108
112
|
const tooOld = major < minMajor
|
|
109
113
|
|| (major === minMajor && minor < minMinor)
|
|
110
114
|
|| (major === minMajor && minor === minMinor && patch < minPatch);
|
|
@@ -2278,9 +2278,13 @@ var ConfigSchema = Type.Object({
|
|
|
2278
2278
|
* never killed before it has used its full nav budget plus time waiting in the queue.
|
|
2279
2279
|
* (default: 10000) */
|
|
2280
2280
|
BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2e3, maximum: 12e4, default: 1e4 }),
|
|
2281
|
-
/** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-
|
|
2282
|
-
* Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
|
|
2283
|
-
|
|
2281
|
+
/** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
|
|
2282
|
+
* Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
|
|
2283
|
+
* Ceiling raised to 30min (matching RESEARCHER_TIMEOUT_MS): a slow model on a
|
|
2284
|
+
* large-context synthesis/plan call can legitimately run >10min, and the old
|
|
2285
|
+
* 600000 ceiling silently clamped an explicit PI_RESEARCH_LLM_TIMEOUT_MS
|
|
2286
|
+
* (e.g. 900000), causing those calls to time out mid-generation. */
|
|
2287
|
+
LLM_TIMEOUT_MS: Type.Number({ minimum: 6e4, maximum: 18e5, default: 3e5 }),
|
|
2284
2288
|
/** Chain-of-thought "thinking" level for the engine's own LLM calls (coordinator,
|
|
2285
2289
|
* evaluator, synthesis, JSON-repair, knowledge extraction) AND the researcher
|
|
2286
2290
|
* sub-agents. Default 'off': these calls emit structured JSON / cited reports, not
|
|
@@ -22,6 +22,7 @@ import type { Config } from '../config.ts';
|
|
|
22
22
|
import { logger } from '../logger.ts';
|
|
23
23
|
|
|
24
24
|
import { resolveResearchModel } from '../core/llm/research-model-resolver.ts';
|
|
25
|
+
import { getRuntimeForRegistry } from '../core/llm/model-registry-factory.ts';
|
|
25
26
|
|
|
26
27
|
export interface CreateResearcherSessionOptions {
|
|
27
28
|
cwd: string;
|
|
@@ -137,7 +138,17 @@ export async function createResearcherSession(options: CreateResearcherSessionOp
|
|
|
137
138
|
sessionManager: SessionManager.inMemory(), // Each researcher gets its own isolated session
|
|
138
139
|
settingsManager: researcherSettings,
|
|
139
140
|
model: modelToUse,
|
|
140
|
-
modelRegistry
|
|
141
|
+
// pi 0.80.8 replaced the SDK's `modelRegistry`/`authStorage` session options
|
|
142
|
+
// with the async `modelRuntime`. Pass the runtime backing OUR registry when
|
|
143
|
+
// this run built one (standalone CLI/SDK): it carries any explicit API key
|
|
144
|
+
// seeded via setRuntimeApiKey, which the session's own default runtime
|
|
145
|
+
// (auto-built from ~/.pi/agent/{auth,models}.json) would never see — without
|
|
146
|
+
// this, standalone PI_RESEARCH_API_KEY users fail auth on every researcher
|
|
147
|
+
// LLM call despite passing pre-flight. For registries we did NOT build
|
|
148
|
+
// (the pi host's ExtensionContext.modelRegistry), this resolves to
|
|
149
|
+
// undefined and the host auto-builds from its own agent dir — correct
|
|
150
|
+
// in-host, where auth lives in the host's config.
|
|
151
|
+
modelRuntime: getRuntimeForRegistry(modelRegistry),
|
|
141
152
|
resourceLoader: makeResourceLoader(systemPrompt),
|
|
142
153
|
// Thinking/reasoning is disabled for researcher agents: they do retrieval and
|
|
143
154
|
// synthesis from scraped pages, not open-ended reasoning. Keeping it off reduces
|
package/src/sdk.ts
CHANGED
|
@@ -286,7 +286,7 @@ async function _doInit(options: ResearchSDKOptions = {}): Promise<void> {
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
// Build and cache the registry (one instance for the lifetime of this init cycle).
|
|
289
|
-
globalRegistry = sharedBuildModelRegistry(globalApiKey, parsedProvider);
|
|
289
|
+
globalRegistry = await sharedBuildModelRegistry(globalApiKey, parsedProvider);
|
|
290
290
|
|
|
291
291
|
try {
|
|
292
292
|
// Resolve the model. Precedence: explicit option (a Model object is used
|
|
@@ -5,31 +5,21 @@
|
|
|
5
5
|
* Both files use identical ResourceLoader implementations.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import { createExtensionRuntime, type ResourceLoader } from '@earendil-works/pi-coding-agent';
|
|
9
9
|
|
|
10
10
|
export function makeResourceLoader(systemPromptText: string): ResourceLoader {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
getAllTools: () => [],
|
|
24
|
-
setActiveTools: () => {},
|
|
25
|
-
refreshTools: () => {},
|
|
26
|
-
getCommands: () => [],
|
|
27
|
-
setModel: async () => true,
|
|
28
|
-
getThinkingLevel: () => 'off',
|
|
29
|
-
setThinkingLevel: () => {},
|
|
30
|
-
assertActive: () => {},
|
|
31
|
-
invalidate: () => {},
|
|
32
|
-
};
|
|
11
|
+
// Use pi's own createExtensionRuntime() instead of hand-maintaining a mock.
|
|
12
|
+
//
|
|
13
|
+
// A literal mock object drifted out of sync with the ExtensionRuntime contract:
|
|
14
|
+
// pi 0.81.0 ("Full provider extensions") added `pendingNativeProviderRegistrations`
|
|
15
|
+
// + `registerNativeProvider`, which this mock omitted. Under pi >= 0.81.0 the
|
|
16
|
+
// host's runner.bindCore() / createAgentSessionServices() iterate that field —
|
|
17
|
+
// `for (... of runtime.pendingNativeProviderRegistrations)` — and an `undefined`
|
|
18
|
+
// field threw `this.runtime.pendingNativeProviderRegistrations is not iterable`,
|
|
19
|
+
// aborting EVERY researcher session ("Research produced no report — no source
|
|
20
|
+
// material was collected"). createExtensionRuntime() always returns a runtime
|
|
21
|
+
// that satisfies the current host's full contract, so this can't drift again.
|
|
22
|
+
const mockRuntime = createExtensionRuntime();
|
|
33
23
|
|
|
34
24
|
return {
|
|
35
25
|
getExtensions: () => ({ extensions: [], errors: [], runtime: mockRuntime }),
|
|
@@ -61,10 +61,22 @@ export async function performSearch(
|
|
|
61
61
|
if (onProgress) onProgress(0);
|
|
62
62
|
|
|
63
63
|
// Hard cap per query so a single Cloudflare block or hung browser worker
|
|
64
|
-
// cannot stall the entire Promise.all burst.
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
64
|
+
// cannot stall the entire Promise.all burst.
|
|
65
|
+
//
|
|
66
|
+
// The budget is the worker's nav budget (SEARCH_TIMEOUT_MS) PLUS the
|
|
67
|
+
// queue-wait/overhead margin (BROWSER_TASK_TIMEOUT_MS), mirroring the
|
|
68
|
+
// scheduler's own task-timeout ceiling in BrowserTaskScheduler.runSearch
|
|
69
|
+
// (= SEARCH_TIMEOUT_MS + BROWSER_TASK_TIMEOUT_MS). This clock starts at
|
|
70
|
+
// ENQUEUE (before runWorkerSearch queues onto the shared browser pool), so
|
|
71
|
+
// a budget of only SEARCH_TIMEOUT_MS would let a query that sat in the
|
|
72
|
+
// saturated worker queue burn its ENTIRE nav budget waiting for a slot and
|
|
73
|
+
// abort with zero actual search work — exactly the "Query timed out after
|
|
74
|
+
// 45000ms (likely blocked or slow startup)" cascade observed under
|
|
75
|
+
// concurrent load. Including the margin lets a queued query still use its
|
|
76
|
+
// full nav budget once a worker picks it up, and keeps this layer's hard
|
|
77
|
+
// cap coherent with the scheduler's so it never preempts the worker's own
|
|
78
|
+
// (post-dequeue) deadline.
|
|
79
|
+
const QUERY_TIMEOUT_MS = (config?.SEARCH_TIMEOUT_MS ?? 45_000) + (config?.BROWSER_TASK_TIMEOUT_MS ?? 10_000);
|
|
68
80
|
|
|
69
81
|
const filteredQueries = queries.filter(q => q.trim());
|
|
70
82
|
let timeoutCount = 0;
|
|
@@ -133,7 +145,7 @@ export async function performSearch(
|
|
|
133
145
|
metrics.increment('browser_search_queries_total', 1, { status });
|
|
134
146
|
|
|
135
147
|
if (isTimeout) {
|
|
136
|
-
logger.warn(`[Search] Query timed out after ${QUERY_TIMEOUT_MS}ms
|
|
148
|
+
logger.warn(`[Search] Query timed out after ${QUERY_TIMEOUT_MS}ms — likely starved waiting for a free browser worker under concurrent load, or a slow/blocked search provider: "${query}"`);
|
|
137
149
|
} else {
|
|
138
150
|
const msg = error instanceof Error ? error.message : String(error);
|
|
139
151
|
if (msg !== 'Aborted') {
|