@lincoln504/pi-research 0.1.13 → 1.0.0
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/LICENSE +1 -1
- package/README.md +64 -132
- package/dist/cli.mjs +23211 -0
- package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
- package/dist/prompts/research-knowledge-search-extractor.md +41 -0
- package/dist/prompts/research-tool-usage.md +92 -0
- package/dist/prompts/researcher.md +89 -0
- package/dist/prompts/system-coordinator.md +75 -0
- package/dist/prompts/system-lead-evaluator.md +118 -0
- package/dist/thread-worker.mjs +2611 -0
- package/dist/webgpu-probe.mjs +80 -0
- package/docs/AGENT-SKILL.md +102 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +213 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +113 -0
- package/docs/THIRD-PARTY-NOTICES.md +64 -0
- package/package.json +102 -36
- package/scripts/build.cjs +140 -0
- package/scripts/cleanup.cjs +149 -0
- package/scripts/prepare.cjs +28 -0
- package/scripts/setup.cjs +134 -0
- package/skills/pi-research/README.md +90 -0
- package/skills/pi-research/SKILL.md +102 -0
- package/skills/pi-research/references/configuration.md +77 -0
- package/skills/pi-research/scripts/run.mjs +237 -0
- package/src/cleanup/research-cleanup.ts +140 -0
- package/src/cli.ts +1044 -0
- package/src/config.ts +1032 -155
- package/src/constants.ts +115 -84
- package/src/core/health-check-service.ts +265 -0
- package/src/core/interfaces/health-check-interfaces.ts +50 -0
- package/src/core/interfaces/knowledge-interfaces.ts +190 -0
- package/src/core/interfaces/observer-interfaces.ts +67 -0
- package/src/core/interfaces/orchestration-interfaces.ts +86 -0
- package/src/core/interfaces/planning-interfaces.ts +100 -0
- package/src/core/interfaces/process-interfaces.ts +37 -0
- package/src/core/interfaces/research-plan-types.ts +52 -0
- package/src/core/interfaces/scheduler-interfaces.ts +91 -0
- package/src/core/interfaces/service-names.ts +39 -0
- package/src/core/interfaces/state-manager-interfaces.ts +29 -0
- package/src/core/interfaces/steering-interfaces.ts +31 -0
- package/src/core/llm/agentic-repair.ts +169 -0
- package/src/core/llm/inject-date.ts +52 -0
- package/src/core/llm/llm-timeout.ts +64 -0
- package/src/core/llm/llm-utils.ts +86 -0
- package/src/core/llm/model-registry-factory.ts +287 -0
- package/src/core/llm/pi-ai-completion.ts +29 -0
- package/src/core/llm/prompts.ts +50 -0
- package/src/core/llm/research-model-resolver.ts +91 -0
- package/src/core/planning-constants.ts +13 -0
- package/src/core/planning-service.ts +649 -0
- package/src/core/planning-utils.ts +299 -0
- package/src/core/scheduler-factory.ts +48 -0
- package/src/core/scheduler-service.ts +123 -0
- package/src/core/service-initialization.ts +194 -0
- package/src/core/service-interfaces.ts +48 -0
- package/src/core/service-registry.ts +659 -0
- package/src/healthcheck/index.ts +271 -265
- package/src/healthcheck/registry.ts +130 -0
- package/src/index.ts +639 -50
- package/src/infrastructure/browser/browser-cleanup.ts +309 -0
- package/src/infrastructure/browser/browser-client.ts +199 -0
- package/src/infrastructure/browser/browser-error-utils.ts +160 -0
- package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
- package/src/infrastructure/browser/browser-server.ts +174 -0
- package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
- package/src/infrastructure/browser/cleanup-utils.ts +155 -0
- package/src/infrastructure/browser/client-agent.ts +31 -0
- package/src/infrastructure/browser/config.ts +288 -0
- package/src/infrastructure/browser/ensure-browser.ts +205 -0
- package/src/infrastructure/browser/index.ts +37 -0
- package/src/infrastructure/browser/priority-task-queue.ts +226 -0
- package/src/infrastructure/browser/scheduler-factory.ts +354 -0
- package/src/infrastructure/browser/task-execution-service.ts +209 -0
- package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
- package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
- package/src/infrastructure/browser/thread-worker-types.ts +25 -0
- package/src/infrastructure/browser/thread-worker.mjs +2611 -0
- package/src/infrastructure/browser/thread-worker.ts +168 -0
- package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
- package/src/infrastructure/embedding/embedding-client.ts +205 -0
- package/src/infrastructure/embedding/embedding-factory.ts +324 -0
- package/src/infrastructure/embedding/embedding-server.ts +535 -0
- package/src/infrastructure/file-lock-service.ts +592 -0
- package/src/infrastructure/gpu-resource-service.ts +191 -0
- package/src/infrastructure/knowledge-store-service.ts +475 -0
- package/src/infrastructure/metrics-service.ts +129 -0
- package/src/infrastructure/process-lifecycle-service.ts +253 -0
- package/src/infrastructure/scheduler-factory-service.ts +58 -0
- package/src/infrastructure/service-initialization.ts +292 -0
- package/src/infrastructure/state/state-backup-manager.ts +290 -0
- package/src/infrastructure/state/state-browser-api.ts +62 -0
- package/src/infrastructure/state/state-browser-manager.ts +70 -0
- package/src/infrastructure/state/state-manager-service.ts +264 -0
- package/src/infrastructure/state/state-manager.ts +547 -0
- package/src/infrastructure/state/state-metrics.ts +92 -0
- package/src/infrastructure/state/state-migration.ts +103 -0
- package/src/infrastructure/state/state-path-configuration.ts +119 -0
- package/src/infrastructure/state/state-session-api.ts +94 -0
- package/src/infrastructure/state/state-session-manager.ts +127 -0
- package/src/infrastructure/state/state-validator.ts +93 -0
- package/src/infrastructure/types/state-types.ts +85 -0
- package/src/knowledge/chunker.ts +157 -0
- package/src/knowledge/embedder-init.ts +413 -0
- package/src/knowledge/embedder-utils.ts +296 -0
- package/src/knowledge/embedder.ts +592 -0
- package/src/knowledge/index.ts +126 -0
- package/src/knowledge/lancedb-loader.ts +35 -0
- package/src/knowledge/migration.ts +17 -0
- package/src/knowledge/model-config.ts +132 -0
- package/src/knowledge/onnx-env.ts +48 -0
- package/src/knowledge/store-operations.ts +291 -0
- package/src/knowledge/store-schema.ts +79 -0
- package/src/knowledge/store.ts +1251 -0
- package/src/knowledge/transformers-loader.ts +31 -0
- package/src/knowledge/webgpu-probe.mjs +80 -0
- package/src/knowledge/webgpu-viability.ts +255 -0
- package/src/knowledge/writer-queue.ts +252 -0
- package/src/logger.ts +316 -122
- package/src/observers/research-observer-impl.ts +459 -0
- package/src/orchestration/deep-research-orchestrator.ts +524 -773
- package/src/orchestration/deep-research-types.ts +47 -35
- package/src/orchestration/headless-observer.ts +114 -0
- package/src/orchestration/orchestration-types.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +463 -0
- package/src/orchestration/research-orchestration-service.ts +549 -0
- package/src/orchestration/research-session-service.ts +178 -0
- package/src/orchestration/research-synthesis-service.ts +348 -0
- package/src/orchestration/researcher-executor.ts +394 -0
- package/src/orchestration/researcher.ts +115 -42
- package/src/orchestration/service-initialization.ts +109 -0
- package/src/orchestration/session-context.ts +7 -3
- package/src/orchestration/session-state.ts +672 -0
- package/src/prompts/research-knowledge-relevance-triage.md +35 -0
- package/src/prompts/research-knowledge-search-extractor.md +41 -0
- package/src/prompts/research-tool-usage.md +63 -16
- package/src/prompts/researcher.md +59 -136
- package/src/prompts/system-coordinator.md +64 -13
- package/src/prompts/system-lead-evaluator.md +82 -189
- package/src/research-config.ts +872 -0
- package/src/sdk.ts +847 -0
- package/src/security/cisa-kev.ts +72 -9
- package/src/security/cvss.ts +92 -0
- package/src/security/ecosystem.ts +45 -0
- package/src/security/github-advisories.ts +168 -428
- package/src/security/github-advisory-types.ts +282 -0
- package/src/security/index.ts +63 -177
- package/src/security/nvd-client.ts +550 -0
- package/src/security/nvd-types.ts +179 -0
- package/src/security/osv-client.ts +246 -0
- package/src/security/osv-types.ts +285 -0
- package/src/security/types.ts +67 -53
- package/src/skill-install/skill-installer.ts +519 -0
- package/src/stackexchange/index.ts +81 -27
- package/src/stackexchange/output/compact.ts +7 -6
- package/src/stackexchange/output/table.ts +11 -7
- package/src/stackexchange/queries.ts +0 -10
- package/src/stackexchange/rest-client.ts +141 -66
- package/src/stackexchange/types.ts +0 -17
- package/src/tool.ts +15 -423
- package/src/tools/grep.ts +32 -310
- package/src/tools/health-tool-definition.ts +101 -0
- package/src/tools/index.ts +38 -9
- package/src/tools/research-knowledge-search.ts +974 -0
- package/src/tools/research-knowledge-types.ts +77 -0
- package/src/tools/research-tool-definition.ts +494 -0
- package/src/tools/scrape.ts +286 -388
- package/src/tools/search.ts +98 -153
- package/src/tools/security.ts +107 -80
- package/src/tools/stackexchange.ts +58 -35
- package/src/tools/youtube-transcript.ts +163 -0
- package/src/tui/knowledge-search-panel.ts +86 -0
- package/src/tui/pulse.ts +86 -0
- package/src/tui/research-health.ts +124 -0
- package/src/tui/research-panel-color-utils.ts +340 -0
- package/src/tui/research-panel-format.ts +54 -0
- package/src/tui/research-panel-state.ts +404 -0
- package/src/tui/research-panel-wave.ts +280 -0
- package/src/tui/research-panel.ts +339 -435
- package/src/tui/research-tui-manager.ts +233 -0
- package/src/tui/tui-controller.ts +137 -0
- package/src/types/extension-context.ts +18 -49
- package/src/types/index.ts +61 -0
- package/src/types/llm.ts +68 -29
- package/src/types/research-panel-types.ts +61 -0
- package/src/utils/circuit-breaker.ts +146 -0
- package/src/utils/citation-utils.ts +97 -0
- package/src/utils/disk-space-checker.ts +55 -0
- package/src/utils/error-tracker.ts +265 -0
- package/src/utils/host-config.ts +45 -0
- package/src/utils/input-validation.ts +8 -10
- package/src/utils/json-utils.ts +150 -16
- package/src/utils/llm-usage.ts +54 -0
- package/src/utils/log-rotation.ts +125 -0
- package/src/utils/log-utils.ts +159 -0
- package/src/utils/make-resource-loader.ts +3 -1
- package/src/utils/metrics-summary.ts +459 -0
- package/src/utils/metrics.ts +302 -0
- package/src/utils/research-export.ts +28 -12
- package/src/utils/safe-unref.ts +31 -0
- package/src/utils/shared-links.ts +195 -90
- package/src/utils/shutdown-manager.ts +65 -0
- package/src/utils/stdio-capture.ts +407 -0
- package/src/utils/text-utils.ts +258 -11
- package/src/utils/tool-usage-tracker.ts +83 -16
- package/src/utils/url-utils.ts +255 -0
- package/src/utils/user-agent.ts +28 -0
- package/src/web-research/browser-search.ts +179 -0
- package/src/web-research/retry-utils.ts +173 -77
- package/src/web-research/scraper-types.ts +72 -0
- package/src/web-research/scraper-utils.ts +434 -0
- package/src/web-research/search.ts +78 -218
- package/src/web-research/types.ts +9 -24
- package/src/web-research/utils.ts +2 -143
- package/src/web-research/web-scraper.ts +477 -0
- package/src/youtube/index.ts +154 -0
- package/src/youtube/potoken.ts +259 -0
- package/src/youtube/transcript-client.ts +390 -0
- package/src/youtube/video-id.ts +118 -0
- package/config/searxng/default-settings.yml +0 -189
- package/config/searxng/limiter.toml +0 -34
- package/config/tooling/eslint.config.js +0 -73
- package/config/tooling/vitest.config.integration.ts +0 -17
- package/config/tooling/vitest.config.ts +0 -40
- package/config/tooling/vitest.config.unit.ts +0 -12
- package/docs/assets/README-banner.jpg +0 -0
- package/scripts/setup.js +0 -212
- package/src/infrastructure/searxng-lifecycle.ts +0 -644
- package/src/infrastructure/searxng-manager.ts +0 -1183
- package/src/infrastructure/state-manager.ts +0 -918
- package/src/orchestration/deep-research-reducer.ts +0 -129
- package/src/orchestration/id-utils.ts +0 -63
- package/src/security/nvd.ts +0 -532
- package/src/security/osv.ts +0 -487
- package/src/stackexchange/cache.ts +0 -111
- package/src/stackexchange/output/index.ts +0 -7
- package/src/stackexchange/output/json.ts +0 -21
- package/src/tui/searxng-status.ts +0 -80
- package/src/utils/inject-date.ts +0 -29
- package/src/utils/research-error.ts +0 -205
- package/src/utils/searxng-config.ts +0 -94
- package/src/utils/session-state.ts +0 -295
- package/src/web-research/dependencies.ts +0 -20
- package/src/web-research/scrapers.ts +0 -935
- package/tsconfig.json +0 -40
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Model Registry Factory
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for creating a ModelRegistry with the correct auth.
|
|
5
|
+
* Used by the SDK entry point (src/sdk.ts), which also backs the bundled
|
|
6
|
+
* `pi-research` CLI and the cross-harness agent skill.
|
|
7
|
+
*
|
|
8
|
+
* Previously this logic was duplicated (#29) — any bug fix now updates both paths.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { AuthStorage, ModelRegistry, getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
12
|
+
import type { Model } from '@earendil-works/pi-ai';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import { logger } from '../../logger.ts';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A structural subset of ModelRegistry that the research resolvers actually use.
|
|
19
|
+
* The registry object can arrive from two places — our own factory and the pi
|
|
20
|
+
* extension host (ctx.modelRegistry) — and a version skew
|
|
21
|
+
* between the pi-coding-agent we build against and the one the host injects has
|
|
22
|
+
* historically produced "modelRegistry.getAvailable is not a function" crashes
|
|
23
|
+
* mid-research. Treating the registry structurally (and feature-detecting each
|
|
24
|
+
* method) keeps researchers alive across that skew instead of throwing a raw
|
|
25
|
+
* TypeError that aborts the run.
|
|
26
|
+
*/
|
|
27
|
+
export interface ModelRegistryLike {
|
|
28
|
+
getAll?: () => Model<any>[];
|
|
29
|
+
getAvailable?: () => Model<any>[];
|
|
30
|
+
find?: (provider: string, id: string) => Model<any> | undefined;
|
|
31
|
+
getApiKeyAndHeaders?: (model: Model<any>) => Promise<AuthResultLike>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Structural result of {@link ModelRegistryLike.getApiKeyAndHeaders}. Mirrors the
|
|
36
|
+
* pi-coding-agent `ApiKeyAndHeaders` discriminated union without importing it, so
|
|
37
|
+
* a host skew on the concrete type can't break our build.
|
|
38
|
+
*/
|
|
39
|
+
export type AuthResultLike =
|
|
40
|
+
| { ok: true; apiKey?: string; headers?: Record<string, string> }
|
|
41
|
+
| { ok: false; error: string };
|
|
42
|
+
|
|
43
|
+
/** Safe `getAll()` — returns [] if the host registry lacks the method. */
|
|
44
|
+
export function safeGetAll(registry: ModelRegistryLike | null | undefined): Model<any>[] {
|
|
45
|
+
if (registry && typeof registry.getAll === 'function') {
|
|
46
|
+
try { return registry.getAll() ?? []; } catch (err) {
|
|
47
|
+
logger.warn('[ModelRegistry] getAll() threw; treating as empty:', err);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Safe `getAvailable()` — the authed/usable subset. Falls back to getAll() when
|
|
55
|
+
* the host registry does not expose getAvailable() (older/newer host skew), so a
|
|
56
|
+
* missing method degrades to "treat every known model as available" rather than
|
|
57
|
+
* crashing the researcher.
|
|
58
|
+
*/
|
|
59
|
+
export function safeGetAvailable(registry: ModelRegistryLike | null | undefined): Model<any>[] {
|
|
60
|
+
if (registry && typeof registry.getAvailable === 'function') {
|
|
61
|
+
try { return registry.getAvailable() ?? []; } catch (err) {
|
|
62
|
+
logger.warn('[ModelRegistry] getAvailable() threw; falling back to getAll():', err);
|
|
63
|
+
}
|
|
64
|
+
} else if (registry) {
|
|
65
|
+
logger.warn('[ModelRegistry] host registry has no getAvailable(); falling back to getAll(). Possible pi-coding-agent version skew.');
|
|
66
|
+
}
|
|
67
|
+
return safeGetAll(registry);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Safe `getApiKeyAndHeaders()` — resolves LLM credentials for a model. The
|
|
72
|
+
* registry can arrive from the pi extension host (ctx.modelRegistry), so a
|
|
73
|
+
* version skew that renamed/removed this method must NOT throw a raw TypeError
|
|
74
|
+
* mid-research (the same failure class {@link safeGetAvailable} guards against).
|
|
75
|
+
* A missing method or a throw degrades to a structured `{ ok: false }` that every
|
|
76
|
+
* caller already branches on, instead of aborting the run.
|
|
77
|
+
*/
|
|
78
|
+
export async function safeGetApiKeyAndHeaders(
|
|
79
|
+
registry: ModelRegistryLike | null | undefined,
|
|
80
|
+
model: Model<any>,
|
|
81
|
+
): Promise<AuthResultLike> {
|
|
82
|
+
if (!registry || typeof registry.getApiKeyAndHeaders !== 'function') {
|
|
83
|
+
logger.warn('[ModelRegistry] host registry has no getApiKeyAndHeaders(); cannot resolve LLM credentials. Possible pi-coding-agent version skew.');
|
|
84
|
+
return { ok: false, error: 'model registry does not expose getApiKeyAndHeaders() (pi-coding-agent version skew)' };
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return await registry.getApiKeyAndHeaders(model);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
90
|
+
logger.warn('[ModelRegistry] getApiKeyAndHeaders() threw; treating as auth failure:', msg);
|
|
91
|
+
return { ok: false, error: msg };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Headers commonly required by third-party LLM providers
|
|
97
|
+
*/
|
|
98
|
+
function buildHeaders(provider?: string): Record<string, string> {
|
|
99
|
+
if (provider === 'openrouter') return { 'HTTP-Referer': 'https://pi.ai', 'X-Title': 'pi-research' };
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create a ModelRegistry with the given credentials.
|
|
105
|
+
*
|
|
106
|
+
* @param apiKey - Optional explicit API key
|
|
107
|
+
* @param provider - Optional provider name to key the API key under
|
|
108
|
+
* @returns A configured ModelRegistry
|
|
109
|
+
*/
|
|
110
|
+
export function buildModelRegistry(apiKey?: string, provider?: string): ModelRegistry {
|
|
111
|
+
const agentDir = getAgentDir();
|
|
112
|
+
const modelsJsonPath = path.join(agentDir, 'models.json');
|
|
113
|
+
const authPath = path.join(agentDir, 'auth.json');
|
|
114
|
+
|
|
115
|
+
if (apiKey && provider) {
|
|
116
|
+
// Explicit key: seed InMemory storage under the correct provider name.
|
|
117
|
+
const authStorage = AuthStorage.inMemory({
|
|
118
|
+
[provider]: { type: 'api_key', key: apiKey },
|
|
119
|
+
});
|
|
120
|
+
return ModelRegistry.create(
|
|
121
|
+
authStorage,
|
|
122
|
+
fs.existsSync(modelsJsonPath) ? modelsJsonPath : undefined,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// No explicit key: use the user's pi auth storage and model list if available.
|
|
127
|
+
// ModelRegistry.create() reads embedded apiKeys from models.json providers
|
|
128
|
+
// directly (via providerRequestConfigs), so glm-coding and other custom
|
|
129
|
+
// providers with embedded keys are visible to hasConfiguredAuth() without
|
|
130
|
+
// needing to duplicate them into AuthStorage.
|
|
131
|
+
if (fs.existsSync(authPath)) {
|
|
132
|
+
const authStorage = AuthStorage.create(authPath);
|
|
133
|
+
return ModelRegistry.create(
|
|
134
|
+
authStorage,
|
|
135
|
+
fs.existsSync(modelsJsonPath) ? modelsJsonPath : undefined,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Last resort: in-memory with nothing — will fail at LLM call time with actionable error
|
|
140
|
+
return ModelRegistry.inMemory(AuthStorage.inMemory());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Construct a minimal Model object directly from provider + modelId + apiKey.
|
|
145
|
+
*
|
|
146
|
+
* Used when the user provides credentials but has no pi config directory
|
|
147
|
+
* (e.g., SDK / CLI / agent-skill users without pi installed).
|
|
148
|
+
* Bypasses the full ModelRegistry which would fail without models.json.
|
|
149
|
+
*/
|
|
150
|
+
export function constructMinimalModel(provider: string, modelId: string, _apiKey: string): Model<any> {
|
|
151
|
+
return {
|
|
152
|
+
provider,
|
|
153
|
+
id: modelId,
|
|
154
|
+
name: modelId,
|
|
155
|
+
api: provider === 'openai' ? 'openai-completions' : (provider + '-conversations' as any),
|
|
156
|
+
baseUrl: '', // Provider-specific base URLs are handled by pi-ai internal registry
|
|
157
|
+
reasoning: false,
|
|
158
|
+
input: ['text'],
|
|
159
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
160
|
+
contextWindow: 128_000,
|
|
161
|
+
maxTokens: 32_768,
|
|
162
|
+
headers: buildHeaders(provider),
|
|
163
|
+
} as unknown as Model<any>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resolve a Model instance from a registry based on an optional model string or provider.
|
|
168
|
+
*
|
|
169
|
+
* @param registry - The ModelRegistry to search in
|
|
170
|
+
* @param modelSpec - Optional model string ("provider/modelId" or "modelId")
|
|
171
|
+
* @param provider - Optional provider name to use as fallback
|
|
172
|
+
* @param apiKey - Optional API key to construct a fallback model when registry is empty
|
|
173
|
+
* @returns The resolved Model instance
|
|
174
|
+
* @throws Error if no model can be resolved
|
|
175
|
+
*/
|
|
176
|
+
export function resolveModel(registry: ModelRegistry, modelSpec?: string, provider?: string, apiKey?: string): Model<any> {
|
|
177
|
+
// 1. Explicit model string: "provider/modelId" or "modelId"
|
|
178
|
+
if (modelSpec) {
|
|
179
|
+
const slashIdx = modelSpec.indexOf('/');
|
|
180
|
+
if (slashIdx > 0) {
|
|
181
|
+
const prov = modelSpec.slice(0, slashIdx);
|
|
182
|
+
const modelId = modelSpec.slice(slashIdx + 1);
|
|
183
|
+
const found = typeof registry.find === 'function'
|
|
184
|
+
? registry.find(prov, modelId)
|
|
185
|
+
: safeGetAll(registry).find(m => m.provider === prov && m.id === modelId);
|
|
186
|
+
if (found) return found;
|
|
187
|
+
|
|
188
|
+
// Fallback for SDK / CLI / skill users without pi config: construct model from credentials
|
|
189
|
+
if (apiKey && provider) {
|
|
190
|
+
return constructMinimalModel(prov, modelId, apiKey);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
throw new Error(`Model "${modelSpec}" not found in pi's configured model registry. Check ${path.join(getAgentDir(), 'models.json')}.`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Validate format: must contain a slash OR be found as a bare model ID.
|
|
197
|
+
// A bare id can exist under several providers (a user-configured authed one
|
|
198
|
+
// plus pi's built-in unauthed one); prefer an authed provider so we don't
|
|
199
|
+
// resolve to a keyless entry that fails at the first call.
|
|
200
|
+
const allModels = safeGetAll(registry);
|
|
201
|
+
const sameId = allModels.filter(m => m.id === modelSpec);
|
|
202
|
+
const authedKeys = new Set(safeGetAvailable(registry).map(m => `${m.provider}/${m.id}`));
|
|
203
|
+
const found = sameId.find(m => authedKeys.has(`${m.provider}/${m.id}`)) ?? sameId[0];
|
|
204
|
+
if (found) return found;
|
|
205
|
+
|
|
206
|
+
throw new Error(`Invalid model string "${modelSpec}". Expected "provider/id" e.g. "openai/gpt-4o".`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 2. Provider-only: pick first available model from that provider
|
|
210
|
+
if (provider) {
|
|
211
|
+
const allModels = safeGetAll(registry);
|
|
212
|
+
const found = allModels.find(m => m.provider === provider);
|
|
213
|
+
if (found) return found;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 3. First available model — prefer providers in the user's models.json order.
|
|
217
|
+
// pi-ai loads built-in providers in a fixed catalog order (openrouter is 26th).
|
|
218
|
+
// User-configured providers in models.json (e.g. glm-coding) are appended after
|
|
219
|
+
// the built-ins, so getAvailable()[0] would otherwise always pick a built-in
|
|
220
|
+
// provider even when the user has a preferred custom one. Walk models.json provider
|
|
221
|
+
// order first to respect the user's explicit configuration.
|
|
222
|
+
const available = safeGetAvailable(registry);
|
|
223
|
+
if (available.length > 0) {
|
|
224
|
+
return pickPreferredAvailable(available, readModelsJsonProviderOrder())!;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 4. Any model at all
|
|
228
|
+
const all = safeGetAll(registry);
|
|
229
|
+
if (all.length > 0) return all[0]!;
|
|
230
|
+
|
|
231
|
+
// 5. Fallback: construct minimal model from credentials when registry is empty
|
|
232
|
+
// (Re-checking modelSpec with slash check is redundant but kept for logic safety)
|
|
233
|
+
if (apiKey && provider && modelSpec) {
|
|
234
|
+
const slashIdx = modelSpec.indexOf('/');
|
|
235
|
+
if (slashIdx > 0) {
|
|
236
|
+
return constructMinimalModel(
|
|
237
|
+
modelSpec.slice(0, slashIdx),
|
|
238
|
+
modelSpec.slice(slashIdx + 1),
|
|
239
|
+
apiKey,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
// No slash: use the explicit provider and the modelSpec as the ID
|
|
243
|
+
return constructMinimalModel(provider, modelSpec, apiKey);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
throw new Error(
|
|
247
|
+
`No LLM model available. Please configure your model registry (${path.join(getAgentDir(), 'models.json')}) or provide an explicit apiKey.`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Read the user's provider order from ~/.pi/agent/models.json.
|
|
253
|
+
* Returns provider keys in document (insertion) order, or [] when the file is
|
|
254
|
+
* absent or malformed. Kept separate from {@link pickPreferredAvailable} so the
|
|
255
|
+
* selection logic stays pure and unit-testable without touching the filesystem.
|
|
256
|
+
*/
|
|
257
|
+
export function readModelsJsonProviderOrder(): string[] {
|
|
258
|
+
const modelsJsonPath = path.join(getAgentDir(), 'models.json');
|
|
259
|
+
if (!fs.existsSync(modelsJsonPath)) return [];
|
|
260
|
+
try {
|
|
261
|
+
const raw = JSON.parse(fs.readFileSync(modelsJsonPath, 'utf-8'));
|
|
262
|
+
return Object.keys(raw?.providers ?? {});
|
|
263
|
+
} catch (err) {
|
|
264
|
+
// Malformed models.json only affects provider-preference ordering (benign), but log
|
|
265
|
+
// a warn to match every other registry fault in this file rather than swallowing it.
|
|
266
|
+
logger.warn(`[ModelRegistry] Could not read provider order from models.json: ${err}`);
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Pick the first available model whose provider appears earliest in the user's
|
|
273
|
+
* configured provider order; fall back to the first available model when no
|
|
274
|
+
* provider in `providerOrder` is available. Pure — no filesystem access — so it
|
|
275
|
+
* is the single source of truth for "first available" fallback ordering shared
|
|
276
|
+
* by both {@link resolveModel} and the research-model resolver.
|
|
277
|
+
*/
|
|
278
|
+
export function pickPreferredAvailable<T extends { provider: string }>(
|
|
279
|
+
available: T[],
|
|
280
|
+
providerOrder: string[],
|
|
281
|
+
): T | undefined {
|
|
282
|
+
for (const prov of providerOrder) {
|
|
283
|
+
const match = available.find(m => m.provider === prov);
|
|
284
|
+
if (match) return match;
|
|
285
|
+
}
|
|
286
|
+
return available[0];
|
|
287
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single integration point for pi-ai's stateless completion primitive.
|
|
3
|
+
*
|
|
4
|
+
* pi-research resolves a `Model` externally (via pi's `ModelRegistry` / models.json,
|
|
5
|
+
* which includes CUSTOM providers such as `glm-coding`, `cerebras`, `zai`) and then
|
|
6
|
+
* makes a stateless, one-shot completion with explicit per-call `apiKey`/`headers`,
|
|
7
|
+
* outside any AgentSession. The pi-ai API that fits this today is the standalone
|
|
8
|
+
* `completeSimple` from `@earendil-works/pi-ai/compat`.
|
|
9
|
+
*
|
|
10
|
+
* Why not the modern instance API (`createModels()` / `builtinModels()`):
|
|
11
|
+
* - `Models.completeSimple` dispatches by `model.provider` and throws
|
|
12
|
+
* `Unknown provider: <id>` for any provider not registered on that instance
|
|
13
|
+
* (`@earendil-works/pi-ai` models.ts). `builtinModels()` registers only the
|
|
14
|
+
* built-in providers, so it would reject every custom provider we use.
|
|
15
|
+
* - The standalone `completeSimple` instead dispatches custom models by
|
|
16
|
+
* `model.api` (e.g. `openai-completions`) with pi-ai's `detectCompat` fallback —
|
|
17
|
+
* the exact behavior pi RESTORED in 0.80.2 (the `streamSimpleOpenAICompletions`
|
|
18
|
+
* aliases and `detectCompat` fallback) precisely because removing it broke
|
|
19
|
+
* custom OpenAI-compatible models.
|
|
20
|
+
* - pi's own `ModelRegistry` keeps its configured `Models` private and exposes no
|
|
21
|
+
* `completeSimple`/`stream`, so there is no public modern instance that already
|
|
22
|
+
* carries the custom providers either.
|
|
23
|
+
*
|
|
24
|
+
* This is therefore the correct, actively-maintained API for the use case — not
|
|
25
|
+
* legacy cruft. pi-ai documents that the `/compat` entrypoint will be removed "in a
|
|
26
|
+
* future release with a migration guide"; when that lands, switch THIS module to the
|
|
27
|
+
* new provider-factory API and every caller follows automatically.
|
|
28
|
+
*/
|
|
29
|
+
export { completeSimple } from '@earendil-works/pi-ai/compat';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Loading Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared helper for loading prompt template files.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { logger } from '../../logger.ts';
|
|
11
|
+
|
|
12
|
+
// Get the directory where this file is located
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const CORE_LLM_DIR = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// When running from the source tree: src/core/llm/ → ../../prompts = src/prompts/
|
|
17
|
+
// When running from the bundled CLI (dist/cli.mjs): dist/ → ./prompts = dist/prompts/
|
|
18
|
+
// (dist/prompts/ is copied next to the bundle at build time by build:cli)
|
|
19
|
+
const PROMPT_CANDIDATES = [
|
|
20
|
+
join(CORE_LLM_DIR, '../../prompts'), // source tree
|
|
21
|
+
join(CORE_LLM_DIR, 'prompts'), // bundled CLI (dist/prompts/)
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load a prompt template from the prompts directory.
|
|
26
|
+
*
|
|
27
|
+
* @param name - Name of the prompt file (without .md extension)
|
|
28
|
+
* @returns The prompt content as a string
|
|
29
|
+
*/
|
|
30
|
+
export function loadPrompt(name: string): string {
|
|
31
|
+
// Prompt names are internal identifiers (e.g. 'researcher', 'evaluator').
|
|
32
|
+
// Reject anything that could escape the prompts directory — path separators,
|
|
33
|
+
// parent-dir segments, or absolute/drive-letter prefixes — so a name can never
|
|
34
|
+
// resolve to a file outside PROMPT_CANDIDATES even if it matches a real .md.
|
|
35
|
+
if (typeof name !== 'string' || !/^[A-Za-z0-9._-]+$/.test(name) || name.includes('..')) {
|
|
36
|
+
logger.error(`[prompts] Rejected unsafe prompt name: ${JSON.stringify(name)}`);
|
|
37
|
+
throw new Error(`[prompts] Rejected unsafe prompt name: ${JSON.stringify(name)}`);
|
|
38
|
+
}
|
|
39
|
+
for (const dir of PROMPT_CANDIDATES) {
|
|
40
|
+
try {
|
|
41
|
+
return readFileSync(join(dir, `${name}.md`), 'utf-8');
|
|
42
|
+
} catch { /* try next candidate */ }
|
|
43
|
+
}
|
|
44
|
+
// Fail loud: returning '' here fed a BLANK system prompt straight into the LLM
|
|
45
|
+
// call (every caller pipes the result into populatePrompt → the model), wasting
|
|
46
|
+
// tokens and producing garbage with no signal. A missing prompt is a packaging
|
|
47
|
+
// bug, so surface it.
|
|
48
|
+
logger.error(`[prompts] Failed to load prompt: ${name} (searched: ${PROMPT_CANDIDATES.join(', ')})`);
|
|
49
|
+
throw new Error(`[prompts] Failed to load prompt "${name}" (searched: ${PROMPT_CANDIDATES.join(', ')})`);
|
|
50
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Research Model Resolver
|
|
3
|
+
*
|
|
4
|
+
* Centralized logic for resolving the research model based on
|
|
5
|
+
* explicit parameters, configuration, and host context.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type Model } from '@earendil-works/pi-ai';
|
|
9
|
+
import { type ModelRegistry } from '@earendil-works/pi-coding-agent';
|
|
10
|
+
import { getConfig } from '../../config.ts';
|
|
11
|
+
import { logger } from '../../logger.ts';
|
|
12
|
+
import { pickPreferredAvailable, readModelsJsonProviderOrder, safeGetAll, safeGetAvailable } from './model-registry-factory.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the research model with standardized priority:
|
|
16
|
+
* 1. Explicit modelId parameter (highest priority)
|
|
17
|
+
* 2. RESEARCH_MODEL from configuration
|
|
18
|
+
* 3. Host model from context (fallback)
|
|
19
|
+
*
|
|
20
|
+
* @param options - Resolution context
|
|
21
|
+
* @returns The resolved model
|
|
22
|
+
*/
|
|
23
|
+
export function resolveResearchModel(options: {
|
|
24
|
+
modelRegistry: ModelRegistry;
|
|
25
|
+
config?: import('../../config.ts').Config;
|
|
26
|
+
modelId?: string;
|
|
27
|
+
hostModel?: Model<any>;
|
|
28
|
+
cwd?: string;
|
|
29
|
+
}): Model<any> {
|
|
30
|
+
const { modelRegistry, config, modelId, hostModel, cwd } = options;
|
|
31
|
+
const activeConfig = config || getConfig(cwd);
|
|
32
|
+
|
|
33
|
+
// The same model id can be registered under multiple providers — typically a
|
|
34
|
+
// user-configured authed provider plus pi's built-in unauthed one (e.g.
|
|
35
|
+
// `glm-coding/glm-4.7` vs `zai/glm-4.7`). When matching by bare id, prefer a
|
|
36
|
+
// provider that actually has auth so we never resolve to a keyless entry that
|
|
37
|
+
// throws "No API key for provider" at the first LLM call.
|
|
38
|
+
const authedKeys = new Set(
|
|
39
|
+
safeGetAvailable(modelRegistry).map((m) => `${m.provider}/${m.id}`)
|
|
40
|
+
);
|
|
41
|
+
const matchById = (id: string): Model<any> | undefined => {
|
|
42
|
+
// Exact "provider/id" wins outright; otherwise prefer an authed same-id entry.
|
|
43
|
+
const allModels = safeGetAll(modelRegistry);
|
|
44
|
+
const exact = allModels.find((m) => `${m.provider}/${m.id}` === id);
|
|
45
|
+
if (exact) return exact as Model<any>;
|
|
46
|
+
const sameId = allModels.filter((m) => m.id === id) as Model<any>[];
|
|
47
|
+
return sameId.find((m) => authedKeys.has(`${m.provider}/${m.id}`)) ?? sameId[0];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// 1. Explicit parameter (e.g. from tool call)
|
|
51
|
+
if (modelId) {
|
|
52
|
+
const found = matchById(modelId);
|
|
53
|
+
if (found) {
|
|
54
|
+
return found as Model<any>;
|
|
55
|
+
}
|
|
56
|
+
logger.warn(`[ModelResolver] Explicit model '${modelId}' not found in registry; falling back.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. RESEARCH_MODEL override from configuration
|
|
60
|
+
if (activeConfig.RESEARCH_MODEL) {
|
|
61
|
+
const target = activeConfig.RESEARCH_MODEL;
|
|
62
|
+
const found = matchById(target);
|
|
63
|
+
if (found) {
|
|
64
|
+
return found as Model<any>;
|
|
65
|
+
}
|
|
66
|
+
logger.warn(`[ModelResolver] RESEARCH_MODEL '${target}' not found in registry; falling back.`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3. Host model fallback
|
|
70
|
+
if (hostModel) {
|
|
71
|
+
return hostModel;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 4. Absolute fallback: pick first available model with auth, honoring the
|
|
75
|
+
// user's models.json provider order (consistent with resolveModel step 3 —
|
|
76
|
+
// otherwise the host model and the research fallback could disagree on which
|
|
77
|
+
// of several authed providers to prefer).
|
|
78
|
+
const available = safeGetAvailable(modelRegistry);
|
|
79
|
+
if (available.length > 0) {
|
|
80
|
+
return pickPreferredAvailable(available, readModelsJsonProviderOrder()) as Model<any>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 5. Last resort before failing: any known model at all (host may not expose
|
|
84
|
+
// getAvailable(), so an empty "available" set is not proof of "no models").
|
|
85
|
+
const anyModel = safeGetAll(modelRegistry);
|
|
86
|
+
if (anyModel.length > 0) {
|
|
87
|
+
return pickPreferredAvailable(anyModel, readModelsJsonProviderOrder()) as Model<any>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw new Error('No LLM model available for research. Please configure your model registry (~/.pi/agent/models.json).');
|
|
91
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planning Constants
|
|
3
|
+
*
|
|
4
|
+
* Configuration values for the planning service.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ResearchPlanSchema } from './interfaces/planning-interfaces.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Schemas for LLM response validation
|
|
11
|
+
*/
|
|
12
|
+
export const PlanningResponseSchemaAsTSchema = ResearchPlanSchema;
|
|
13
|
+
export const EvaluationResponseSchemaAsTSchema = ResearchPlanSchema;
|