@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.
Files changed (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +64 -132
  3. package/dist/cli.mjs +23211 -0
  4. package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
  5. package/dist/prompts/research-knowledge-search-extractor.md +41 -0
  6. package/dist/prompts/research-tool-usage.md +92 -0
  7. package/dist/prompts/researcher.md +89 -0
  8. package/dist/prompts/system-coordinator.md +75 -0
  9. package/dist/prompts/system-lead-evaluator.md +118 -0
  10. package/dist/thread-worker.mjs +2611 -0
  11. package/dist/webgpu-probe.mjs +80 -0
  12. package/docs/AGENT-SKILL.md +102 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +213 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +113 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +64 -0
  19. package/package.json +102 -36
  20. package/scripts/build.cjs +140 -0
  21. package/scripts/cleanup.cjs +149 -0
  22. package/scripts/prepare.cjs +28 -0
  23. package/scripts/setup.cjs +134 -0
  24. package/skills/pi-research/README.md +90 -0
  25. package/skills/pi-research/SKILL.md +102 -0
  26. package/skills/pi-research/references/configuration.md +77 -0
  27. package/skills/pi-research/scripts/run.mjs +237 -0
  28. package/src/cleanup/research-cleanup.ts +140 -0
  29. package/src/cli.ts +1044 -0
  30. package/src/config.ts +1032 -155
  31. package/src/constants.ts +115 -84
  32. package/src/core/health-check-service.ts +265 -0
  33. package/src/core/interfaces/health-check-interfaces.ts +50 -0
  34. package/src/core/interfaces/knowledge-interfaces.ts +190 -0
  35. package/src/core/interfaces/observer-interfaces.ts +67 -0
  36. package/src/core/interfaces/orchestration-interfaces.ts +86 -0
  37. package/src/core/interfaces/planning-interfaces.ts +100 -0
  38. package/src/core/interfaces/process-interfaces.ts +37 -0
  39. package/src/core/interfaces/research-plan-types.ts +52 -0
  40. package/src/core/interfaces/scheduler-interfaces.ts +91 -0
  41. package/src/core/interfaces/service-names.ts +39 -0
  42. package/src/core/interfaces/state-manager-interfaces.ts +29 -0
  43. package/src/core/interfaces/steering-interfaces.ts +31 -0
  44. package/src/core/llm/agentic-repair.ts +169 -0
  45. package/src/core/llm/inject-date.ts +52 -0
  46. package/src/core/llm/llm-timeout.ts +64 -0
  47. package/src/core/llm/llm-utils.ts +86 -0
  48. package/src/core/llm/model-registry-factory.ts +287 -0
  49. package/src/core/llm/pi-ai-completion.ts +29 -0
  50. package/src/core/llm/prompts.ts +50 -0
  51. package/src/core/llm/research-model-resolver.ts +91 -0
  52. package/src/core/planning-constants.ts +13 -0
  53. package/src/core/planning-service.ts +649 -0
  54. package/src/core/planning-utils.ts +299 -0
  55. package/src/core/scheduler-factory.ts +48 -0
  56. package/src/core/scheduler-service.ts +123 -0
  57. package/src/core/service-initialization.ts +194 -0
  58. package/src/core/service-interfaces.ts +48 -0
  59. package/src/core/service-registry.ts +659 -0
  60. package/src/healthcheck/index.ts +271 -265
  61. package/src/healthcheck/registry.ts +130 -0
  62. package/src/index.ts +639 -50
  63. package/src/infrastructure/browser/browser-cleanup.ts +309 -0
  64. package/src/infrastructure/browser/browser-client.ts +199 -0
  65. package/src/infrastructure/browser/browser-error-utils.ts +160 -0
  66. package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
  67. package/src/infrastructure/browser/browser-server.ts +174 -0
  68. package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
  69. package/src/infrastructure/browser/cleanup-utils.ts +155 -0
  70. package/src/infrastructure/browser/client-agent.ts +31 -0
  71. package/src/infrastructure/browser/config.ts +288 -0
  72. package/src/infrastructure/browser/ensure-browser.ts +205 -0
  73. package/src/infrastructure/browser/index.ts +37 -0
  74. package/src/infrastructure/browser/priority-task-queue.ts +226 -0
  75. package/src/infrastructure/browser/scheduler-factory.ts +354 -0
  76. package/src/infrastructure/browser/task-execution-service.ts +209 -0
  77. package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
  78. package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
  79. package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
  80. package/src/infrastructure/browser/thread-worker-types.ts +25 -0
  81. package/src/infrastructure/browser/thread-worker.mjs +2611 -0
  82. package/src/infrastructure/browser/thread-worker.ts +168 -0
  83. package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
  84. package/src/infrastructure/embedding/embedding-client.ts +205 -0
  85. package/src/infrastructure/embedding/embedding-factory.ts +324 -0
  86. package/src/infrastructure/embedding/embedding-server.ts +535 -0
  87. package/src/infrastructure/file-lock-service.ts +592 -0
  88. package/src/infrastructure/gpu-resource-service.ts +191 -0
  89. package/src/infrastructure/knowledge-store-service.ts +475 -0
  90. package/src/infrastructure/metrics-service.ts +129 -0
  91. package/src/infrastructure/process-lifecycle-service.ts +253 -0
  92. package/src/infrastructure/scheduler-factory-service.ts +58 -0
  93. package/src/infrastructure/service-initialization.ts +292 -0
  94. package/src/infrastructure/state/state-backup-manager.ts +290 -0
  95. package/src/infrastructure/state/state-browser-api.ts +62 -0
  96. package/src/infrastructure/state/state-browser-manager.ts +70 -0
  97. package/src/infrastructure/state/state-manager-service.ts +264 -0
  98. package/src/infrastructure/state/state-manager.ts +547 -0
  99. package/src/infrastructure/state/state-metrics.ts +92 -0
  100. package/src/infrastructure/state/state-migration.ts +103 -0
  101. package/src/infrastructure/state/state-path-configuration.ts +119 -0
  102. package/src/infrastructure/state/state-session-api.ts +94 -0
  103. package/src/infrastructure/state/state-session-manager.ts +127 -0
  104. package/src/infrastructure/state/state-validator.ts +93 -0
  105. package/src/infrastructure/types/state-types.ts +85 -0
  106. package/src/knowledge/chunker.ts +157 -0
  107. package/src/knowledge/embedder-init.ts +413 -0
  108. package/src/knowledge/embedder-utils.ts +296 -0
  109. package/src/knowledge/embedder.ts +592 -0
  110. package/src/knowledge/index.ts +126 -0
  111. package/src/knowledge/lancedb-loader.ts +35 -0
  112. package/src/knowledge/migration.ts +17 -0
  113. package/src/knowledge/model-config.ts +132 -0
  114. package/src/knowledge/onnx-env.ts +48 -0
  115. package/src/knowledge/store-operations.ts +291 -0
  116. package/src/knowledge/store-schema.ts +79 -0
  117. package/src/knowledge/store.ts +1251 -0
  118. package/src/knowledge/transformers-loader.ts +31 -0
  119. package/src/knowledge/webgpu-probe.mjs +80 -0
  120. package/src/knowledge/webgpu-viability.ts +255 -0
  121. package/src/knowledge/writer-queue.ts +252 -0
  122. package/src/logger.ts +316 -122
  123. package/src/observers/research-observer-impl.ts +459 -0
  124. package/src/orchestration/deep-research-orchestrator.ts +524 -773
  125. package/src/orchestration/deep-research-types.ts +47 -35
  126. package/src/orchestration/headless-observer.ts +114 -0
  127. package/src/orchestration/orchestration-types.ts +33 -0
  128. package/src/orchestration/quick-research-orchestrator.ts +463 -0
  129. package/src/orchestration/research-orchestration-service.ts +549 -0
  130. package/src/orchestration/research-session-service.ts +178 -0
  131. package/src/orchestration/research-synthesis-service.ts +348 -0
  132. package/src/orchestration/researcher-executor.ts +394 -0
  133. package/src/orchestration/researcher.ts +115 -42
  134. package/src/orchestration/service-initialization.ts +109 -0
  135. package/src/orchestration/session-context.ts +7 -3
  136. package/src/orchestration/session-state.ts +672 -0
  137. package/src/prompts/research-knowledge-relevance-triage.md +35 -0
  138. package/src/prompts/research-knowledge-search-extractor.md +41 -0
  139. package/src/prompts/research-tool-usage.md +63 -16
  140. package/src/prompts/researcher.md +59 -136
  141. package/src/prompts/system-coordinator.md +64 -13
  142. package/src/prompts/system-lead-evaluator.md +82 -189
  143. package/src/research-config.ts +872 -0
  144. package/src/sdk.ts +847 -0
  145. package/src/security/cisa-kev.ts +72 -9
  146. package/src/security/cvss.ts +92 -0
  147. package/src/security/ecosystem.ts +45 -0
  148. package/src/security/github-advisories.ts +168 -428
  149. package/src/security/github-advisory-types.ts +282 -0
  150. package/src/security/index.ts +63 -177
  151. package/src/security/nvd-client.ts +550 -0
  152. package/src/security/nvd-types.ts +179 -0
  153. package/src/security/osv-client.ts +246 -0
  154. package/src/security/osv-types.ts +285 -0
  155. package/src/security/types.ts +67 -53
  156. package/src/skill-install/skill-installer.ts +519 -0
  157. package/src/stackexchange/index.ts +81 -27
  158. package/src/stackexchange/output/compact.ts +7 -6
  159. package/src/stackexchange/output/table.ts +11 -7
  160. package/src/stackexchange/queries.ts +0 -10
  161. package/src/stackexchange/rest-client.ts +141 -66
  162. package/src/stackexchange/types.ts +0 -17
  163. package/src/tool.ts +15 -423
  164. package/src/tools/grep.ts +32 -310
  165. package/src/tools/health-tool-definition.ts +101 -0
  166. package/src/tools/index.ts +38 -9
  167. package/src/tools/research-knowledge-search.ts +974 -0
  168. package/src/tools/research-knowledge-types.ts +77 -0
  169. package/src/tools/research-tool-definition.ts +494 -0
  170. package/src/tools/scrape.ts +286 -388
  171. package/src/tools/search.ts +98 -153
  172. package/src/tools/security.ts +107 -80
  173. package/src/tools/stackexchange.ts +58 -35
  174. package/src/tools/youtube-transcript.ts +163 -0
  175. package/src/tui/knowledge-search-panel.ts +86 -0
  176. package/src/tui/pulse.ts +86 -0
  177. package/src/tui/research-health.ts +124 -0
  178. package/src/tui/research-panel-color-utils.ts +340 -0
  179. package/src/tui/research-panel-format.ts +54 -0
  180. package/src/tui/research-panel-state.ts +404 -0
  181. package/src/tui/research-panel-wave.ts +280 -0
  182. package/src/tui/research-panel.ts +339 -435
  183. package/src/tui/research-tui-manager.ts +233 -0
  184. package/src/tui/tui-controller.ts +137 -0
  185. package/src/types/extension-context.ts +18 -49
  186. package/src/types/index.ts +61 -0
  187. package/src/types/llm.ts +68 -29
  188. package/src/types/research-panel-types.ts +61 -0
  189. package/src/utils/circuit-breaker.ts +146 -0
  190. package/src/utils/citation-utils.ts +97 -0
  191. package/src/utils/disk-space-checker.ts +55 -0
  192. package/src/utils/error-tracker.ts +265 -0
  193. package/src/utils/host-config.ts +45 -0
  194. package/src/utils/input-validation.ts +8 -10
  195. package/src/utils/json-utils.ts +150 -16
  196. package/src/utils/llm-usage.ts +54 -0
  197. package/src/utils/log-rotation.ts +125 -0
  198. package/src/utils/log-utils.ts +159 -0
  199. package/src/utils/make-resource-loader.ts +3 -1
  200. package/src/utils/metrics-summary.ts +459 -0
  201. package/src/utils/metrics.ts +302 -0
  202. package/src/utils/research-export.ts +28 -12
  203. package/src/utils/safe-unref.ts +31 -0
  204. package/src/utils/shared-links.ts +195 -90
  205. package/src/utils/shutdown-manager.ts +65 -0
  206. package/src/utils/stdio-capture.ts +407 -0
  207. package/src/utils/text-utils.ts +258 -11
  208. package/src/utils/tool-usage-tracker.ts +83 -16
  209. package/src/utils/url-utils.ts +255 -0
  210. package/src/utils/user-agent.ts +28 -0
  211. package/src/web-research/browser-search.ts +179 -0
  212. package/src/web-research/retry-utils.ts +173 -77
  213. package/src/web-research/scraper-types.ts +72 -0
  214. package/src/web-research/scraper-utils.ts +434 -0
  215. package/src/web-research/search.ts +78 -218
  216. package/src/web-research/types.ts +9 -24
  217. package/src/web-research/utils.ts +2 -143
  218. package/src/web-research/web-scraper.ts +477 -0
  219. package/src/youtube/index.ts +154 -0
  220. package/src/youtube/potoken.ts +259 -0
  221. package/src/youtube/transcript-client.ts +390 -0
  222. package/src/youtube/video-id.ts +118 -0
  223. package/config/searxng/default-settings.yml +0 -189
  224. package/config/searxng/limiter.toml +0 -34
  225. package/config/tooling/eslint.config.js +0 -73
  226. package/config/tooling/vitest.config.integration.ts +0 -17
  227. package/config/tooling/vitest.config.ts +0 -40
  228. package/config/tooling/vitest.config.unit.ts +0 -12
  229. package/docs/assets/README-banner.jpg +0 -0
  230. package/scripts/setup.js +0 -212
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -644
  232. package/src/infrastructure/searxng-manager.ts +0 -1183
  233. package/src/infrastructure/state-manager.ts +0 -918
  234. package/src/orchestration/deep-research-reducer.ts +0 -129
  235. package/src/orchestration/id-utils.ts +0 -63
  236. package/src/security/nvd.ts +0 -532
  237. package/src/security/osv.ts +0 -487
  238. package/src/stackexchange/cache.ts +0 -111
  239. package/src/stackexchange/output/index.ts +0 -7
  240. package/src/stackexchange/output/json.ts +0 -21
  241. package/src/tui/searxng-status.ts +0 -80
  242. package/src/utils/inject-date.ts +0 -29
  243. package/src/utils/research-error.ts +0 -205
  244. package/src/utils/searxng-config.ts +0 -94
  245. package/src/utils/session-state.ts +0 -295
  246. package/src/web-research/dependencies.ts +0 -20
  247. package/src/web-research/scrapers.ts +0 -935
  248. 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;