@lincoln504/pi-research 1.0.2 → 1.0.4
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 +119 -34
- package/docs/AGENT-SKILL.md +16 -3
- 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 +92 -22
- package/src/core/llm/model-registry-factory.ts +17 -6
- package/src/orchestration/deep-research-orchestrator.ts +17 -2
- package/src/orchestration/research-orchestration-service.ts +3 -2
- package/src/orchestration/researcher-executor.ts +1 -1
- package/src/orchestration/session-state.ts +33 -1
- package/src/research-config.ts +14 -1
- package/src/sdk.ts +48 -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
|
@@ -3252,7 +3252,7 @@ function buildModelRegistry(apiKey, provider) {
|
|
|
3252
3252
|
fs5.existsSync(modelsJsonPath) ? modelsJsonPath : void 0
|
|
3253
3253
|
);
|
|
3254
3254
|
}
|
|
3255
|
-
if (fs5.existsSync(authPath)) {
|
|
3255
|
+
if (fs5.existsSync(authPath) || fs5.existsSync(modelsJsonPath)) {
|
|
3256
3256
|
const authStorage = AuthStorage.create(authPath);
|
|
3257
3257
|
return ModelRegistry.create(
|
|
3258
3258
|
authStorage,
|
|
@@ -3266,7 +3266,17 @@ function constructMinimalModel(provider, modelId, _apiKey) {
|
|
|
3266
3266
|
provider,
|
|
3267
3267
|
id: modelId,
|
|
3268
3268
|
name: modelId,
|
|
3269
|
-
|
|
3269
|
+
// Map to a REAL pi-ai api id — "<provider>-conversations" is not one, and a
|
|
3270
|
+
// keyed no-registry setup would pass pre-flight then die on the first call.
|
|
3271
|
+
// Unknown providers default to the OpenAI-compatible protocol, which is what
|
|
3272
|
+
// virtually every third-party endpoint (openrouter, groq, cerebras, …) speaks.
|
|
3273
|
+
api: {
|
|
3274
|
+
openai: "openai-completions",
|
|
3275
|
+
anthropic: "anthropic-messages",
|
|
3276
|
+
google: "google-generative-ai",
|
|
3277
|
+
gemini: "google-generative-ai",
|
|
3278
|
+
mistral: "mistral-conversations"
|
|
3279
|
+
}[provider] ?? "openai-completions",
|
|
3270
3280
|
baseUrl: "",
|
|
3271
3281
|
// Provider-specific base URLs are handled by pi-ai internal registry
|
|
3272
3282
|
reasoning: false,
|
|
@@ -19812,6 +19822,7 @@ function getPiState(piSessionId) {
|
|
|
19812
19822
|
if (!state) {
|
|
19813
19823
|
state = {
|
|
19814
19824
|
failures: /* @__PURE__ */ new Map(),
|
|
19825
|
+
failureReasons: /* @__PURE__ */ new Map(),
|
|
19815
19826
|
order: [],
|
|
19816
19827
|
panels: /* @__PURE__ */ new Map(),
|
|
19817
19828
|
aborts: /* @__PURE__ */ new Map(),
|
|
@@ -19883,6 +19894,7 @@ function refreshAllSessions(piSessionId) {
|
|
|
19883
19894
|
for (const id of state.order) {
|
|
19884
19895
|
if (!validIds.includes(id)) {
|
|
19885
19896
|
state.failures.delete(id);
|
|
19897
|
+
state.failureReasons.delete(id);
|
|
19886
19898
|
state.panels.delete(id);
|
|
19887
19899
|
}
|
|
19888
19900
|
}
|
|
@@ -19914,6 +19926,7 @@ function endResearchSession(piSessionId, researchId) {
|
|
|
19914
19926
|
const state = piSessions.get(sid);
|
|
19915
19927
|
if (!state) return;
|
|
19916
19928
|
state.failures.delete(researchId);
|
|
19929
|
+
state.failureReasons.delete(researchId);
|
|
19917
19930
|
state.panels.delete(researchId);
|
|
19918
19931
|
state.aborts.delete(researchId);
|
|
19919
19932
|
const index = state.order.indexOf(researchId);
|
|
@@ -19931,6 +19944,7 @@ function endResearchSession(piSessionId, researchId) {
|
|
|
19931
19944
|
piSessions.delete(sid);
|
|
19932
19945
|
} else {
|
|
19933
19946
|
state.failures.clear();
|
|
19947
|
+
state.failureReasons.clear();
|
|
19934
19948
|
state.aborts.clear();
|
|
19935
19949
|
}
|
|
19936
19950
|
}
|
|
@@ -19952,12 +19966,23 @@ function clearAllSessionState() {
|
|
|
19952
19966
|
clearAllSharedLinks();
|
|
19953
19967
|
logger.log("[session-state] All session state cleared");
|
|
19954
19968
|
}
|
|
19955
|
-
function recordResearcherFailure(piSessionId, researchId, researcherId) {
|
|
19969
|
+
function recordResearcherFailure(piSessionId, researchId, researcherId, reason) {
|
|
19956
19970
|
const sid = normalizeSessionId(piSessionId);
|
|
19957
19971
|
const state = getPiState(sid);
|
|
19958
19972
|
const failures = state.failures.get(researchId) || [];
|
|
19959
19973
|
failures.push(researcherId);
|
|
19960
19974
|
state.failures.set(researchId, failures);
|
|
19975
|
+
if (reason) {
|
|
19976
|
+
const reasons = state.failureReasons.get(researchId) ?? /* @__PURE__ */ new Map();
|
|
19977
|
+
if (!reasons.has(researcherId)) reasons.set(researcherId, reason);
|
|
19978
|
+
state.failureReasons.set(researchId, reasons);
|
|
19979
|
+
}
|
|
19980
|
+
}
|
|
19981
|
+
function getResearcherFailureReasons(piSessionId, researchId) {
|
|
19982
|
+
const sid = normalizeSessionId(piSessionId);
|
|
19983
|
+
const state = getPiState(sid);
|
|
19984
|
+
const reasons = state.failureReasons.get(researchId);
|
|
19985
|
+
return reasons ? Object.fromEntries(reasons) : {};
|
|
19961
19986
|
}
|
|
19962
19987
|
function getFailedResearchers(piSessionId, researchId) {
|
|
19963
19988
|
const sid = normalizeSessionId(piSessionId);
|
|
@@ -19974,9 +19999,13 @@ function getResearchStopMessage(piSessionId, researchId) {
|
|
|
19974
19999
|
const sid = normalizeSessionId(piSessionId);
|
|
19975
20000
|
const failed = getFailedResearchers(sid, researchId);
|
|
19976
20001
|
const count = failed.length;
|
|
20002
|
+
const state = getPiState(sid);
|
|
20003
|
+
const reasons = state.failureReasons.get(researchId);
|
|
20004
|
+
const recorded = reasons && reasons.size > 0 ? ["Recorded errors:", ...[...reasons].map(([id, r]) => `\u2022 researcher ${id}: ${r}`), ""] : [];
|
|
19977
20005
|
return [
|
|
19978
20006
|
`Research stopped: ${count} researcher(s) did not return a usable report: ${failed.join(", ")}.`,
|
|
19979
20007
|
"",
|
|
20008
|
+
...recorded,
|
|
19980
20009
|
"Most likely cause \u2014 the research model could not produce grounded results:",
|
|
19981
20010
|
'\u2022 Try a more capable research model. Very small or "thinking-only" models often',
|
|
19982
20011
|
" emit no final text after their tool calls, which fails every researcher.",
|
|
@@ -20026,7 +20055,7 @@ ${currentPlan.allQueries.map((q) => `- ${q}`).join("\n")}
|
|
|
20026
20055
|
const researcherPromptTemplate = loadPrompt("researcher");
|
|
20027
20056
|
if (initialLinks.length === 0 && historicalUrls.length === 0) {
|
|
20028
20057
|
logger.warn(`[ResearcherExecutor] Researcher ${id} has no initial search results or historical links; skipping.`);
|
|
20029
|
-
recordResearcherFailure(sessionId, researchId, id);
|
|
20058
|
+
recordResearcherFailure(sessionId, researchId, id, "no initial search results or historical links (search produced nothing usable)");
|
|
20030
20059
|
metrics.increment("researcher_skipped_total", 1, { mode: "deep", complexity: String(complexity), reason: "no_initial_links" });
|
|
20031
20060
|
observer?.onResearcherFailure?.(id, "No initial search results or historical links available");
|
|
20032
20061
|
return;
|
|
@@ -20966,7 +20995,12 @@ var DeepResearchOrchestrator = class {
|
|
|
20966
20995
|
logger.warn(`[DeepOrchestrator] LLM returned empty synthesis for ${researchId}, building fallback from ${synthesisService2.getReportCount(researchId)} reports`);
|
|
20967
20996
|
result = synthesisService2.buildFallbackSynthesis(researchId, this.currentRound);
|
|
20968
20997
|
} else {
|
|
20969
|
-
|
|
20998
|
+
const failedIds = getFailedResearchers(this.options.sessionId, researchId);
|
|
20999
|
+
const reasons = getResearcherFailureReasons(this.options.sessionId, researchId);
|
|
21000
|
+
const causes = failedIds.map((id) => `researcher ${id}: ${reasons[id] ?? "no usable report"}`).join("; ");
|
|
21001
|
+
throw new Error(
|
|
21002
|
+
"Research produced no report \u2014 no source material was collected" + (causes ? `. Causes: ${causes}` : ".")
|
|
21003
|
+
);
|
|
20970
21004
|
}
|
|
20971
21005
|
}
|
|
20972
21006
|
try {
|
|
@@ -21268,7 +21302,7 @@ var ResearchOrchestrationService = class {
|
|
|
21268
21302
|
logger.debug(`[ResearchOrchestrationService] Researcher ${id} cancelled (aborted).`);
|
|
21269
21303
|
} else {
|
|
21270
21304
|
logger.error(`[ResearchOrchestrationService] Researcher ${id} failed: ${errMsg}`);
|
|
21271
|
-
recordResearcherFailure(sessionId, researchId, id);
|
|
21305
|
+
recordResearcherFailure(sessionId, researchId, id, errMsg);
|
|
21272
21306
|
observer?.onResearcherFailure?.(id, errMsg);
|
|
21273
21307
|
}
|
|
21274
21308
|
}
|
|
@@ -22000,14 +22034,19 @@ async function _doInit(options = {}) {
|
|
|
22000
22034
|
}
|
|
22001
22035
|
globalApiKey = options.apiKey || process.env["PI_RESEARCH_API_KEY"];
|
|
22002
22036
|
let parsedProvider = options.provider || process.env["PI_RESEARCH_PROVIDER"];
|
|
22003
|
-
if (
|
|
22004
|
-
|
|
22005
|
-
|
|
22037
|
+
if (options.model) {
|
|
22038
|
+
const explicit = typeof options.model === "string" ? options.model : `${options.model.provider}/${options.model.id}`;
|
|
22039
|
+
globalConfig = { ...globalConfig, RESEARCH_MODEL: explicit };
|
|
22040
|
+
}
|
|
22041
|
+
const effectiveModel = options.model ?? globalConfig.RESEARCH_MODEL;
|
|
22042
|
+
if (!parsedProvider && effectiveModel) {
|
|
22043
|
+
if (typeof effectiveModel === "string") {
|
|
22044
|
+
const slashIdx = effectiveModel.indexOf("/");
|
|
22006
22045
|
if (slashIdx > 0) {
|
|
22007
|
-
parsedProvider =
|
|
22046
|
+
parsedProvider = effectiveModel.slice(0, slashIdx);
|
|
22008
22047
|
}
|
|
22009
|
-
} else if (
|
|
22010
|
-
parsedProvider =
|
|
22048
|
+
} else if (effectiveModel.provider) {
|
|
22049
|
+
parsedProvider = effectiveModel.provider;
|
|
22011
22050
|
}
|
|
22012
22051
|
}
|
|
22013
22052
|
if (globalApiKey && !parsedProvider) {
|
|
@@ -22015,7 +22054,16 @@ async function _doInit(options = {}) {
|
|
|
22015
22054
|
}
|
|
22016
22055
|
globalRegistry = buildModelRegistry(globalApiKey, parsedProvider);
|
|
22017
22056
|
try {
|
|
22018
|
-
|
|
22057
|
+
if (effectiveModel && typeof effectiveModel === "object" && effectiveModel.id) {
|
|
22058
|
+
globalModel = effectiveModel;
|
|
22059
|
+
} else {
|
|
22060
|
+
globalModel = resolveModel(
|
|
22061
|
+
globalRegistry,
|
|
22062
|
+
typeof effectiveModel === "string" ? effectiveModel : void 0,
|
|
22063
|
+
parsedProvider,
|
|
22064
|
+
globalApiKey
|
|
22065
|
+
);
|
|
22066
|
+
}
|
|
22019
22067
|
globalContainer = getServiceContainer();
|
|
22020
22068
|
registerCoreServices(globalContainer);
|
|
22021
22069
|
registerInfrastructureServices(globalContainer);
|
|
@@ -22465,6 +22513,7 @@ Total cost: ${costStr}`;
|
|
|
22465
22513
|
|
|
22466
22514
|
// src/cli.ts
|
|
22467
22515
|
init_config();
|
|
22516
|
+
init_model_registry_factory();
|
|
22468
22517
|
import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent";
|
|
22469
22518
|
function readPkgVersion() {
|
|
22470
22519
|
try {
|
|
@@ -22583,6 +22632,15 @@ function fileExists(p) {
|
|
|
22583
22632
|
return false;
|
|
22584
22633
|
}
|
|
22585
22634
|
}
|
|
22635
|
+
function piKeysAvailable(piAuthPresent, provider) {
|
|
22636
|
+
try {
|
|
22637
|
+
const available = safeGetAvailable(buildModelRegistry(void 0, void 0));
|
|
22638
|
+
if (!provider) return available.length > 0;
|
|
22639
|
+
return available.some((m) => m.provider === provider);
|
|
22640
|
+
} catch {
|
|
22641
|
+
return piAuthPresent;
|
|
22642
|
+
}
|
|
22643
|
+
}
|
|
22586
22644
|
function detectCredentials() {
|
|
22587
22645
|
const paths = resolvedConfigPaths();
|
|
22588
22646
|
const piAuthPresent = fileExists(paths.piAuth);
|
|
@@ -22594,6 +22652,7 @@ function detectCredentials() {
|
|
|
22594
22652
|
const model = modelEnv ?? modelConfig;
|
|
22595
22653
|
const modelFrom = modelEnv ? "PI_RESEARCH_MODEL" : modelConfig ? "config.env" : "unset";
|
|
22596
22654
|
const provider = providerEnv ?? (model && model.includes("/") ? model.slice(0, model.indexOf("/")) : void 0);
|
|
22655
|
+
const piKeysUsable = piKeysAvailable(piAuthPresent, provider);
|
|
22597
22656
|
if (apiKey) {
|
|
22598
22657
|
if (!provider) {
|
|
22599
22658
|
return {
|
|
@@ -22629,11 +22688,23 @@ function detectCredentials() {
|
|
|
22629
22688
|
piModelsPresent
|
|
22630
22689
|
};
|
|
22631
22690
|
}
|
|
22632
|
-
if (
|
|
22691
|
+
if (piKeysUsable) {
|
|
22692
|
+
if (!model) {
|
|
22693
|
+
return {
|
|
22694
|
+
source: "pi-config",
|
|
22695
|
+
apiKeyConfigured: true,
|
|
22696
|
+
provider,
|
|
22697
|
+
model,
|
|
22698
|
+
modelFrom,
|
|
22699
|
+
piAuthPresent,
|
|
22700
|
+
piModelsPresent,
|
|
22701
|
+
problem: `pi credentials were found (in pi's configuration), 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).`
|
|
22702
|
+
};
|
|
22703
|
+
}
|
|
22633
22704
|
return {
|
|
22634
22705
|
source: "pi-config",
|
|
22635
22706
|
apiKeyConfigured: true,
|
|
22636
|
-
// key lives in
|
|
22707
|
+
// key lives in pi's configuration; the SDK reads it at call time
|
|
22637
22708
|
provider,
|
|
22638
22709
|
model,
|
|
22639
22710
|
modelFrom,
|
|
@@ -22649,7 +22720,7 @@ function detectCredentials() {
|
|
|
22649
22720
|
modelFrom,
|
|
22650
22721
|
piAuthPresent,
|
|
22651
22722
|
piModelsPresent,
|
|
22652
|
-
problem: "No model or API key is configured for pi-research. Provide credentials via environment variables (PI_RESEARCH_API_KEY + PI_RESEARCH_PROVIDER + PI_RESEARCH_MODEL) or, if you use pi, via ~/.pi/agent/auth.json."
|
|
22723
|
+
problem: "No model or API key is configured for pi-research. Provide credentials via environment variables (PI_RESEARCH_API_KEY + PI_RESEARCH_PROVIDER + PI_RESEARCH_MODEL) or, if you use pi, via pi's configuration (a provider entry in ~/.pi/agent/auth.json, or a per-provider apiKey in ~/.pi/agent/models.json)."
|
|
22653
22724
|
};
|
|
22654
22725
|
}
|
|
22655
22726
|
function configBlock(det, extraNote) {
|
|
@@ -22659,14 +22730,14 @@ function configBlock(det, extraNote) {
|
|
|
22659
22730
|
lines.push(` \u2022 base config file: ${paths.configEnv}`);
|
|
22660
22731
|
lines.push(` \u2022 cli overlay: ${paths.cliIfaceEnv} (optional; overrides base for this CLI / agent skill)`);
|
|
22661
22732
|
lines.push(` \u2022 env vars: PI_RESEARCH_API_KEY / PI_RESEARCH_PROVIDER / PI_RESEARCH_MODEL`);
|
|
22662
|
-
lines.push(` \u2022 pi
|
|
22663
|
-
lines.push(` \u2022 pi models:
|
|
22733
|
+
lines.push(` \u2022 pi config (keys): ${paths.piAuth}`);
|
|
22734
|
+
lines.push(` \u2022 pi config (models): ${paths.piModels}`);
|
|
22664
22735
|
lines.push("");
|
|
22665
22736
|
lines.push("Detected:");
|
|
22666
22737
|
lines.push(` \u2022 credential source: ${det.source}`);
|
|
22667
22738
|
lines.push(` \u2022 api key configured: ${det.apiKeyConfigured}`);
|
|
22668
|
-
lines.push(` \u2022 provider: ${det.provider ?? "(unset)"}`);
|
|
22669
|
-
lines.push(` \u2022 model: ${det.model
|
|
22739
|
+
lines.push(` \u2022 provider: ${det.provider ?? "(unset \u2014 inferred from a provider/model-id model)"}`);
|
|
22740
|
+
lines.push(` \u2022 model: ${det.model ? `${det.model} [from: ${det.modelFrom}]` : "(not set \u2014 set PI_RESEARCH_MODEL, required)"}`);
|
|
22670
22741
|
lines.push(` \u2022 pi auth.json: ${det.piAuthPresent ? "present" : "absent"}`);
|
|
22671
22742
|
lines.push(` \u2022 pi models.json: ${det.piModelsPresent ? "present" : "absent"}`);
|
|
22672
22743
|
if (extraNote) lines.push(` \u2022 note: ${extraNote}`);
|
|
@@ -22923,8 +22994,9 @@ uninstall: ${BINARY_NAME} skill uninstall
|
|
|
22923
22994
|
} catch (err) {
|
|
22924
22995
|
return reportError(err, "skill install", s.json);
|
|
22925
22996
|
}
|
|
22997
|
+
const skillModel = process.env["PI_RESEARCH_MODEL"] ?? getConfig(process.cwd(), "cli").RESEARCH_MODEL;
|
|
22926
22998
|
if (s.json) {
|
|
22927
|
-
toStdout(pretty({ command: "skill", action: "install", dryRun: !!s.dryRun, results: results2 }));
|
|
22999
|
+
toStdout(pretty({ command: "skill", action: "install", dryRun: !!s.dryRun, results: results2, modelConfigured: Boolean(skillModel) }));
|
|
22928
23000
|
} else {
|
|
22929
23001
|
toStdout(`pi-research agent skill \u2014 ${s.dryRun ? "install (dry run)" : "install"}
|
|
22930
23002
|
|
|
@@ -22934,6 +23006,15 @@ uninstall: ${BINARY_NAME} skill uninstall
|
|
|
22934
23006
|
toStdout(`
|
|
22935
23007
|
The skill activates automatically \u2014 ask the agent to research something.
|
|
22936
23008
|
`);
|
|
23009
|
+
if (!skillModel) {
|
|
23010
|
+
toStdout(
|
|
23011
|
+
`
|
|
23012
|
+
Required: set PI_RESEARCH_MODEL=provider/model-id in ${resolvedConfigPaths().configEnv}
|
|
23013
|
+
(or as an env var) \u2014 the skill runs only on this configured model, not your pi
|
|
23014
|
+
session model. Verify with: ${BINARY_NAME} status
|
|
23015
|
+
`
|
|
23016
|
+
);
|
|
23017
|
+
}
|
|
22937
23018
|
}
|
|
22938
23019
|
return results2.some((r) => r.status === "error") ? EXIT.SOFTWARE : EXIT.OK;
|
|
22939
23020
|
}
|
|
@@ -23003,7 +23084,7 @@ function reportError(err, what, json) {
|
|
|
23003
23084
|
const lower = msg2.toLowerCase();
|
|
23004
23085
|
const isConfigError = lower.includes("no llm model available") || lower.includes("not found in pi") || lower.includes("no api key") || lower.includes("provider must be specified") || lower.includes("invalid model string") || lower.includes("authentication") || lower.includes("unauthorized") || lower.includes("api key");
|
|
23005
23086
|
const isRateLimit = lower.includes("429") || lower.includes("rate limit") || lower.includes("too many requests");
|
|
23006
|
-
const exitCode = isConfigError ? EXIT.CONFIG : EXIT.SOFTWARE;
|
|
23087
|
+
const exitCode = isConfigError && !isRateLimit ? EXIT.CONFIG : EXIT.SOFTWARE;
|
|
23007
23088
|
if (json) {
|
|
23008
23089
|
const payload = { ok: false, error: msg2, exitCode };
|
|
23009
23090
|
if (process.env["PI_RESEARCH_DEBUG"] === "true" && err instanceof Error && err.stack) {
|
|
@@ -23012,18 +23093,18 @@ function reportError(err, what, json) {
|
|
|
23012
23093
|
toStdout(pretty(payload));
|
|
23013
23094
|
return exitCode;
|
|
23014
23095
|
}
|
|
23015
|
-
if (
|
|
23096
|
+
if (isRateLimit) {
|
|
23016
23097
|
toStderr(`
|
|
23017
|
-
Error: pi-research ${what}
|
|
23018
|
-
|
|
23019
|
-
${configBlock(detectCredentials())}
|
|
23098
|
+
Error: pi-research ${what} halted: provider rate limit reached. Wait a moment and retry.
|
|
23099
|
+
${msg2}
|
|
23020
23100
|
`);
|
|
23021
23101
|
return exitCode;
|
|
23022
23102
|
}
|
|
23023
|
-
if (
|
|
23103
|
+
if (isConfigError) {
|
|
23024
23104
|
toStderr(`
|
|
23025
|
-
Error: pi-research ${what}
|
|
23026
|
-
|
|
23105
|
+
Error: pi-research ${what} failed: ${msg2}
|
|
23106
|
+
|
|
23107
|
+
${configBlock(detectCredentials())}
|
|
23027
23108
|
`);
|
|
23028
23109
|
return exitCode;
|
|
23029
23110
|
}
|
|
@@ -23091,11 +23172,15 @@ COMMANDS
|
|
|
23091
23172
|
CONFIGURE
|
|
23092
23173
|
Credentials are resolved in this order (first match wins):
|
|
23093
23174
|
|
|
23094
|
-
env vars:
|
|
23095
|
-
cli overlay:
|
|
23096
|
-
base config:
|
|
23097
|
-
pi
|
|
23098
|
-
pi models:
|
|
23175
|
+
env vars: PI_RESEARCH_API_KEY PI_RESEARCH_PROVIDER PI_RESEARCH_MODEL
|
|
23176
|
+
cli overlay: ${p.cliIfaceEnv} (optional; this CLI / agent skill only)
|
|
23177
|
+
base config: ${p.configEnv}
|
|
23178
|
+
pi config (keys): ${p.piAuth}
|
|
23179
|
+
pi config (models): ${p.piModels}
|
|
23180
|
+
|
|
23181
|
+
A model is REQUIRED: set PI_RESEARCH_MODEL to a "provider/model-id". This CLI and
|
|
23182
|
+
the agent skill run only on that configured model \u2014 they do not follow the model
|
|
23183
|
+
selected inside the pi extension.
|
|
23099
23184
|
|
|
23100
23185
|
The pi extension has its own optional overlay file:
|
|
23101
23186
|
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/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 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.4", "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 your pi
|
|
19
|
+
configuration 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
|
@@ -39,6 +39,7 @@ import { validateInitialLink, MAX_INITIAL_LINKS } from './utils/url-utils.ts';
|
|
|
39
39
|
import { exportResearchReport, appendExportMessage } from './utils/research-export.ts';
|
|
40
40
|
import { getConfig, getGlobalConfigDir, getGlobalEnvFilePath, getInterfaceEnvFilePath, saveConfig, resetConfig, describeKnowledgeStoreMode, isProjectScopedKey, parseDotEnv } from './config.ts';
|
|
41
41
|
import { getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
42
|
+
import { buildModelRegistry, safeGetAvailable } from './core/llm/model-registry-factory.ts';
|
|
42
43
|
|
|
43
44
|
// ---------------------------------------------------------------------------
|
|
44
45
|
// Constants
|
|
@@ -196,7 +197,7 @@ export interface ResolvedConfigPaths {
|
|
|
196
197
|
cliIfaceEnv: string;
|
|
197
198
|
/** Per-interface overlay for the pi extension (~/.pi/research/pi.env). */
|
|
198
199
|
piIfaceEnv: string;
|
|
199
|
-
/** pi
|
|
200
|
+
/** pi's key file (~/.pi/agent/auth.json), part of pi's configuration. */
|
|
200
201
|
piAuth: string;
|
|
201
202
|
/** pi model definitions (~/.pi/agent/models.json). */
|
|
202
203
|
piModels: string;
|
|
@@ -226,6 +227,32 @@ function fileExists(p: string): boolean {
|
|
|
226
227
|
}
|
|
227
228
|
}
|
|
228
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Are keys usable from pi's configuration? Answered by pi's OWN semantics, not
|
|
232
|
+
* by file presence: build pi's model registry (two small JSON reads — no
|
|
233
|
+
* services, network, or native deps) and ask for the authed subset. That check
|
|
234
|
+
* (`hasConfiguredAuth`) covers every source the registry consults at call
|
|
235
|
+
* time: an auth.json provider entry (api-key or OAuth), a per-provider apiKey
|
|
236
|
+
* embedded in models.json (env-template aware), and provider env vars like
|
|
237
|
+
* OPENAI_API_KEY. File presence alone is neither necessary (keys can live in
|
|
238
|
+
* models.json / env with no auth.json) nor sufficient (auth.json can be `{}`).
|
|
239
|
+
*/
|
|
240
|
+
function piKeysAvailable(piAuthPresent: boolean, provider?: string): boolean {
|
|
241
|
+
try {
|
|
242
|
+
const available = safeGetAvailable(buildModelRegistry(undefined, undefined));
|
|
243
|
+
if (!provider) return available.length > 0;
|
|
244
|
+
// Provider known (from the configured provider/model-id): keys must exist
|
|
245
|
+
// for THAT provider — a key for some other provider would pass pre-flight
|
|
246
|
+
// and then die on the first LLM call ("No API key found for <provider>").
|
|
247
|
+
return available.some((m) => m.provider === provider);
|
|
248
|
+
} catch {
|
|
249
|
+
// Cannot inspect (e.g. malformed models.json) — fall back to file
|
|
250
|
+
// presence: pre-flight must never block a possibly-working setup; the run
|
|
251
|
+
// itself reports the precise error.
|
|
252
|
+
return piAuthPresent;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
229
256
|
export interface CredentialDetection {
|
|
230
257
|
/** Where credentials will come from once initResearchSDK runs. */
|
|
231
258
|
source: 'explicit-env' | 'pi-config' | 'none';
|
|
@@ -258,6 +285,9 @@ function detectCredentials(): CredentialDetection {
|
|
|
258
285
|
// Provider is either explicit, or inferred from a "provider/id" model string.
|
|
259
286
|
const provider =
|
|
260
287
|
providerEnv ?? (model && model.includes('/') ? model.slice(0, model.indexOf('/')) : undefined);
|
|
288
|
+
// Provider-aware when the configured model names one: a key for a DIFFERENT
|
|
289
|
+
// provider must not greenlight this run.
|
|
290
|
+
const piKeysUsable = piKeysAvailable(piAuthPresent, provider);
|
|
261
291
|
|
|
262
292
|
// --- Explicit API-key path -------------------------------------------------
|
|
263
293
|
if (apiKey) {
|
|
@@ -300,12 +330,32 @@ function detectCredentials(): CredentialDetection {
|
|
|
300
330
|
};
|
|
301
331
|
}
|
|
302
332
|
|
|
303
|
-
// --- pi auth
|
|
304
|
-
if (
|
|
305
|
-
//
|
|
333
|
+
// --- pi configuration path (key from auth.json, models.json, or provider env) ---
|
|
334
|
+
if (piKeysUsable) {
|
|
335
|
+
// Keys come from pi's configuration, but the MODEL must be configured
|
|
336
|
+
// explicitly: the standalone CLI / agent skill run only on the configured
|
|
337
|
+
// model — they never follow the model selected inside the pi extension, and
|
|
338
|
+
// silently falling back to the registry's first authed entry picks a model
|
|
339
|
+
// nobody chose. Fail fast here (pre-flight, before any SDK init).
|
|
340
|
+
if (!model) {
|
|
341
|
+
return {
|
|
342
|
+
source: 'pi-config',
|
|
343
|
+
apiKeyConfigured: true,
|
|
344
|
+
provider,
|
|
345
|
+
model,
|
|
346
|
+
modelFrom,
|
|
347
|
+
piAuthPresent,
|
|
348
|
+
piModelsPresent,
|
|
349
|
+
problem:
|
|
350
|
+
'pi credentials were found (in pi\'s configuration), but no research model is configured. ' +
|
|
351
|
+
'The standalone CLI / agent skill run only on an explicitly configured model — they do not ' +
|
|
352
|
+
'follow the model selected inside the pi extension. Set PI_RESEARCH_MODEL to a ' +
|
|
353
|
+
'"provider/model-id" (in the environment, or in config.env / cli.env below).',
|
|
354
|
+
};
|
|
355
|
+
}
|
|
306
356
|
return {
|
|
307
357
|
source: 'pi-config',
|
|
308
|
-
apiKeyConfigured: true, // key lives in
|
|
358
|
+
apiKeyConfigured: true, // key lives in pi's configuration; the SDK reads it at call time
|
|
309
359
|
provider,
|
|
310
360
|
model,
|
|
311
361
|
modelFrom,
|
|
@@ -326,7 +376,8 @@ function detectCredentials(): CredentialDetection {
|
|
|
326
376
|
problem:
|
|
327
377
|
'No model or API key is configured for pi-research. Provide credentials via environment ' +
|
|
328
378
|
'variables (PI_RESEARCH_API_KEY + PI_RESEARCH_PROVIDER + PI_RESEARCH_MODEL) or, if you use ' +
|
|
329
|
-
|
|
379
|
+
"pi, via pi's configuration (a provider entry in ~/.pi/agent/auth.json, or a per-provider " +
|
|
380
|
+
'apiKey in ~/.pi/agent/models.json).',
|
|
330
381
|
};
|
|
331
382
|
}
|
|
332
383
|
|
|
@@ -342,14 +393,14 @@ function configBlock(det: CredentialDetection, extraNote?: string): string {
|
|
|
342
393
|
lines.push(` • base config file: ${paths.configEnv}`);
|
|
343
394
|
lines.push(` • cli overlay: ${paths.cliIfaceEnv} (optional; overrides base for this CLI / agent skill)`);
|
|
344
395
|
lines.push(` • env vars: PI_RESEARCH_API_KEY / PI_RESEARCH_PROVIDER / PI_RESEARCH_MODEL`);
|
|
345
|
-
lines.push(` • pi
|
|
346
|
-
lines.push(` • pi models:
|
|
396
|
+
lines.push(` • pi config (keys): ${paths.piAuth}`);
|
|
397
|
+
lines.push(` • pi config (models): ${paths.piModels}`);
|
|
347
398
|
lines.push('');
|
|
348
399
|
lines.push('Detected:');
|
|
349
400
|
lines.push(` • credential source: ${det.source}`);
|
|
350
401
|
lines.push(` • api key configured: ${det.apiKeyConfigured}`);
|
|
351
|
-
lines.push(` • provider: ${det.provider ?? '(unset)'}`);
|
|
352
|
-
lines.push(` • model: ${det.model
|
|
402
|
+
lines.push(` • provider: ${det.provider ?? '(unset — inferred from a provider/model-id model)'}`);
|
|
403
|
+
lines.push(` • model: ${det.model ? `${det.model} [from: ${det.modelFrom}]` : '(not set — set PI_RESEARCH_MODEL, required)'}`);
|
|
353
404
|
lines.push(` • pi auth.json: ${det.piAuthPresent ? 'present' : 'absent'}`);
|
|
354
405
|
lines.push(` • pi models.json: ${det.piModelsPresent ? 'present' : 'absent'}`);
|
|
355
406
|
if (extraNote) lines.push(` • note: ${extraNote}`);
|
|
@@ -657,12 +708,23 @@ async function cmdSkill(s: NonNullable<ParsedArgs['skill']>): Promise<number> {
|
|
|
657
708
|
} catch (err) {
|
|
658
709
|
return reportError(err, 'skill install', s.json);
|
|
659
710
|
}
|
|
711
|
+
// The skill runs only on an explicitly configured model — if none is set yet,
|
|
712
|
+
// say so right here, at the moment of install (mirrors the TUI install action).
|
|
713
|
+
const skillModel =
|
|
714
|
+
process.env['PI_RESEARCH_MODEL'] ?? getConfig(process.cwd(), 'cli').RESEARCH_MODEL;
|
|
660
715
|
if (s.json) {
|
|
661
|
-
toStdout(pretty({ command: 'skill', action: 'install', dryRun: !!s.dryRun, results }));
|
|
716
|
+
toStdout(pretty({ command: 'skill', action: 'install', dryRun: !!s.dryRun, results, modelConfigured: Boolean(skillModel) }));
|
|
662
717
|
} else {
|
|
663
718
|
toStdout(`pi-research agent skill — ${s.dryRun ? 'install (dry run)' : 'install'}\n\n`);
|
|
664
719
|
for (const r of results) toStdout(` ${r.tool.padEnd(10)} ${describeInstall(r)}\n`);
|
|
665
720
|
toStdout(`\nThe skill activates automatically — ask the agent to research something.\n`);
|
|
721
|
+
if (!skillModel) {
|
|
722
|
+
toStdout(
|
|
723
|
+
`\nRequired: set PI_RESEARCH_MODEL=provider/model-id in ${resolvedConfigPaths().configEnv}\n` +
|
|
724
|
+
`(or as an env var) — the skill runs only on this configured model, not your pi\n` +
|
|
725
|
+
`session model. Verify with: ${BINARY_NAME} status\n`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
666
728
|
}
|
|
667
729
|
return results.some((r) => r.status === 'error') ? EXIT.SOFTWARE : EXIT.OK;
|
|
668
730
|
}
|
|
@@ -732,8 +794,12 @@ function reportError(err: unknown, what: string, json?: boolean): number {
|
|
|
732
794
|
lower.includes('unauthorized') ||
|
|
733
795
|
lower.includes('api key');
|
|
734
796
|
|
|
797
|
+
// Rate limits win over config-shaped matches: a provider 429 often mentions
|
|
798
|
+
// "api key" in its body (e.g. "rate limit exceeded for this api key"), and
|
|
799
|
+
// classifying that as a setup problem sends the user to reconfigure a
|
|
800
|
+
// perfectly working key instead of waiting out the limit.
|
|
735
801
|
const isRateLimit = lower.includes('429') || lower.includes('rate limit') || lower.includes('too many requests');
|
|
736
|
-
const exitCode = isConfigError ? EXIT.CONFIG : EXIT.SOFTWARE;
|
|
802
|
+
const exitCode = isConfigError && !isRateLimit ? EXIT.CONFIG : EXIT.SOFTWARE;
|
|
737
803
|
|
|
738
804
|
// --json: emit a structured error on stdout so a machine consumer never has to
|
|
739
805
|
// special-case plain-text stderr. Mirrors the { ok: true, ... } success shape.
|
|
@@ -746,15 +812,15 @@ function reportError(err: unknown, what: string, json?: boolean): number {
|
|
|
746
812
|
return exitCode;
|
|
747
813
|
}
|
|
748
814
|
|
|
749
|
-
|
|
750
|
-
toStderr(`\nError: pi-research ${what} failed: ${msg}\n\n${configBlock(detectCredentials())}\n`);
|
|
751
|
-
return exitCode;
|
|
752
|
-
}
|
|
753
|
-
// Rate limits are operational, not fatal setup problems.
|
|
815
|
+
// Rate limits are operational, not fatal setup problems — checked FIRST.
|
|
754
816
|
if (isRateLimit) {
|
|
755
817
|
toStderr(`\nError: pi-research ${what} halted: provider rate limit reached. Wait a moment and retry.\n ${msg}\n`);
|
|
756
818
|
return exitCode;
|
|
757
819
|
}
|
|
820
|
+
if (isConfigError) {
|
|
821
|
+
toStderr(`\nError: pi-research ${what} failed: ${msg}\n\n${configBlock(detectCredentials())}\n`);
|
|
822
|
+
return exitCode;
|
|
823
|
+
}
|
|
758
824
|
toStderr(`\nError: pi-research ${what} failed: ${msg}\n`);
|
|
759
825
|
if (process.env['PI_RESEARCH_DEBUG'] === 'true' && err instanceof Error && err.stack) {
|
|
760
826
|
toStderr('\n' + err.stack + '\n');
|
|
@@ -842,11 +908,15 @@ COMMANDS
|
|
|
842
908
|
CONFIGURE
|
|
843
909
|
Credentials are resolved in this order (first match wins):
|
|
844
910
|
|
|
845
|
-
env vars:
|
|
846
|
-
cli overlay:
|
|
847
|
-
base config:
|
|
848
|
-
pi
|
|
849
|
-
pi models:
|
|
911
|
+
env vars: PI_RESEARCH_API_KEY PI_RESEARCH_PROVIDER PI_RESEARCH_MODEL
|
|
912
|
+
cli overlay: ${p.cliIfaceEnv} (optional; this CLI / agent skill only)
|
|
913
|
+
base config: ${p.configEnv}
|
|
914
|
+
pi config (keys): ${p.piAuth}
|
|
915
|
+
pi config (models): ${p.piModels}
|
|
916
|
+
|
|
917
|
+
A model is REQUIRED: set PI_RESEARCH_MODEL to a "provider/model-id". This CLI and
|
|
918
|
+
the agent skill run only on that configured model — they do not follow the model
|
|
919
|
+
selected inside the pi extension.
|
|
850
920
|
|
|
851
921
|
The pi extension has its own optional overlay file:
|
|
852
922
|
pi extension: ${p.piIfaceEnv}
|
|
@@ -123,12 +123,13 @@ export function buildModelRegistry(apiKey?: string, provider?: string): ModelReg
|
|
|
123
123
|
);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
// No explicit key: use the user's pi
|
|
126
|
+
// No explicit key: use the user's pi configuration when EITHER file exists.
|
|
127
127
|
// ModelRegistry.create() reads embedded apiKeys from models.json providers
|
|
128
|
-
// directly (via providerRequestConfigs), so
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
128
|
+
// directly (via providerRequestConfigs), so custom providers with embedded
|
|
129
|
+
// keys work with no auth.json at all — auth.json presence must not gate
|
|
130
|
+
// models.json (AuthStorage.create tolerates a missing file). Env provider
|
|
131
|
+
// keys (e.g. OPENAI_API_KEY) are honored by AuthStorage in every branch.
|
|
132
|
+
if (fs.existsSync(authPath) || fs.existsSync(modelsJsonPath)) {
|
|
132
133
|
const authStorage = AuthStorage.create(authPath);
|
|
133
134
|
return ModelRegistry.create(
|
|
134
135
|
authStorage,
|
|
@@ -152,7 +153,17 @@ export function constructMinimalModel(provider: string, modelId: string, _apiKey
|
|
|
152
153
|
provider,
|
|
153
154
|
id: modelId,
|
|
154
155
|
name: modelId,
|
|
155
|
-
|
|
156
|
+
// Map to a REAL pi-ai api id — "<provider>-conversations" is not one, and a
|
|
157
|
+
// keyed no-registry setup would pass pre-flight then die on the first call.
|
|
158
|
+
// Unknown providers default to the OpenAI-compatible protocol, which is what
|
|
159
|
+
// virtually every third-party endpoint (openrouter, groq, cerebras, …) speaks.
|
|
160
|
+
api: ({
|
|
161
|
+
openai: 'openai-completions',
|
|
162
|
+
anthropic: 'anthropic-messages',
|
|
163
|
+
google: 'google-generative-ai',
|
|
164
|
+
gemini: 'google-generative-ai',
|
|
165
|
+
mistral: 'mistral-conversations',
|
|
166
|
+
} as Record<string, string>)[provider] ?? 'openai-completions',
|
|
156
167
|
baseUrl: '', // Provider-specific base URLs are handled by pi-ai internal registry
|
|
157
168
|
reasoning: false,
|
|
158
169
|
input: ['text'],
|
|
@@ -9,7 +9,7 @@ import { type Model } from '@earendil-works/pi-ai';
|
|
|
9
9
|
import { logger } from '../logger.ts';
|
|
10
10
|
import { safeUnref } from '../utils/safe-unref.ts';
|
|
11
11
|
import { metrics } from '../utils/metrics.ts';
|
|
12
|
-
import { getSteeringMessages, consumeQueuedMessages, getActiveSteeringMessages } from './session-state.ts';
|
|
12
|
+
import { getSteeringMessages, consumeQueuedMessages, getActiveSteeringMessages, getFailedResearchers, getResearcherFailureReasons } from './session-state.ts';
|
|
13
13
|
import { MAX_EXTRA_ROUNDS_WITH_STEERING } from '../constants.ts';
|
|
14
14
|
import {
|
|
15
15
|
getMaxRounds,
|
|
@@ -486,7 +486,22 @@ export class DeepResearchOrchestrator {
|
|
|
486
486
|
logger.warn(`[DeepOrchestrator] LLM returned empty synthesis for ${researchId}, building fallback from ${synthesisService.getReportCount(researchId)} reports`);
|
|
487
487
|
result = synthesisService.buildFallbackSynthesis(researchId, this.currentRound);
|
|
488
488
|
} else {
|
|
489
|
-
|
|
489
|
+
// ZERO reports were collected — the run produced nothing groundable.
|
|
490
|
+
// With the default single-researcher plan the fast-stop threshold
|
|
491
|
+
// (MAX_FAILED_RESEARCHERS = 2) never trips, so a lone researcher that
|
|
492
|
+
// exhausts its retries (e.g. a provider rate limit) used to land here
|
|
493
|
+
// and return a hollow "no summary was generated" string on the SUCCESS
|
|
494
|
+
// path — exit 0. That is a FAILED run: throw, and surface the recorded
|
|
495
|
+
// root causes so the user sees the 429/model error, not a shrug.
|
|
496
|
+
const failedIds = getFailedResearchers(this.options.sessionId, researchId);
|
|
497
|
+
const reasons = getResearcherFailureReasons(this.options.sessionId, researchId);
|
|
498
|
+
const causes = failedIds
|
|
499
|
+
.map((id) => `researcher ${id}: ${reasons[id] ?? 'no usable report'}`)
|
|
500
|
+
.join('; ');
|
|
501
|
+
throw new Error(
|
|
502
|
+
'Research produced no report — no source material was collected' +
|
|
503
|
+
(causes ? `. Causes: ${causes}` : '.')
|
|
504
|
+
);
|
|
490
505
|
}
|
|
491
506
|
}
|
|
492
507
|
|
|
@@ -358,8 +358,9 @@ export class ResearchOrchestrationService implements IResearchOrchestration {
|
|
|
358
358
|
logger.debug(`[ResearchOrchestrationService] Researcher ${id} cancelled (aborted).`);
|
|
359
359
|
} else {
|
|
360
360
|
logger.error(`[ResearchOrchestrationService] Researcher ${id} failed: ${errMsg}`);
|
|
361
|
-
// Record failure for stopping logic
|
|
362
|
-
|
|
361
|
+
// Record failure (with its root cause) for stopping logic and for the
|
|
362
|
+
// zero-report failure surfaced at synthesis time.
|
|
363
|
+
recordResearcherFailure(sessionId, researchId, id, errMsg);
|
|
363
364
|
// Notify observer
|
|
364
365
|
observer?.onResearcherFailure?.(id, errMsg);
|
|
365
366
|
}
|
|
@@ -69,7 +69,7 @@ export async function runResearcher(options: RunResearcherOptions): Promise<void
|
|
|
69
69
|
// use), recording under ctx.sessionId would file this no-links skip under a
|
|
70
70
|
// different id than shouldStopResearch() checks, so the fast-stop guard would
|
|
71
71
|
// under-count exactly the broad-search-failure case it exists to catch.
|
|
72
|
-
recordResearcherFailure(sessionId, researchId, id);
|
|
72
|
+
recordResearcherFailure(sessionId, researchId, id, 'no initial search results or historical links (search produced nothing usable)');
|
|
73
73
|
metrics.increment('researcher_skipped_total', 1, { mode: 'deep', complexity: String(complexity), reason: 'no_initial_links' });
|
|
74
74
|
observer?.onResearcherFailure?.(id, 'No initial search results or historical links available');
|
|
75
75
|
return;
|
|
@@ -26,6 +26,8 @@ const MAX_STEERING_MESSAGE_LENGTH = 2000;
|
|
|
26
26
|
interface PiSessionState {
|
|
27
27
|
/** Map of research run ID → array of failed researcher IDs */
|
|
28
28
|
failures: Map<string, string[]>;
|
|
29
|
+
/** Map of research run ID → (researcher ID → first failure reason) */
|
|
30
|
+
failureReasons: Map<string, Map<string, string>>;
|
|
29
31
|
/** Ordered list of research run IDs for TUI stacking (Index 0 = oldest/bottom-most) */
|
|
30
32
|
order: string[];
|
|
31
33
|
/** Registry of panel states for each research run */
|
|
@@ -70,6 +72,7 @@ function getPiState(piSessionId: string | undefined): PiSessionState {
|
|
|
70
72
|
if (!state) {
|
|
71
73
|
state = {
|
|
72
74
|
failures: new Map(),
|
|
75
|
+
failureReasons: new Map(),
|
|
73
76
|
order: [],
|
|
74
77
|
panels: new Map(),
|
|
75
78
|
aborts: new Map(),
|
|
@@ -392,6 +395,7 @@ export function refreshAllSessions(piSessionId: string | undefined): void {
|
|
|
392
395
|
for (const id of state.order) {
|
|
393
396
|
if (!validIds.includes(id)) {
|
|
394
397
|
state.failures.delete(id);
|
|
398
|
+
state.failureReasons.delete(id);
|
|
395
399
|
state.panels.delete(id);
|
|
396
400
|
}
|
|
397
401
|
}
|
|
@@ -454,6 +458,7 @@ export function startResearchSession(piSessionId: string | undefined, customRese
|
|
|
454
458
|
const sid = normalizeSessionId(piSessionId);
|
|
455
459
|
const state = getPiState(sid);
|
|
456
460
|
state.failures.set(researchId, []);
|
|
461
|
+
state.failureReasons.set(researchId, new Map());
|
|
457
462
|
return researchId;
|
|
458
463
|
}
|
|
459
464
|
|
|
@@ -501,6 +506,7 @@ export function endResearchSession(piSessionId: string | undefined, researchId:
|
|
|
501
506
|
if (!state) return;
|
|
502
507
|
|
|
503
508
|
state.failures.delete(researchId);
|
|
509
|
+
state.failureReasons.delete(researchId);
|
|
504
510
|
state.panels.delete(researchId);
|
|
505
511
|
state.aborts.delete(researchId);
|
|
506
512
|
|
|
@@ -525,6 +531,7 @@ export function endResearchSession(piSessionId: string | undefined, researchId:
|
|
|
525
531
|
} else {
|
|
526
532
|
// Clear research-specific state but preserve the session and steering
|
|
527
533
|
state.failures.clear();
|
|
534
|
+
state.failureReasons.clear();
|
|
528
535
|
state.aborts.clear();
|
|
529
536
|
}
|
|
530
537
|
}
|
|
@@ -563,12 +570,28 @@ export function isBottomMostSession(piSessionId: string | undefined, researchId:
|
|
|
563
570
|
/**
|
|
564
571
|
* Record a researcher failure
|
|
565
572
|
*/
|
|
566
|
-
export function recordResearcherFailure(piSessionId: string | undefined, researchId: string, researcherId: string): void {
|
|
573
|
+
export function recordResearcherFailure(piSessionId: string | undefined, researchId: string, researcherId: string, reason?: string): void {
|
|
567
574
|
const sid = normalizeSessionId(piSessionId);
|
|
568
575
|
const state = getPiState(sid);
|
|
569
576
|
const failures = state.failures.get(researchId) || [];
|
|
570
577
|
failures.push(researcherId);
|
|
571
578
|
state.failures.set(researchId, failures);
|
|
579
|
+
if (reason) {
|
|
580
|
+
const reasons = state.failureReasons.get(researchId) ?? new Map<string, string>();
|
|
581
|
+
// Keep the FIRST reason per researcher — the root cause; retries repeat it.
|
|
582
|
+
if (!reasons.has(researcherId)) reasons.set(researcherId, reason);
|
|
583
|
+
state.failureReasons.set(researchId, reasons);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Get the recorded failure reason per failed researcher (first failure wins).
|
|
589
|
+
*/
|
|
590
|
+
export function getResearcherFailureReasons(piSessionId: string | undefined, researchId: string): Record<string, string> {
|
|
591
|
+
const sid = normalizeSessionId(piSessionId);
|
|
592
|
+
const state = getPiState(sid);
|
|
593
|
+
const reasons = state.failureReasons.get(researchId);
|
|
594
|
+
return reasons ? Object.fromEntries(reasons) : {};
|
|
572
595
|
}
|
|
573
596
|
|
|
574
597
|
/**
|
|
@@ -610,9 +633,18 @@ export function getResearchStopMessage(piSessionId: string | undefined, research
|
|
|
610
633
|
// search engine blocking" here mis-sends the user to debug their network; the
|
|
611
634
|
// readiness probes do not even run a search, so a failure is not evidence of bot
|
|
612
635
|
// blocking.
|
|
636
|
+
// Lead with the RECORDED errors when we have them — the actual root cause
|
|
637
|
+
// (e.g. a provider 429) beats generic guidance every time.
|
|
638
|
+
const state = getPiState(sid);
|
|
639
|
+
const reasons = state.failureReasons.get(researchId);
|
|
640
|
+
const recorded = reasons && reasons.size > 0
|
|
641
|
+
? ['Recorded errors:', ...[...reasons].map(([id, r]) => `• researcher ${id}: ${r}`), '']
|
|
642
|
+
: [];
|
|
643
|
+
|
|
613
644
|
return [
|
|
614
645
|
`Research stopped: ${count} researcher(s) did not return a usable report: ${failed.join(', ')}.`,
|
|
615
646
|
'',
|
|
647
|
+
...recorded,
|
|
616
648
|
'Most likely cause — the research model could not produce grounded results:',
|
|
617
649
|
'• Try a more capable research model. Very small or "thinking-only" models often',
|
|
618
650
|
' emit no final text after their tool calls, which fails every researcher.',
|
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,31 @@ 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
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
// An EXPLICIT model option must govern the entire run — coordinator (via
|
|
240
|
+
// ctx.model) AND researchers/synthesis (which resolve through
|
|
241
|
+
// RESEARCH_MODEL, where config would otherwise outrank the option and split
|
|
242
|
+
// the run across two models, with the report metadata naming the wrong one).
|
|
243
|
+
// Pin the config to the explicit choice. No-op when the option was itself
|
|
244
|
+
// seeded from the config (the CLI does that).
|
|
245
|
+
if (options.model) {
|
|
246
|
+
const explicit = typeof options.model === 'string'
|
|
247
|
+
? options.model
|
|
248
|
+
: `${(options.model as Model<any>).provider}/${(options.model as Model<any>).id}`;
|
|
249
|
+
globalConfig = { ...globalConfig, RESEARCH_MODEL: explicit };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Infer provider from the effective model if not explicitly provided. The
|
|
253
|
+
// effective model is the explicit option first, else the configured
|
|
254
|
+
// RESEARCH_MODEL — the same precedence the resolve below uses.
|
|
255
|
+
const effectiveModel = options.model ?? globalConfig.RESEARCH_MODEL;
|
|
256
|
+
if (!parsedProvider && effectiveModel) {
|
|
257
|
+
if (typeof effectiveModel === 'string') {
|
|
258
|
+
const slashIdx = effectiveModel.indexOf('/');
|
|
239
259
|
if (slashIdx > 0) {
|
|
240
|
-
parsedProvider =
|
|
260
|
+
parsedProvider = effectiveModel.slice(0, slashIdx);
|
|
241
261
|
}
|
|
242
|
-
} else if ((
|
|
243
|
-
parsedProvider = (
|
|
262
|
+
} else if ((effectiveModel as any).provider) {
|
|
263
|
+
parsedProvider = (effectiveModel as any).provider;
|
|
244
264
|
}
|
|
245
265
|
}
|
|
246
266
|
|
|
@@ -252,8 +272,23 @@ async function _doInit(options: ResearchSDKOptions = {}): Promise<void> {
|
|
|
252
272
|
globalRegistry = sharedBuildModelRegistry(globalApiKey, parsedProvider);
|
|
253
273
|
|
|
254
274
|
try {
|
|
255
|
-
// Resolve model
|
|
256
|
-
|
|
275
|
+
// Resolve the model. Precedence: explicit option (a Model object is used
|
|
276
|
+
// as-is; it need not exist in the registry) → configured RESEARCH_MODEL →
|
|
277
|
+
// provider default → registry fallback. Seeding the resolver with the
|
|
278
|
+
// configured model is what keeps the coordinator (which runs on ctx.model)
|
|
279
|
+
// on the SAME model as the researchers/synthesis (which read
|
|
280
|
+
// RESEARCH_MODEL themselves) — without it a configured model would govern
|
|
281
|
+
// only the sub-agents and planning would silently run on the fallback.
|
|
282
|
+
if (effectiveModel && typeof effectiveModel === 'object' && (effectiveModel as any).id) {
|
|
283
|
+
globalModel = effectiveModel as Model<any>;
|
|
284
|
+
} else {
|
|
285
|
+
globalModel = resolveModel(
|
|
286
|
+
globalRegistry,
|
|
287
|
+
typeof effectiveModel === 'string' ? effectiveModel : undefined,
|
|
288
|
+
parsedProvider,
|
|
289
|
+
globalApiKey
|
|
290
|
+
);
|
|
291
|
+
}
|
|
257
292
|
|
|
258
293
|
// Use the global container to ensure internal services can resolve dependencies
|
|
259
294
|
globalContainer = getServiceContainer();
|