@lincoln504/pi-research 0.1.13 → 1.0.1
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 +55 -138
- package/dist/cli.mjs +23375 -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 +130 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +211 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +122 -0
- package/docs/THIRD-PARTY-NOTICES.md +66 -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 +1195 -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,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Research Orchestration Service
|
|
3
|
+
*
|
|
4
|
+
* Handles the core orchestration logic for multi-round research.
|
|
5
|
+
* Responsible for:
|
|
6
|
+
* - Multi-round research coordination
|
|
7
|
+
* - Agent spawning and concurrent execution management
|
|
8
|
+
* - Search result distribution to researchers
|
|
9
|
+
* - Health check integration
|
|
10
|
+
* - Knowledge store integration for link descriptions
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Model } from '@earendil-works/pi-ai';
|
|
14
|
+
import { resolveResearchModel } from '../core/llm/research-model-resolver.ts';
|
|
15
|
+
import type { QueryResultWithError } from '../web-research/types.ts';
|
|
16
|
+
import type { RunResearchersOptions } from './orchestration-types.ts';
|
|
17
|
+
import { RESEARCHER_LAUNCH_DELAY_MS } from '../constants.ts';
|
|
18
|
+
import { search } from '../web-research/search.ts';
|
|
19
|
+
import { parseCitations } from '../utils/text-utils.ts';
|
|
20
|
+
import { logger, resetLogger } from '../logger.ts';
|
|
21
|
+
import { healthRegistry } from '../healthcheck/index.ts';
|
|
22
|
+
import { clearSessionCircuitBreaker } from '../infrastructure/browser/browser-error-utils.ts';
|
|
23
|
+
import { getService, ServiceLifecycle, tryGetService, tryGetServiceContainerFromCtx } from '../core/service-registry.ts';
|
|
24
|
+
import { ServiceNames } from '../core/service-interfaces.ts';
|
|
25
|
+
import type {
|
|
26
|
+
IWriterQueue,
|
|
27
|
+
IKnowledgeStoreService,
|
|
28
|
+
IHealthRegistryService,
|
|
29
|
+
IResearchOrchestration,
|
|
30
|
+
StoreUrlEntry,
|
|
31
|
+
ResearchOptions,
|
|
32
|
+
ResearchPlan,
|
|
33
|
+
IPlanningService,
|
|
34
|
+
IResearchSynthesisService,
|
|
35
|
+
} from '../core/service-interfaces.ts';
|
|
36
|
+
import type { Config } from '../config.ts';
|
|
37
|
+
import { getConfig, DEFAULTS } from '../config.ts';
|
|
38
|
+
import { getCachedScrapedContent, normalizeUrl, cleanupSharedLinks } from '../utils/shared-links.ts';
|
|
39
|
+
import { runResearcher } from './researcher-executor.ts';
|
|
40
|
+
import { recordResearcherFailure, shouldStopResearch, getResearchStopMessage } from './session-state.ts';
|
|
41
|
+
import type { ResearchSessionService } from './research-session-service.ts';
|
|
42
|
+
import { QuickResearchOrchestrator } from './quick-research-orchestrator.ts';
|
|
43
|
+
import { DeepResearchOrchestrator } from './deep-research-orchestrator.ts';
|
|
44
|
+
import { metrics } from '../utils/metrics.ts';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Research Orchestration Service
|
|
48
|
+
*
|
|
49
|
+
* Handles core orchestration logic for multi-round research.
|
|
50
|
+
*/
|
|
51
|
+
export class ResearchOrchestrationService implements IResearchOrchestration {
|
|
52
|
+
readonly name = ServiceNames.RESEARCH_ORCHESTRATION;
|
|
53
|
+
lifecycle = ServiceLifecycle.UNINITIALIZED;
|
|
54
|
+
|
|
55
|
+
async initialize(): Promise<void> {
|
|
56
|
+
this.lifecycle = ServiceLifecycle.INITIALIZED;
|
|
57
|
+
}
|
|
58
|
+
async dispose(): Promise<void> {
|
|
59
|
+
this.lifecycle = ServiceLifecycle.DISPOSED;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the model for research based on options and config.
|
|
64
|
+
*/
|
|
65
|
+
async resolveResearchModel(options: ResearchOptions): Promise<Model<any>> {
|
|
66
|
+
const { ctx, model, config } = options;
|
|
67
|
+
// If a fully-resolved model was already provided (carries both provider and
|
|
68
|
+
// id), use it verbatim. Re-resolving from its id alone can land on a
|
|
69
|
+
// different provider's entry that happens to share the same id — e.g. the
|
|
70
|
+
// host's authed `glm-coding/glm-4.7` vs pi's built-in unauthed `zai/glm-4.7`
|
|
71
|
+
// — silently dropping the provider that actually has an API key.
|
|
72
|
+
if (model && (model as any).provider && (model as any).id) {
|
|
73
|
+
return model as Model<any>;
|
|
74
|
+
}
|
|
75
|
+
return resolveResearchModel({
|
|
76
|
+
modelRegistry: ctx.modelRegistry,
|
|
77
|
+
config: config || getConfig(ctx.cwd),
|
|
78
|
+
modelId: (model as any)?.id,
|
|
79
|
+
hostModel: ctx.model as Model<any>,
|
|
80
|
+
cwd: ctx.cwd,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Run a research task (Quick or Deep)
|
|
86
|
+
*/
|
|
87
|
+
async runResearch(options: ResearchOptions, signal?: AbortSignal): Promise<string> {
|
|
88
|
+
const { ctx, query, depth = 0, observer, onUpdate, sessionId, researchId, config, excludeTools } = options;
|
|
89
|
+
|
|
90
|
+
const researchConfig = config || getConfig(ctx.cwd);
|
|
91
|
+
|
|
92
|
+
// Fold the config-level per-tool disable list (PI_RESEARCH_DISABLED_TOOLS) into the
|
|
93
|
+
// per-run excludeTools stream. This is the single chokepoint that constructs both the
|
|
94
|
+
// Quick and Deep orchestrators, so the merged list reaches BOTH each researcher's tool
|
|
95
|
+
// allowlist AND the coordinator/evaluator "DISABLED TOOLS" prompt section for free.
|
|
96
|
+
// Disable-only and additive: any per-call excludeTools (e.g. CLI --exclude-tools) is preserved.
|
|
97
|
+
const configDisabledTools = researchConfig.DISABLED_TOOLS
|
|
98
|
+
? researchConfig.DISABLED_TOOLS.split(',').map((s) => s.trim()).filter(Boolean)
|
|
99
|
+
: [];
|
|
100
|
+
const mergedExcludeTools = configDisabledTools.length > 0
|
|
101
|
+
? [...new Set([...(excludeTools ?? []), ...configDisabledTools])]
|
|
102
|
+
: excludeTools;
|
|
103
|
+
|
|
104
|
+
// Resolve model using centralized priority logic
|
|
105
|
+
const selectedModel = await this.resolveResearchModel(options);
|
|
106
|
+
logger.info(`[ResearchOrchestrationService] Using model: ${selectedModel.provider}/${selectedModel.id}`);
|
|
107
|
+
|
|
108
|
+
const researchStart = Date.now();
|
|
109
|
+
|
|
110
|
+
let result: string;
|
|
111
|
+
try {
|
|
112
|
+
if (depth === 0) {
|
|
113
|
+
const orchestrator = new QuickResearchOrchestrator({
|
|
114
|
+
ctx,
|
|
115
|
+
model: selectedModel,
|
|
116
|
+
query,
|
|
117
|
+
sessionId,
|
|
118
|
+
researchId,
|
|
119
|
+
observer,
|
|
120
|
+
onUpdate,
|
|
121
|
+
config: researchConfig,
|
|
122
|
+
excludeTools: mergedExcludeTools,
|
|
123
|
+
initialLinks: options.initialLinks,
|
|
124
|
+
});
|
|
125
|
+
result = await orchestrator.run(signal);
|
|
126
|
+
} else {
|
|
127
|
+
const orchestrator = new DeepResearchOrchestrator({
|
|
128
|
+
ctx,
|
|
129
|
+
model: selectedModel,
|
|
130
|
+
query,
|
|
131
|
+
complexity: depth as 1 | 2 | 3,
|
|
132
|
+
sessionId,
|
|
133
|
+
researchId,
|
|
134
|
+
observer,
|
|
135
|
+
onUpdate,
|
|
136
|
+
config: researchConfig,
|
|
137
|
+
excludeTools: mergedExcludeTools,
|
|
138
|
+
orchestrationService: this,
|
|
139
|
+
initialLinks: options.initialLinks,
|
|
140
|
+
});
|
|
141
|
+
result = await orchestrator.run(signal);
|
|
142
|
+
}
|
|
143
|
+
const researchDuration = Date.now() - researchStart;
|
|
144
|
+
metrics.observe('research_manager_latency_ms', researchDuration, { depth: String(depth), status: 'success', source: 'extension' });
|
|
145
|
+
metrics.increment('research_manager_requests_total', 1, { depth: String(depth), status: 'success', source: 'extension' });
|
|
146
|
+
return result;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const researchDuration = Date.now() - researchStart;
|
|
149
|
+
metrics.observe('research_manager_latency_ms', researchDuration, { depth: String(depth), status: 'error', source: 'extension' });
|
|
150
|
+
metrics.increment('research_manager_requests_total', 1, { depth: String(depth), status: 'error', source: 'extension' });
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Cleanup and reset services for the current research run
|
|
157
|
+
* @param opts.skipStoreMaintenance - skip the post-run FTS rebuild + optimize
|
|
158
|
+
* (used on user abort: both are non-signal-aware optimization passes; row
|
|
159
|
+
* durability lives in the LanceDB commits already made, and the next run's
|
|
160
|
+
* cleanup performs the same rebuild).
|
|
161
|
+
*/
|
|
162
|
+
async cleanupResearchServices(sessionId?: string, researchId?: string, ctx?: any, config?: Config, opts?: { skipStoreMaintenance?: boolean }): Promise<void> {
|
|
163
|
+
const targetId = researchId || sessionId;
|
|
164
|
+
const container = tryGetServiceContainerFromCtx(ctx);
|
|
165
|
+
// Use the run's RESOLVED config when the caller has it — an SDK caller's
|
|
166
|
+
// options.config override (e.g. KNOWLEDGE_STORE_MODE:'none' to stay off the native
|
|
167
|
+
// vector stack) must govern this post-run FTS/optimize gate too. Falling back to a
|
|
168
|
+
// fresh getConfig(cwd) would ignore that override and either load the native stack on
|
|
169
|
+
// a platform that lacks it, or skip the rebuild a store-enabled run legitimately needs.
|
|
170
|
+
const resolvedConfig = config ?? getConfig(ctx?.cwd);
|
|
171
|
+
|
|
172
|
+
// Cleanup session service
|
|
173
|
+
try {
|
|
174
|
+
const sessionService = await getService<ResearchSessionService>(ServiceNames.RESEARCH_SESSION_SERVICE, ctx, container);
|
|
175
|
+
if (sessionService && targetId) {
|
|
176
|
+
await sessionService.cleanup(targetId);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Perform FTS index rebuild here after synthesis/completion — but only when
|
|
180
|
+
// the knowledge store is enabled. With KNOWLEDGE_STORE_MODE='none' (or on a
|
|
181
|
+
// platform missing the native @lancedb binding, e.g. Intel macOS) merely
|
|
182
|
+
// resolving the service loads the native vector stack and throws; gating it
|
|
183
|
+
// lets the rest of cleanup proceed instead of jumping to the catch below.
|
|
184
|
+
if (opts?.skipStoreMaintenance) {
|
|
185
|
+
logger.debug('[ResearchOrchestrationService] Skipping FTS rebuild/optimize (run aborted); next run\'s cleanup will rebuild');
|
|
186
|
+
} else if (resolvedConfig.KNOWLEDGE_STORE_MODE !== 'none') {
|
|
187
|
+
const ksService = await getService<IKnowledgeStoreService>(ServiceNames.KNOWLEDGE_STORE, ctx, container);
|
|
188
|
+
if (ksService && ksService.isReady()) {
|
|
189
|
+
const store = await ksService.getStore();
|
|
190
|
+
if (store) {
|
|
191
|
+
logger.info('[ResearchOrchestrationService] Rebuilding FTS index after research run');
|
|
192
|
+
const rebuilt = await store.rebuildFtsIndex();
|
|
193
|
+
// Only compact/prune when the FTS index was actually rebuilt — i.e. the
|
|
194
|
+
// run changed the data. A no-op run skips both, so steady-state loops no
|
|
195
|
+
// longer accumulate orphaned index/version files (the disk-bloat fix).
|
|
196
|
+
if (rebuilt) {
|
|
197
|
+
await store.optimize();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch (_err) {
|
|
203
|
+
logger.debug('[ResearchOrchestrationService] Service cleanup failed:', _err);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// NOTE: synthesis reports are intentionally NOT cleared here. This cleanup runs in
|
|
207
|
+
// the orchestrator's own finally, i.e. INSIDE the run() call — clearing here erased
|
|
208
|
+
// the per-researcher reports before the SDK's runDeepResearch() promise even resolved,
|
|
209
|
+
// making getResearchReports()/runResearchDetailed().reports always return an empty Map.
|
|
210
|
+
// Retention is bounded instead by ResearchSynthesisService's MAX_SESSIONS LRU cap, its
|
|
211
|
+
// designed backstop; reports for a completed run linger only until evicted or the SDK
|
|
212
|
+
// reads them. See getResearchReports() in sdk.ts.
|
|
213
|
+
|
|
214
|
+
// Clear planning state
|
|
215
|
+
const planningService = tryGetService<IPlanningService>(ServiceNames.PLANNING, container);
|
|
216
|
+
if (planningService && targetId) {
|
|
217
|
+
planningService.clearPlanningState(targetId);
|
|
218
|
+
logger.debug(`[ResearchOrchestrationService] Cleared planning state for ${targetId}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (targetId) {
|
|
222
|
+
cleanupSharedLinks(targetId);
|
|
223
|
+
resetLogger(targetId);
|
|
224
|
+
clearSessionCircuitBreaker(targetId);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
logger.debug(`[ResearchOrchestrationService] Cleaned up research services for ${targetId}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Distribute search results to researchers based on query matching
|
|
232
|
+
* @param plan - Research plan with researchers and queries
|
|
233
|
+
* @param results - Search results from queries
|
|
234
|
+
* @param _ctx - Optional context for container isolation
|
|
235
|
+
* @returns Map of researcher ID -> array of URLs
|
|
236
|
+
*/
|
|
237
|
+
async distributeSearchResults(plan: ResearchPlan, results: QueryResultWithError[], _ctx?: any): Promise<Map<string, string[]>> {
|
|
238
|
+
const startTime = Date.now();
|
|
239
|
+
const queryToResults = new Map(results.map(r => [r.query, r.results || []]));
|
|
240
|
+
const linkMap = new Map<string, string[]>();
|
|
241
|
+
|
|
242
|
+
for (const researcher of plan.researchers || []) {
|
|
243
|
+
const researcherUrls = new Set<string>();
|
|
244
|
+
for (const query of researcher.queries || []) {
|
|
245
|
+
const queryResults = queryToResults.get(query) || [];
|
|
246
|
+
for (const res of queryResults) {
|
|
247
|
+
researcherUrls.add(res.url);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
linkMap.set(String(researcher.id), Array.from(researcherUrls));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
logger.debug(`[ResearchOrchestrationService] Distributed ${results.length} results in ${Date.now() - startTime}ms`);
|
|
254
|
+
return linkMap;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Run researchers concurrently with launch delay
|
|
259
|
+
* @param options - Run options
|
|
260
|
+
* @param researcherLinks - Optional map of researcher ID -> search results
|
|
261
|
+
* @param storeLinks - Optional map of researcher ID -> store results
|
|
262
|
+
* @param _ctx - Optional context for container isolation
|
|
263
|
+
*/
|
|
264
|
+
async runResearchers(
|
|
265
|
+
options: RunResearchersOptions,
|
|
266
|
+
researcherLinks?: Map<string, string[]>,
|
|
267
|
+
storeLinks?: Map<string, StoreUrlEntry[]>,
|
|
268
|
+
_ctx?: any
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
const { plan, options: orchestratorOptions, currentRound, signal } = options;
|
|
271
|
+
const { sessionId, researchId, observer, ctx } = orchestratorOptions;
|
|
272
|
+
const container = tryGetServiceContainerFromCtx(ctx);
|
|
273
|
+
|
|
274
|
+
// Obtain the planning service once for all researchers in this round
|
|
275
|
+
let planningService: IPlanningService;
|
|
276
|
+
try {
|
|
277
|
+
planningService = await getService<IPlanningService>(ServiceNames.PLANNING, ctx, container);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
280
|
+
// If the service container is already disposing (SIGTERM during active research),
|
|
281
|
+
// return gracefully rather than throwing — the research run is ending anyway.
|
|
282
|
+
if (errMsg.includes('during container disposal') || options.signal?.aborted) {
|
|
283
|
+
logger.info('[ResearchOrchestrationService] Service container disposing — skipping researchers gracefully');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
logger.error('[ResearchOrchestrationService] Failed to get planning service:', err);
|
|
287
|
+
throw new Error('Planning service not available. Research cannot continue.', { cause: err });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const researchers = plan.researchers || [];
|
|
291
|
+
const active = new Set<Promise<void>>();
|
|
292
|
+
// Honour MAX_CONCURRENT_RESEARCHERS — prevents resource spikes when a plan has many researchers.
|
|
293
|
+
const maxConcurrent: number = (orchestratorOptions.config as Config)?.MAX_CONCURRENT_RESEARCHERS ?? DEFAULTS.MAX_CONCURRENT_RESEARCHERS;
|
|
294
|
+
|
|
295
|
+
for (const configItem of researchers) {
|
|
296
|
+
if (signal?.aborted) break;
|
|
297
|
+
|
|
298
|
+
// Stop BEFORE launching if the failure threshold is already crossed. The
|
|
299
|
+
// post-launch check below only fires after a researcher is in-flight, so
|
|
300
|
+
// without this an early check, up to maxConcurrent-1 extra researchers could
|
|
301
|
+
// start past the stop threshold and waste a browser slot + an LLM call.
|
|
302
|
+
if (shouldStopResearch(sessionId, researchId)) {
|
|
303
|
+
const sessionService = await getService<ResearchSessionService>(ServiceNames.RESEARCH_SESSION_SERVICE, ctx, container);
|
|
304
|
+
await sessionService.abortAllSessions(researchId);
|
|
305
|
+
throw new Error(getResearchStopMessage(sessionId, researchId));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Enforce the concurrency cap before launching the next researcher.
|
|
309
|
+
// Wait for one slot to free up when we're at capacity, or abort immediately if signalled.
|
|
310
|
+
if (active.size >= maxConcurrent) {
|
|
311
|
+
if (signal?.aborted) break;
|
|
312
|
+
// Hoist onAbort so the listener is removed once the race settles normally (a researcher
|
|
313
|
+
// finished). The previous {once:true} listener was only cleaned up if abort actually
|
|
314
|
+
// fired, so every capacity-wait that resolved by completion leaked a listener on `signal`
|
|
315
|
+
// — accumulating across a long run toward MaxListenersExceededWarning.
|
|
316
|
+
let onAbort: (() => void) | undefined;
|
|
317
|
+
const abortPromise = signal ? new Promise<void>((_, reject) => {
|
|
318
|
+
onAbort = () => reject(new Error('Aborted'));
|
|
319
|
+
if (signal.aborted) onAbort();
|
|
320
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
321
|
+
}) : null;
|
|
322
|
+
try {
|
|
323
|
+
await Promise.race([...active, ...(abortPromise ? [abortPromise] : [])])
|
|
324
|
+
.catch(err => { if (err.message !== 'Aborted') throw err; });
|
|
325
|
+
} finally {
|
|
326
|
+
if (signal && onAbort) signal.removeEventListener('abort', onAbort);
|
|
327
|
+
}
|
|
328
|
+
if (signal?.aborted) break;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const promise = (async () => {
|
|
332
|
+
const id = String(configItem.id);
|
|
333
|
+
try {
|
|
334
|
+
const initialLinks = researcherLinks?.get(id) || [];
|
|
335
|
+
const historicalUrls = storeLinks?.get(id) || [];
|
|
336
|
+
|
|
337
|
+
await runResearcher({
|
|
338
|
+
...orchestratorOptions,
|
|
339
|
+
// Correct field mappings — orchestratorOptions.config is the app Config;
|
|
340
|
+
// the per-researcher plan item goes into 'config' (overriding the spread),
|
|
341
|
+
// and the app Config moves to 'researchConfig'.
|
|
342
|
+
config: configItem,
|
|
343
|
+
researchConfig: orchestratorOptions.config ?? getConfig(ctx.cwd),
|
|
344
|
+
round: currentRound,
|
|
345
|
+
planningService,
|
|
346
|
+
initialLinks,
|
|
347
|
+
historicalUrls,
|
|
348
|
+
signal,
|
|
349
|
+
excludeTools: orchestratorOptions.excludeTools,
|
|
350
|
+
});
|
|
351
|
+
} catch (err) {
|
|
352
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
353
|
+
// A user cancellation (quit mid-run) surfaces here as an 'Aborted' throw.
|
|
354
|
+
// That is a clean stop, not a researcher failure: don't log it at ERROR,
|
|
355
|
+
// don't count it toward the fast-stop threshold, and don't paint the TUI
|
|
356
|
+
// researcher slice red. Mirrors the guard in researcher-executor.ts.
|
|
357
|
+
if (signal?.aborted || errMsg === 'Aborted') {
|
|
358
|
+
logger.debug(`[ResearchOrchestrationService] Researcher ${id} cancelled (aborted).`);
|
|
359
|
+
} else {
|
|
360
|
+
logger.error(`[ResearchOrchestrationService] Researcher ${id} failed: ${errMsg}`);
|
|
361
|
+
// Record failure for stopping logic
|
|
362
|
+
recordResearcherFailure(sessionId, researchId, id);
|
|
363
|
+
// Notify observer
|
|
364
|
+
observer?.onResearcherFailure?.(id, errMsg);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
})();
|
|
368
|
+
|
|
369
|
+
// Prune the promise from the active set when it settles so Promise.race works
|
|
370
|
+
// correctly and active.size stays accurate for the concurrency cap.
|
|
371
|
+
promise.finally(() => active.delete(promise));
|
|
372
|
+
active.add(promise);
|
|
373
|
+
|
|
374
|
+
// Throttled launch to prevent resource spikes (stagger by RESEARCHER_LAUNCH_DELAY_MS,
|
|
375
|
+
// skip after last). Abortable: an inert setTimeout would keep the loop alive for up to
|
|
376
|
+
// RESEARCHER_LAUNCH_DELAY_MS per pending researcher after a cancel, delaying shutdown.
|
|
377
|
+
if (RESEARCHER_LAUNCH_DELAY_MS > 0 && researchers.indexOf(configItem) < researchers.length - 1) {
|
|
378
|
+
if (signal?.aborted) break;
|
|
379
|
+
await new Promise<void>((resolve) => {
|
|
380
|
+
const timer = setTimeout(() => {
|
|
381
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
382
|
+
resolve();
|
|
383
|
+
}, RESEARCHER_LAUNCH_DELAY_MS);
|
|
384
|
+
const onAbort = () => { clearTimeout(timer); resolve(); };
|
|
385
|
+
if (signal) {
|
|
386
|
+
if (signal.aborted) onAbort();
|
|
387
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
if (signal?.aborted) break;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (shouldStopResearch(sessionId, researchId)) {
|
|
394
|
+
const sessionService = await getService<ResearchSessionService>(ServiceNames.RESEARCH_SESSION_SERVICE, ctx, container);
|
|
395
|
+
// Abort sessions specifically for this researchId, not the whole piSessionId
|
|
396
|
+
await sessionService.abortAllSessions(researchId);
|
|
397
|
+
|
|
398
|
+
const stopMessage = getResearchStopMessage(sessionId, researchId);
|
|
399
|
+
throw new Error(stopMessage);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Wait for remaining researchers
|
|
404
|
+
await Promise.all(active);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Run a search burst for the given queries
|
|
409
|
+
* @param queries - Array of search queries
|
|
410
|
+
* @param config - Research configuration
|
|
411
|
+
* @param signal - Optional abort signal
|
|
412
|
+
* @param onProgress - Optional progress callback
|
|
413
|
+
* @param ctx - Optional context for container isolation
|
|
414
|
+
* @returns Search results
|
|
415
|
+
*/
|
|
416
|
+
async runSearchBurst(
|
|
417
|
+
queries: string[],
|
|
418
|
+
config: Config,
|
|
419
|
+
signal?: AbortSignal,
|
|
420
|
+
onProgress?: (links: number) => void,
|
|
421
|
+
ctx?: any
|
|
422
|
+
): Promise<QueryResultWithError[]> {
|
|
423
|
+
const container = tryGetServiceContainerFromCtx(ctx);
|
|
424
|
+
const results = await search(queries, config, signal, onProgress, container);
|
|
425
|
+
const totalResults = results.reduce((sum, r) => sum + (r.results?.length || 0), 0);
|
|
426
|
+
logger.info(`[ResearchOrchestrationService] Search burst completed. Total results: ${totalResults}`);
|
|
427
|
+
return results;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Store link descriptions to knowledge store for a specific round
|
|
432
|
+
* @param sessionId - Session identifier
|
|
433
|
+
* @param round - Round number
|
|
434
|
+
* @param researchId - Research ID
|
|
435
|
+
* @param config - Research configuration
|
|
436
|
+
* @param ctx - Optional extension context for container isolation
|
|
437
|
+
*/
|
|
438
|
+
async storeLinkDescriptions(_sessionId: string, round: number, researchId: string, config: Config, ctx?: any): Promise<void> {
|
|
439
|
+
// Knowledge store disabled — never load the native vector stack. Mirrors the
|
|
440
|
+
// gates in the quick orchestrator and scrape.ts; without this, resolving the
|
|
441
|
+
// service throws on platforms lacking the native @lancedb binding (e.g. Intel
|
|
442
|
+
// macOS) and needlessly opens LanceDB even when the user set MODE='none'.
|
|
443
|
+
if (config.KNOWLEDGE_STORE_MODE === 'none') return;
|
|
444
|
+
|
|
445
|
+
const container = tryGetServiceContainerFromCtx(ctx);
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
const ksService = await getService<IKnowledgeStoreService>(ServiceNames.KNOWLEDGE_STORE, ctx, container);
|
|
449
|
+
if (!ksService.isReady()) {
|
|
450
|
+
logger.debug('[ResearchOrchestrationService] Knowledge store not ready, skipping link descriptions');
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const synthesisService = await getService<IResearchSynthesisService>(ServiceNames.RESEARCH_SYNTHESIS_SERVICE, ctx, container);
|
|
455
|
+
const writer = await getService<IWriterQueue>(ServiceNames.WRITER_QUEUE, ctx, container);
|
|
456
|
+
|
|
457
|
+
const roundPrefix = `${round}.`;
|
|
458
|
+
let enqueued = 0;
|
|
459
|
+
let researcherCount = 0;
|
|
460
|
+
|
|
461
|
+
const allReports = synthesisService.getAllReports(researchId);
|
|
462
|
+
if (allReports.size === 0) {
|
|
463
|
+
logger.warn(`[ResearchOrchestrationService] No reports found in synthesis service for researchId ${researchId} round ${round}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
for (const [key, report] of allReports.entries()) {
|
|
467
|
+
if (!key.startsWith(roundPrefix)) continue;
|
|
468
|
+
researcherCount++;
|
|
469
|
+
|
|
470
|
+
const links = parseCitations(report);
|
|
471
|
+
if (links.length === 0) {
|
|
472
|
+
logger.warn(`[ResearchOrchestrationService] Researcher ${key} produced no parseable CITED LINKS - no descriptions stored`);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
logger.debug(`[ResearchOrchestrationService] Storing ${links.length} citations for researcher ${key}`);
|
|
477
|
+
|
|
478
|
+
for (const link of links) {
|
|
479
|
+
if (link.url) {
|
|
480
|
+
const cachedContent = getCachedScrapedContent(researchId, link.url) ?? undefined;
|
|
481
|
+
const markdown = link.description || `(source: ${link.url})`;
|
|
482
|
+
writer.enqueue({
|
|
483
|
+
url: normalizeUrl(link.url),
|
|
484
|
+
markdown: markdown,
|
|
485
|
+
content: cachedContent,
|
|
486
|
+
metadata: {
|
|
487
|
+
researchId,
|
|
488
|
+
round,
|
|
489
|
+
researcherId: key,
|
|
490
|
+
description: link.description || '',
|
|
491
|
+
sourceOrigin: link.url,
|
|
492
|
+
source: link.source || 'unknown',
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
enqueued++;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (enqueued > 0) {
|
|
501
|
+
logger.info(`[ResearchOrchestrationService] Enqueued ${enqueued} citations from ${researcherCount} researchers for round ${round}`);
|
|
502
|
+
await writer.drain();
|
|
503
|
+
} else if (researcherCount > 0) {
|
|
504
|
+
logger.warn(`[ResearchOrchestrationService] No valid citations found among ${researcherCount} researchers in round ${round}`);
|
|
505
|
+
}
|
|
506
|
+
} catch (err) {
|
|
507
|
+
logger.warn('[ResearchOrchestrationService] Failed to store link descriptions (non-fatal):', err);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Run the infrastructure health check and log its status. Purely ADVISORY:
|
|
514
|
+
* research never aborts on an unhealthy result. This is deliberate — the health
|
|
515
|
+
* check has historically produced false negatives (e.g. a single-endpoint DDG
|
|
516
|
+
* probe, or native deps unavailable on Intel macOS) and an abort-on-unhealthy
|
|
517
|
+
* would needlessly kill runs that would otherwise succeed on cached/partial
|
|
518
|
+
* data. The return type is `void` so a future edit can't quietly start
|
|
519
|
+
* "honoring" a boolean and reintroduce that false-negative abort.
|
|
520
|
+
* @param round - Current round number
|
|
521
|
+
* @param ctx - Optional extension context for container isolation
|
|
522
|
+
*/
|
|
523
|
+
async checkHealth(round: number, _researchId?: string, ctx?: any): Promise<void> {
|
|
524
|
+
if (round <= 1) return;
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const container = tryGetServiceContainerFromCtx(ctx);
|
|
528
|
+
let registry: IHealthRegistryService;
|
|
529
|
+
try {
|
|
530
|
+
registry = await getService<IHealthRegistryService>(ServiceNames.HEALTH_REGISTRY, ctx, container);
|
|
531
|
+
} catch {
|
|
532
|
+
registry = healthRegistry;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const health = await registry.runAll();
|
|
536
|
+
|
|
537
|
+
if (health.status === 'healthy') {
|
|
538
|
+
logger.debug(`[ResearchOrchestrationService] Health status at Round ${round}: [OK] All systems operational`);
|
|
539
|
+
} else {
|
|
540
|
+
// Degraded or unhealthy — log for visibility but never stop the run.
|
|
541
|
+
const failed = health.components.filter(c => !c.healthy).map(c => c.component);
|
|
542
|
+
const label = health.status === 'degraded' ? 'Degraded' : 'Unhealthy';
|
|
543
|
+
logger.warn(`[ResearchOrchestrationService] Health status at Round ${round}: [WARN] ${label} (${failed.join(', ')}). Continuing.`);
|
|
544
|
+
}
|
|
545
|
+
} catch (err) {
|
|
546
|
+
logger.warn('[ResearchOrchestrationService] Failed to check health status:', err);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|