@lincoln504/pi-research 0.1.12 → 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 -115
- 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 +1036 -147
- 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 +264 -7
- 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 -174
- package/src/infrastructure/searxng-lifecycle.ts +0 -577
- 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,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planning Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for planning service
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logger } from '../logger.ts';
|
|
8
|
+
import { Value } from 'typebox/value';
|
|
9
|
+
import { extractJson } from '../utils/json-utils.ts';
|
|
10
|
+
import type { ResearchPlan, ResearcherConfig } from './interfaces/planning-interfaces.ts';
|
|
11
|
+
import { ResearchPlanSchema } from './interfaces/planning-interfaces.ts';
|
|
12
|
+
import {
|
|
13
|
+
MAX_TEAM_SIZE_LEVEL_1,
|
|
14
|
+
MAX_TEAM_SIZE_LEVEL_2,
|
|
15
|
+
MAX_TEAM_SIZE_LEVEL_3,
|
|
16
|
+
MAX_ROUNDS_LEVEL_1,
|
|
17
|
+
MAX_ROUNDS_LEVEL_2,
|
|
18
|
+
MAX_ROUNDS_LEVEL_3,
|
|
19
|
+
MAX_QUERIES_PER_RESEARCHER_LEVEL_1,
|
|
20
|
+
MAX_QUERIES_PER_RESEARCHER_LEVEL_2,
|
|
21
|
+
MAX_QUERIES_PER_RESEARCHER_LEVEL_3,
|
|
22
|
+
} from '../constants.ts';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the max team size for a complexity level
|
|
26
|
+
*/
|
|
27
|
+
export function getTeamSize(complexity: 1 | 2 | 3): number {
|
|
28
|
+
return complexity === 1 ? MAX_TEAM_SIZE_LEVEL_1
|
|
29
|
+
: complexity === 2 ? MAX_TEAM_SIZE_LEVEL_2
|
|
30
|
+
: MAX_TEAM_SIZE_LEVEL_3;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the query budget per researcher for a complexity level
|
|
35
|
+
*/
|
|
36
|
+
export function getQueryBudget(complexity: 1 | 2 | 3): number {
|
|
37
|
+
return complexity === 1 ? MAX_QUERIES_PER_RESEARCHER_LEVEL_1
|
|
38
|
+
: complexity === 2 ? MAX_QUERIES_PER_RESEARCHER_LEVEL_2
|
|
39
|
+
: MAX_QUERIES_PER_RESEARCHER_LEVEL_3;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the max rounds for a complexity level
|
|
44
|
+
*/
|
|
45
|
+
export function getMaxRounds(complexity: 1 | 2 | 3): number {
|
|
46
|
+
return complexity === 1 ? MAX_ROUNDS_LEVEL_1
|
|
47
|
+
: complexity === 2 ? MAX_ROUNDS_LEVEL_2
|
|
48
|
+
: MAX_ROUNDS_LEVEL_3;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get complexity-specific guidance for the coordinator
|
|
53
|
+
*/
|
|
54
|
+
export function getComplexityGuidance(complexity: 1 | 2 | 3, maxTeamSize: number, queryBudget: number): string {
|
|
55
|
+
if (complexity === 1) {
|
|
56
|
+
return '**Complexity: Level 1 (Normal)**. Conduct a thorough, well-rounded investigation of the topic covering the primary angles with adequate citations. **Plan ONLY Round 1** — a single comprehensive round. **Default to a single researcher** for most Level 1 topics — one focused researcher can handle the majority of normal-complexity queries efficiently. Use 2 researchers only when the topic clearly spans two distinct, non-overlapping domains that benefit from parallel investigation. This is a ONE-SHOT effort: plan Round 1 as if it is the only round, because it should be. Only Level 2 or Level 3 research should involve multiple rounds.';
|
|
57
|
+
} else if (complexity === 2) {
|
|
58
|
+
return `**Complexity: Level 2 (Deep)**. **ONLY for HIGH/EXTREME research needs.** Level 2 should be reserved for topics that clearly require deeper investigation than a single comprehensive round can provide. **Plan ONLY Round 1** — a thorough, well-structured first round. Subsequent rounds are reactive and only delegated when Round 1 findings reveal clear gaps that warrant deeper or broader coverage. Do not pre-plan multiple rounds; delegate follow-ups only when Round 1 demonstrates genuine need.
|
|
59
|
+
|
|
60
|
+
Scale your team (1-${maxTeamSize}) based on topic scope — not every round needs the full team. A single well-targeted researcher for follow-up gaps is usually sufficient.`;
|
|
61
|
+
} else {
|
|
62
|
+
return `**Complexity: Level 3 (Ultra)**. Perform an exhaustive, deep-dive research effort, leaving no stone unturned. **Plan ONLY Round 1** — make it comprehensive, deploying up to ${maxTeamSize} researchers with full query budgets (${queryBudget} each) covering all major dimensions of the topic. Round 1 should aim to cover the full scope comprehensively.
|
|
63
|
+
|
|
64
|
+
Rounds 2-3 are purely reactive — they only happen when Round 1 findings reveal gaps that warrant deeper diving into specific dimensions or broader exploration of adjacent topics. Do NOT pre-plan multiple rounds; let the findings drive the need. Think of Round 1 as the comprehensive landscape map, with follow-up rounds as targeted expeditions into areas that need more detail.
|
|
65
|
+
|
|
66
|
+
**ULTRA-SPECIFICITY MANDATE**: Level 3 demands granular, exhaustive detail on every fact that benefits from it — exact figures, dates, names, mechanisms, edge cases, historical context, technical specifics, and primary-source precision. Plan dedicated researchers for drilling into the ultra-specific dimensions of any finding where greater detail adds value.`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get complexity-specific guidance for the evaluator
|
|
72
|
+
*/
|
|
73
|
+
export function getEvaluatorComplexityGuidance(complexity: 1 | 2 | 3): string {
|
|
74
|
+
if (complexity === 1) {
|
|
75
|
+
return `**Level 1 (Normal)** - Thorough, well-rounded investigation with solid multi-source coverage.
|
|
76
|
+
|
|
77
|
+
- **SYNTHESIZE when**: The primary topic is covered from multiple angles with evidence from diverse sources and no significant gaps remain that would prevent a complete answer.
|
|
78
|
+
- **DELEGATE when**: Coverage is incomplete, important angles are missing, or additional sources would meaningfully strengthen the findings.
|
|
79
|
+
|
|
80
|
+
**DEFAULT PATH**: Synthesize after Round 1 if the researcher produced solid coverage of the core topic. **Level 1 should almost always be a single round** — only delegate a second round when Round 1 clearly missed important angles or lacks source diversity. A single researcher is the default for Level 1 follow-up rounds — only use 2 researchers if the remaining gaps clearly span two distinct domains. Level 2 research escalation should be reserved for truly high/extreme research needs only.`;
|
|
81
|
+
} else if (complexity === 2) {
|
|
82
|
+
return `**Level 2 (Deep)** - Thorough, multi-phase investigation with comprehensive citations.
|
|
83
|
+
|
|
84
|
+
- **SYNTHESIZE when**: You are confident the research is genuinely complete. This means multiple angles covered with substantial findings across all major topics, diverse sources cited throughout, and no significant gaps in coverage.
|
|
85
|
+
- **DELEGATE when**: ANY gaps remain in major topics, insufficient source diversity, missing details, or areas that need deeper exploration. Don't synthesize prematurely.
|
|
86
|
+
|
|
87
|
+
**DEFAULT PATH: When in doubt, DELEGATE**. It is better to conduct additional research rounds than to synthesize with incomplete findings. Level 2 is designed for multi-round research. Each round adds depth and citation diversity — but do not delegate unnecessarily. Synthesize after Round 1 ONLY if the researcher produced comprehensive coverage of the topic with good source diversity. If Round 1 has any gaps, delegate Round 2. A third round is only warranted when Round 2 still leaves significant holes. Scale researcher count to match the gaps — focused gaps may only need 1 researcher, while broad gaps benefit from the full team.`;
|
|
88
|
+
} else {
|
|
89
|
+
return `**Level 3 (Ultra)** - Exhaustive, comprehensive deep-dive with extensive citations.
|
|
90
|
+
|
|
91
|
+
- **SYNTHESIZE when**: You are confident the research is genuinely and exhaustively complete. This means exhaustively covered across ALL substantial avenues with multiple diverse sources per major topic, comprehensive citations throughout, and no meaningful gaps remain.
|
|
92
|
+
- **DELEGATE when**: ANY meaningful gaps, nuanced angles, insufficient source diversity, inadequate citations, or areas needing deeper investigation remain.
|
|
93
|
+
|
|
94
|
+
**DEFAULT PATH: When in doubt, DELEGATE**. It is better to conduct additional research rounds than to synthesize with incomplete findings. Level 3 has ${MAX_ROUNDS_LEVEL_3} rounds available. Be generous with follow-up delegation — lean toward using all available rounds. Each round adds breadth, depth, and citation diversity. Delegate for follow-up whenever remaining gaps or under-explored angles exist, even if progress has been good. Only synthesize when you have genuinely comprehensive coverage across all major areas with no meaningful gaps that another round would address.
|
|
95
|
+
|
|
96
|
+
**ULTRA-SPECIFICITY MANDATE**: For every fact, finding, or topic area where greater granularity adds value, delegate additional researchers to pursue it. This includes: exact figures and statistics, precise dates and timelines, technical mechanisms, named individuals and their specific contributions, primary-source verbatim data, edge cases, and any dimension where surface-level coverage would leave the reader with unanswered questions.`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get round phase guidance based on current round and max rounds
|
|
102
|
+
*/
|
|
103
|
+
export function getRoundPhaseGuidance(currentRound: number, maxRounds: number, complexity: 1 | 2 | 3, maxTeamSize: number): string {
|
|
104
|
+
const roundRatio = currentRound / maxRounds;
|
|
105
|
+
|
|
106
|
+
if (roundRatio <= 0.5) {
|
|
107
|
+
// Early rounds
|
|
108
|
+
if (complexity === 3) {
|
|
109
|
+
return `\n\n---\n\n**Round Phase: EARLY (Round ${currentRound} of ${maxRounds}) — Level 3 Ultra**\n\n**DELEGATE. Do not synthesize.** You are in the early phase of an exhaustive research effort with ${maxRounds} rounds available.\n- Deploy up to ${maxTeamSize} researchers across completely distinct angles — leave no major dimension unmapped\n- Not every round needs all ${maxTeamSize} researchers — use as many as the remaining gaps require\n- Fully saturate each researcher's query budget; partial budgets waste available depth\n- This phase exists to establish the broadest possible foundation of sources and findings\n- Do NOT synthesize under any circumstances this early`;
|
|
110
|
+
}
|
|
111
|
+
// Level 1 / Level 2 early
|
|
112
|
+
return `\n\n---\n\n**Round Phase: EARLY (Round ${currentRound} of ${maxRounds})**\n\nYou are in the early phase of research. Be more permissive with delegation:\n- Deploy researchers to broadly map the landscape\n- Not every round needs all ${maxTeamSize} researchers — use as many as the remaining gaps require\n- Don't worry if findings are incomplete — later rounds can fill gaps\n- Focus on breadth and initial exploration\n- Use available researchers to cover distinct angles in parallel`;
|
|
113
|
+
} else if (roundRatio <= 0.8) {
|
|
114
|
+
// Middle rounds
|
|
115
|
+
if (complexity === 3) {
|
|
116
|
+
return `\n\n---\n\n**Round Phase: MIDDLE (Round ${currentRound} of ${maxRounds}) — Level 3 Ultra**\n\n**PREFER DELEGATION.** You are in the middle phase of exhaustive research — use your remaining rounds.\n- Not every round needs all ${maxTeamSize} researchers — use as many as the remaining gaps require\n- With ${maxRounds - currentRound} round(s) remaining, delegate to cover angles not yet fully explored, drill into deeper sub-topics, or verify findings with additional sources\n- Be generous with follow-up — delegate whenever gaps or under-explored angles exist\n- Synthesize ONLY when you genuinely cannot identify gaps that another round would address\n- If you can name even one meaningful unexplored angle — DELEGATE`;
|
|
117
|
+
}
|
|
118
|
+
if (complexity === 2) {
|
|
119
|
+
return `\n\n---\n\n**Round Phase: MIDDLE (Round ${currentRound} of ${maxRounds})**\n\n**Level 2 (Deep) Guidance**: You are in the middle phase of deep research. Continue delegating when there are still meaningful gaps or areas to explore.\n\n- Not every round needs all ${maxTeamSize} researchers — use as many as the remaining gaps require\n- Synthesize only when findings are comprehensive and no significant gaps remain that warrant another round\n- Default to delegating when in doubt\n\n`;
|
|
120
|
+
}
|
|
121
|
+
// Level 1 middle
|
|
122
|
+
return `\n\n---\n\n**Round Phase: MIDDLE (Round ${currentRound} of ${maxRounds})**\n\nYou are in the middle phase of research. Apply balanced judgment:\n- Not every round needs all ${maxTeamSize} researchers — use as many as the remaining gaps require\n- Synthesize if you have substantial coverage of the key aspects\n- Delegate for significant gaps or to explore specialized sub-topics\n- Consider depth over breadth at this stage\n- Focus on rounding out incomplete areas`;
|
|
123
|
+
} else {
|
|
124
|
+
// Late rounds
|
|
125
|
+
if (complexity === 3) {
|
|
126
|
+
return `\n\n---\n\n**Round Phase: LATE (Round ${currentRound} of ${maxRounds}) — Level 3 Ultra**\n\nYou are in the late phase of exhaustive research. Still prefer delegation — use your remaining rounds.\n- Not every round needs all ${maxTeamSize} researchers — use as many as the remaining gaps require\n- Delegate if ANY meaningful dimension remains under-sourced, under-verified, or shallowly covered\n- Use remaining rounds to drill into specialist detail, verify findings, or explore nuanced angles surfaced by earlier rounds\n- Synthesize only when you have genuinely exhausted meaningful research avenues AND have comprehensive multi-source coverage across all major areas`;
|
|
127
|
+
}
|
|
128
|
+
// Level 1 / Level 2 late
|
|
129
|
+
return `
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
**Round Phase: LATE (Round ${currentRound} of ${maxRounds})**
|
|
134
|
+
|
|
135
|
+
You are in the late phase of research. Set a higher threshold for delegation:
|
|
136
|
+
- Not every round needs all ${maxTeamSize} researchers — use as many as the remaining gaps require\n- Synthesize if the core question is answerable with current findings
|
|
137
|
+
- Delegate only for CRITICAL gaps that cannot be resolved from existing findings
|
|
138
|
+
- Avoid delegating for minor details or marginal improvements
|
|
139
|
+
- Focus on delivering a complete, coherent response`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Parse JSON from LLM response into ResearchPlan
|
|
145
|
+
*/
|
|
146
|
+
export function parseJsonPlan(text: string): ResearchPlan {
|
|
147
|
+
const result = extractJson<unknown>(text, 'object');
|
|
148
|
+
if (!result.success || !result.value) {
|
|
149
|
+
const preview = text.length > 100 ? text.slice(0, 100) + '...' : text;
|
|
150
|
+
throw new Error(`Failed to extract valid JSON plan: ${result.error}. Raw response preview: "${preview}"`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Robust validation using TypeBox
|
|
154
|
+
try {
|
|
155
|
+
// 1. Convert/Coerce values (e.g. string numbers to numbers, single values to arrays if possible)
|
|
156
|
+
const coerced = Value.Convert(ResearchPlanSchema, result.value);
|
|
157
|
+
|
|
158
|
+
// 2. Validate against schema
|
|
159
|
+
if (!Value.Check(ResearchPlanSchema, coerced)) {
|
|
160
|
+
const errors = [...Value.Errors(ResearchPlanSchema, coerced)];
|
|
161
|
+
const errorMsg = errors.map((e: any) => `${e.path}: ${e.message}`).join(', ');
|
|
162
|
+
// If validation fails, we throw to satisfy tests and ensure system integrity
|
|
163
|
+
throw new Error(`Plan validation failed: ${errorMsg}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const plan = coerced as ResearchPlan;
|
|
167
|
+
|
|
168
|
+
// Invariant: only a 'synthesize' response may omit researchers. Every other action
|
|
169
|
+
// (delegate, or an unset action that defaults to delegation downstream) must carry a
|
|
170
|
+
// researchers array to act on. The schema relaxes `researchers` to optional so a valid
|
|
171
|
+
// synthesis ({ action, content }) is accepted instead of being thrown into the costly
|
|
172
|
+
// agentic-repair path; this guard preserves the delegate-must-have-researchers rule.
|
|
173
|
+
if (plan.action !== 'synthesize' && plan.researchers === undefined) {
|
|
174
|
+
throw new Error('Plan validation failed: /researchers: a non-synthesize plan must include a researchers array');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 3. Post-processing
|
|
178
|
+
if (plan.researchers) {
|
|
179
|
+
plan.researchers.forEach((r) => {
|
|
180
|
+
// Coerce IDs to strings for consistency
|
|
181
|
+
r.id = String(r.id);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return plan;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
188
|
+
throw new Error(`Plan validation error: ${msg}`, { cause: err });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Build a fallback coordinator plan when LLM fails
|
|
194
|
+
*/
|
|
195
|
+
export function buildFallbackCoordinatorPlan(serviceName: string, _rawText: string, query: string): ResearchPlan {
|
|
196
|
+
const words = query.split(/\s+/).slice(0, 6).join(' ');
|
|
197
|
+
const queries = [query, `${words} overview`, `${words} latest`].filter(Boolean).slice(0, 3);
|
|
198
|
+
logger.warn(`[${serviceName}] Coordinator fallback: single researcher for "${query.slice(0, 80)}"`);
|
|
199
|
+
return {
|
|
200
|
+
action: 'delegate',
|
|
201
|
+
researchers: [{ id: '1', name: 'General Researcher', goal: `Research the following query comprehensively: ${query}`, queries }],
|
|
202
|
+
allQueries: queries,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Cap researcher queries to stay within budget
|
|
208
|
+
*/
|
|
209
|
+
export function capResearcherQueries(plan: ResearchPlan, complexity: 1 | 2 | 3, serviceName: string): ResearchPlan {
|
|
210
|
+
const budget = getQueryBudget(complexity);
|
|
211
|
+
|
|
212
|
+
// Hard caps per round - based on actual maximum possible queries
|
|
213
|
+
// Level 1: 2 researchers x 10 queries = 20 maximum
|
|
214
|
+
// Level 2: 3 researchers x 15 queries = 45 maximum
|
|
215
|
+
// Level 3: 5 researchers x 20 queries = 100 maximum
|
|
216
|
+
const ROUND_HARD_CAP = complexity === 1 ? 20
|
|
217
|
+
: complexity === 2 ? 45
|
|
218
|
+
: 100;
|
|
219
|
+
|
|
220
|
+
if (!plan.researchers) return plan;
|
|
221
|
+
|
|
222
|
+
// 1. Normalize IDs to strings and cap individual researchers
|
|
223
|
+
// CRITICAL: Only keep researchers with non-empty queries to guarantee search results
|
|
224
|
+
const maxTeam = getTeamSize(complexity);
|
|
225
|
+
plan.researchers = plan.researchers
|
|
226
|
+
.filter(r => r && typeof r === 'object' && Array.isArray(r.queries) && r.queries.length > 0)
|
|
227
|
+
.slice(0, maxTeam)
|
|
228
|
+
.map(r => {
|
|
229
|
+
const normalized = { ...r, id: String(r.id) };
|
|
230
|
+
if (normalized.queries.length > budget) {
|
|
231
|
+
logger.warn(`[${serviceName}] Capping researcher ${normalized.id} queries: ${normalized.queries.length} → ${budget}`);
|
|
232
|
+
normalized.queries = normalized.queries.slice(0, budget);
|
|
233
|
+
}
|
|
234
|
+
return normalized;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// 1b. Enforce unique researcher IDs within the round. Reports are keyed `${round}.${id}`
|
|
238
|
+
// and search links by String(id); two researchers sharing an id (the LLM emitting `1.1`
|
|
239
|
+
// twice, or `1` and "1") collide last-writer-wins — both run but one's findings are silently
|
|
240
|
+
// dropped. Renumber collisions so every launched researcher's report is retained.
|
|
241
|
+
const seenIds = new Set<string>();
|
|
242
|
+
for (const r of plan.researchers) {
|
|
243
|
+
const rid = String(r.id);
|
|
244
|
+
if (!seenIds.has(rid)) { seenIds.add(rid); continue; }
|
|
245
|
+
let n = 2;
|
|
246
|
+
let candidate = `${rid}-${n}`;
|
|
247
|
+
while (seenIds.has(candidate)) { n++; candidate = `${rid}-${n}`; }
|
|
248
|
+
logger.warn(`[${serviceName}] Duplicate researcher id '${rid}' → renumbered '${candidate}'`);
|
|
249
|
+
r.id = candidate;
|
|
250
|
+
seenIds.add(candidate);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 2. Enforce global round budget
|
|
254
|
+
let totalQueries = plan.researchers.reduce((sum, r) => sum + r.queries.length, 0);
|
|
255
|
+
if (totalQueries > ROUND_HARD_CAP) {
|
|
256
|
+
logger.warn(`[${serviceName}] Total round queries (${totalQueries}) exceeds hard cap (${ROUND_HARD_CAP}). Trimming...`);
|
|
257
|
+
while (totalQueries > ROUND_HARD_CAP) {
|
|
258
|
+
let maxCount = 0;
|
|
259
|
+
let maxIdx = -1;
|
|
260
|
+
if (!plan.researchers) break;
|
|
261
|
+
for (let i = 0; i < plan.researchers.length; i++) {
|
|
262
|
+
if (plan.researchers[i]!.queries.length > maxCount) {
|
|
263
|
+
maxCount = plan.researchers[i]!.queries.length;
|
|
264
|
+
maxIdx = i;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (maxIdx === -1) break;
|
|
268
|
+
plan.researchers[maxIdx]!.queries.pop();
|
|
269
|
+
totalQueries--;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
plan.allQueries = plan.researchers.flatMap(r => r.queries);
|
|
274
|
+
|
|
275
|
+
// If no researchers remain with valid queries, force synthesis
|
|
276
|
+
if (plan.action === 'delegate' && plan.researchers.length === 0) {
|
|
277
|
+
logger.warn(`[${serviceName}] No valid researchers after query cap/filtering, forcing synthesize`);
|
|
278
|
+
return { ...plan, action: 'synthesize', researchers: [], allQueries: [] };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return plan;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Generate researcher configurations from a plan
|
|
286
|
+
*/
|
|
287
|
+
export function generateResearchers(plan: ResearchPlan, _query: string, _complexity: 1 | 2 | 3): ResearcherConfig[] {
|
|
288
|
+
if (!plan.researchers) {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Normalize IDs to strings
|
|
293
|
+
const researchers = plan.researchers.map(r => ({
|
|
294
|
+
...r,
|
|
295
|
+
id: String(r.id),
|
|
296
|
+
}));
|
|
297
|
+
|
|
298
|
+
return researchers;
|
|
299
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler Factory Interface
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract for creating scheduler instances.
|
|
5
|
+
* This interface lives in the Core layer to follow Clean Architecture principles.
|
|
6
|
+
*
|
|
7
|
+
* Implementations should be provided by the Infrastructure layer.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Config } from '../config.ts';
|
|
11
|
+
import type { IService } from './service-registry.ts';
|
|
12
|
+
import type { IScheduler } from './interfaces/scheduler-interfaces.ts';
|
|
13
|
+
export type { IScheduler };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Scheduler Factory Interface
|
|
17
|
+
*
|
|
18
|
+
* Provides methods for creating and managing scheduler instances.
|
|
19
|
+
* This interface allows the Core layer to depend on abstractions,
|
|
20
|
+
* not concrete implementations from the Infrastructure layer.
|
|
21
|
+
*/
|
|
22
|
+
export interface ISchedulerFactory extends IService {
|
|
23
|
+
/**
|
|
24
|
+
* Get or create a scheduler instance.
|
|
25
|
+
* Handles leader election and client/server mode switching.
|
|
26
|
+
*
|
|
27
|
+
* @param config Optional configuration
|
|
28
|
+
* @returns A scheduler instance
|
|
29
|
+
*/
|
|
30
|
+
getScheduler(config?: Config): Promise<IScheduler>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the current scheduler version string.
|
|
34
|
+
* The version is derived from the configuration and changes when config changes.
|
|
35
|
+
*
|
|
36
|
+
* @param config Optional configuration
|
|
37
|
+
* @returns The scheduler version string
|
|
38
|
+
*/
|
|
39
|
+
getSchedulerVersion(config?: Config): string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Force a restart of the scheduler by clearing the global cache and state.
|
|
43
|
+
* This should be called when configuration changes are detected.
|
|
44
|
+
*
|
|
45
|
+
* @param forceClearRemoteState Whether to force clear state from remote processes
|
|
46
|
+
*/
|
|
47
|
+
forceSchedulerRestart(forceClearRemoteState?: boolean): Promise<void>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler Service
|
|
3
|
+
*
|
|
4
|
+
* Coordinates the multi-process browser worker pool and task scheduling.
|
|
5
|
+
* Implements leader election to ensure only one browser pool is active.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ServiceLifecycle } from './service-registry.ts';
|
|
9
|
+
import { ServiceNames } from './service-interfaces.ts';
|
|
10
|
+
import type { IScheduler, ISchedulerService, ISchedulerInternals, ISchedulerInstance } from './service-interfaces.ts';
|
|
11
|
+
import { logger } from '../logger.ts';
|
|
12
|
+
import { raceWithDeadline } from '../utils/safe-unref.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Scheduler Service Implementation
|
|
16
|
+
*/
|
|
17
|
+
export class SchedulerService implements ISchedulerService, ISchedulerInternals {
|
|
18
|
+
readonly name = ServiceNames.SCHEDULER;
|
|
19
|
+
lifecycle = ServiceLifecycle.UNINITIALIZED;
|
|
20
|
+
|
|
21
|
+
private scheduler: IScheduler | null = null;
|
|
22
|
+
private initializationPromise: Promise<IScheduler> | null = null;
|
|
23
|
+
private schedulerVersion: string | null = null;
|
|
24
|
+
private pendingShutdownPromise: Promise<void> | null = null;
|
|
25
|
+
private restartInProgress = false;
|
|
26
|
+
|
|
27
|
+
async initialize(): Promise<void> {
|
|
28
|
+
this.lifecycle = ServiceLifecycle.INITIALIZED;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async dispose(): Promise<void> {
|
|
32
|
+
if (this.lifecycle === ServiceLifecycle.DISPOSED) return;
|
|
33
|
+
this.lifecycle = ServiceLifecycle.DISPOSING;
|
|
34
|
+
|
|
35
|
+
// Invalidate any in-flight initialization FIRST: the scheduler factory
|
|
36
|
+
// re-checks this slot (its "superseded" guard) after every await point,
|
|
37
|
+
// so nulling it here makes a mid-election init shut down its
|
|
38
|
+
// freshly-created scheduler (closing its HTTP server) instead of
|
|
39
|
+
// installing it onto a disposed service where nothing would ever tear it
|
|
40
|
+
// down. Then await the in-flight init (bounded — the election can block
|
|
41
|
+
// up to ~60s on the browser-init lock) so its product is torn down
|
|
42
|
+
// before we report disposal complete. The expected outcome is a
|
|
43
|
+
// rejection ("Initialization superseded"); swallow it.
|
|
44
|
+
const inFlightInit = this.initializationPromise;
|
|
45
|
+
this.initializationPromise = null;
|
|
46
|
+
if (inFlightInit) {
|
|
47
|
+
try {
|
|
48
|
+
await raceWithDeadline(inFlightInit.then(() => undefined, () => undefined), 10000);
|
|
49
|
+
} catch {
|
|
50
|
+
/* raceWithDeadline only rejects if the promise rejects; already swallowed above */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (this.scheduler) {
|
|
55
|
+
try {
|
|
56
|
+
await this.scheduler.shutdown();
|
|
57
|
+
} catch (err) {
|
|
58
|
+
logger.error('[SchedulerService] Error during scheduler shutdown:', err);
|
|
59
|
+
}
|
|
60
|
+
this.scheduler = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.lifecycle = ServiceLifecycle.DISPOSED;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the current scheduler instance if initialized
|
|
68
|
+
*/
|
|
69
|
+
getScheduler(): IScheduler | null {
|
|
70
|
+
return this.scheduler;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if the scheduler is ready
|
|
75
|
+
*/
|
|
76
|
+
isReady(): boolean {
|
|
77
|
+
return this.scheduler !== null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// ISchedulerInternals Implementation
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
getSchedulerInstance(): ISchedulerInstance | null {
|
|
85
|
+
return this.scheduler as unknown as ISchedulerInstance | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setSchedulerInstance(instance: ISchedulerInstance | null): void {
|
|
89
|
+
this.scheduler = instance as unknown as IScheduler | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getSchedulerVersion(): string | null {
|
|
93
|
+
return this.schedulerVersion;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setSchedulerVersion(version: string | null): void {
|
|
97
|
+
this.schedulerVersion = version;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getSchedulerInitializationPromise(): Promise<ISchedulerInstance> | null {
|
|
101
|
+
return this.initializationPromise as unknown as Promise<ISchedulerInstance> | null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
setSchedulerInitializationPromise(promise: Promise<ISchedulerInstance> | null): void {
|
|
105
|
+
this.initializationPromise = promise as unknown as Promise<IScheduler> | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getPendingShutdownPromise(): Promise<void> | null {
|
|
109
|
+
return this.pendingShutdownPromise;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setPendingShutdownPromise(promise: Promise<void> | null): void {
|
|
113
|
+
this.pendingShutdownPromise = promise;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
isSchedulerRestartInProgress(): boolean {
|
|
117
|
+
return this.restartInProgress;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setSchedulerRestartInProgress(inProgress: boolean): void {
|
|
121
|
+
this.restartInProgress = inProgress;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Initialization Module
|
|
3
|
+
*
|
|
4
|
+
* This module registers all services with the service registry.
|
|
5
|
+
* This is the central place for service configuration and initialization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { registerService, getService, disposeAllServices, type IService, getServiceContainer } from './service-registry.ts';
|
|
9
|
+
import type { ServiceContainer } from './service-registry.ts';
|
|
10
|
+
import { ServiceNames } from './service-interfaces.ts';
|
|
11
|
+
import type { IStateManager } from './interfaces/state-manager-interfaces.ts';
|
|
12
|
+
import { SchedulerService } from './scheduler-service.ts';
|
|
13
|
+
import { HealthCheckService } from './health-check-service.ts';
|
|
14
|
+
import { PlanningService } from './planning-service.ts';
|
|
15
|
+
import { logger } from '../logger.ts';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Register all core services with the service registry
|
|
19
|
+
*/
|
|
20
|
+
export function registerCoreServices(container: ServiceContainer = getServiceContainer()): void {
|
|
21
|
+
logger.debug('[ServiceInitialization] Registering core services...');
|
|
22
|
+
|
|
23
|
+
// Register Scheduler Service
|
|
24
|
+
registerService(
|
|
25
|
+
ServiceNames.SCHEDULER,
|
|
26
|
+
() => new SchedulerService(),
|
|
27
|
+
{
|
|
28
|
+
allowOverwrite: false,
|
|
29
|
+
enableLogging: true,
|
|
30
|
+
},
|
|
31
|
+
container
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Register Health Check Cache Service
|
|
35
|
+
registerService(
|
|
36
|
+
ServiceNames.HEALTH_CHECK_CACHE,
|
|
37
|
+
() => new HealthCheckService(),
|
|
38
|
+
{
|
|
39
|
+
allowOverwrite: false,
|
|
40
|
+
enableLogging: true,
|
|
41
|
+
},
|
|
42
|
+
container
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Register Planning Service
|
|
46
|
+
registerService(
|
|
47
|
+
ServiceNames.PLANNING,
|
|
48
|
+
() => new PlanningService(),
|
|
49
|
+
{
|
|
50
|
+
// (Planning must be available early — it is eager-initialized via the eagerServices list below.)
|
|
51
|
+
allowOverwrite: false,
|
|
52
|
+
enableLogging: true,
|
|
53
|
+
},
|
|
54
|
+
container
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
logger.debug('[ServiceInitialization] All core services registered');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Initialize all core services
|
|
62
|
+
* This is called early in the application startup
|
|
63
|
+
*
|
|
64
|
+
* @param ctx - Optional extension context to pass to services
|
|
65
|
+
* @param container - Service container instance
|
|
66
|
+
*/
|
|
67
|
+
export async function initializeCoreServices(ctx?: any, container: ServiceContainer = getServiceContainer()): Promise<{ success: boolean; initialized: string[]; failed: string[] }> {
|
|
68
|
+
logger.log('[ServiceInitialization] Initializing core services...');
|
|
69
|
+
|
|
70
|
+
const initialized: string[] = [];
|
|
71
|
+
const failed: string[] = [];
|
|
72
|
+
|
|
73
|
+
// Critical infrastructure services (always initialize early)
|
|
74
|
+
const criticalInfrastructure = [
|
|
75
|
+
{ name: ServiceNames.METRICS, label: 'Metrics Service' },
|
|
76
|
+
{ name: ServiceNames.PROCESS_LIFECYCLE, label: 'Process Lifecycle Service' },
|
|
77
|
+
{ name: ServiceNames.STATE_PATH_CONFIGURATION, label: 'State Path Configuration' },
|
|
78
|
+
{ name: ServiceNames.FILE_LOCK_SERVICE, label: 'File Lock Service' },
|
|
79
|
+
{ name: ServiceNames.STATE_BACKUP_MANAGER, label: 'State Backup Manager' },
|
|
80
|
+
{ name: ServiceNames.STATE_SESSION_MANAGER, label: 'State Session Manager' },
|
|
81
|
+
{ name: ServiceNames.STATE_BROWSER_MANAGER, label: 'State Browser Manager' },
|
|
82
|
+
{ name: ServiceNames.STATE_METRICS_COLLECTOR, label: 'State Metrics Collector' },
|
|
83
|
+
{ name: ServiceNames.STATE_VALIDATOR, label: 'State Validator' },
|
|
84
|
+
{ name: ServiceNames.GPU_RESOURCE_SERVICE, label: 'GPU Resource Service' },
|
|
85
|
+
{ name: ServiceNames.STATE_MANAGER, label: 'State Manager Service' },
|
|
86
|
+
{ name: ServiceNames.HEALTH_CHECK_CACHE, label: 'Health Check Cache Service' },
|
|
87
|
+
{ name: ServiceNames.HEALTH_REGISTRY, label: 'Health Registry Service' },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// Services requiring eager initialization — proactively getService'd below so
|
|
91
|
+
// they are constructed at startup rather than on first use.
|
|
92
|
+
const eagerServices = [
|
|
93
|
+
{ name: ServiceNames.PLANNING, label: 'Planning Service' },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
// Lazy services (initialized on first use)
|
|
97
|
+
const lazyServices = [
|
|
98
|
+
ServiceNames.SCHEDULER,
|
|
99
|
+
ServiceNames.KNOWLEDGE_STORE,
|
|
100
|
+
ServiceNames.RESEARCH_ORCHESTRATION,
|
|
101
|
+
ServiceNames.WORKER_POOL_MANAGER,
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// Initialize critical infrastructure services
|
|
106
|
+
logger.log('[ServiceInitialization] Initializing critical infrastructure services...');
|
|
107
|
+
for (const service of criticalInfrastructure) {
|
|
108
|
+
try {
|
|
109
|
+
logger.debug(`[ServiceInitialization] Initializing ${service.label}...`);
|
|
110
|
+
// getService will call initialize(ctx) if not already initialized
|
|
111
|
+
await getService<IService>(service.name, ctx, container);
|
|
112
|
+
initialized.push(service.label);
|
|
113
|
+
logger.debug(`[ServiceInitialization] ${service.label} initialized`);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const errorMsg = `${service.label} initialization failed`;
|
|
116
|
+
logger.error(`[ServiceInitialization] FAILED: ${errorMsg}:`, err);
|
|
117
|
+
failed.push(errorMsg);
|
|
118
|
+
// Continue with other services even if one fails
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Initialize eagerly-marked services
|
|
123
|
+
logger.log('[ServiceInitialization] Initializing eagerly-marked services...');
|
|
124
|
+
for (const service of eagerServices) {
|
|
125
|
+
try {
|
|
126
|
+
logger.debug(`[ServiceInitialization] Initializing ${service.label}...`);
|
|
127
|
+
await getService<IService>(service.name, ctx, container);
|
|
128
|
+
initialized.push(service.label);
|
|
129
|
+
logger.debug(`[ServiceInitialization] ${service.label} initialized`);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const errorMsg = `${service.label} initialization failed`;
|
|
132
|
+
logger.error(`[ServiceInitialization] FAILED: ${errorMsg}:`, err);
|
|
133
|
+
failed.push(errorMsg);
|
|
134
|
+
// Continue with other services even if one fails
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Log lazy services that will be initialized on demand
|
|
139
|
+
if (lazyServices.length > 0) {
|
|
140
|
+
logger.log(`[ServiceInitialization] ${lazyServices.length} services configured for lazy initialization: ${lazyServices.join(', ')}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Log summary
|
|
144
|
+
if (failed.length === 0) {
|
|
145
|
+
logger.log(`[ServiceInitialization] All ${initialized.length} critical services initialized successfully`);
|
|
146
|
+
} else {
|
|
147
|
+
logger.warn(`[ServiceInitialization] WARNING: ${initialized.length}/${initialized.length + failed.length} services initialized, ${failed.length} failed`);
|
|
148
|
+
for (const failure of failed) {
|
|
149
|
+
logger.warn(`[ServiceInitialization] - ${failure}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { success: failed.length === 0, initialized, failed };
|
|
154
|
+
} catch (err) {
|
|
155
|
+
logger.error('[ServiceInitialization] Failed to initialize core services:', err);
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Dispose all core services
|
|
162
|
+
* This is called during application shutdown
|
|
163
|
+
*
|
|
164
|
+
* @param container - Service container instance
|
|
165
|
+
*/
|
|
166
|
+
export async function disposeCoreServices(container: ServiceContainer = getServiceContainer()): Promise<void> {
|
|
167
|
+
logger.log('[ServiceInitialization] Disposing core services...');
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
// Clear embedding server registration before parallel disposal so the
|
|
171
|
+
// EmbeddingServer can deregister itself while the StateManager is still alive.
|
|
172
|
+
// Use tryGet, not getService: on a teardown that follows a partial/failed init
|
|
173
|
+
// (e.g. STATE_MANAGER itself failed to initialize and _doInit's catch calls shutdown),
|
|
174
|
+
// getService would CONSTRUCT-and-initialize a fresh StateManager just to dispose it —
|
|
175
|
+
// a shutdown-resurrection. Only touch an already-live instance.
|
|
176
|
+
try {
|
|
177
|
+
const stateManager = container.tryGet<IStateManager>(ServiceNames.STATE_MANAGER);
|
|
178
|
+
if (stateManager) {
|
|
179
|
+
await stateManager.clearEmbeddingServer();
|
|
180
|
+
logger.debug('[ServiceInitialization] Cleared embedding server state before disposal');
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Non-fatal: embedding server state will be cleaned up on next startup via PID check
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Dispose services in reverse dependency order
|
|
187
|
+
await disposeAllServices(container);
|
|
188
|
+
|
|
189
|
+
logger.log('[ServiceInitialization] Core services disposed successfully');
|
|
190
|
+
} catch (err) {
|
|
191
|
+
logger.error('[ServiceInitialization] Failed to dispose core services:', err);
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
}
|