@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,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Research Knowledge Search Types
|
|
3
|
+
*
|
|
4
|
+
* Strict contracts for the research_knowledge_search tool's internal data flow.
|
|
5
|
+
* Phase 1 of the architecture: rigid schema definitions that the
|
|
6
|
+
* background LLM MUST conform to, enforced via TypeBox validation
|
|
7
|
+
* and agentic repair.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Type, type Static, type TSchema } from 'typebox';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tri-state answer confidence.
|
|
14
|
+
*
|
|
15
|
+
* - `"yes"` — The documents contain a substantive, directly useful answer.
|
|
16
|
+
* - `"maybe"` — The documents contain partial or tangential information
|
|
17
|
+
* that *might* be helpful but is NOT sufficient for a complete answer.
|
|
18
|
+
* The host agent should still consider live research to fill gaps.
|
|
19
|
+
* - `"no"` — No relevant information found in the knowledge store.
|
|
20
|
+
*/
|
|
21
|
+
export const AnswerStatusEnum = Type.Union([
|
|
22
|
+
Type.Literal('yes'),
|
|
23
|
+
Type.Literal('maybe'),
|
|
24
|
+
Type.Literal('no'),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The internal schema the background LLM MUST output.
|
|
29
|
+
*
|
|
30
|
+
* - `answer_status`: Tri-state enum indicating answer confidence.
|
|
31
|
+
* - `"yes"`: Documents contain a substantive answer.
|
|
32
|
+
* - `"maybe"`: Documents contain partial/tangential info — useful but
|
|
33
|
+
* insufficient. The synthesis (if present) summarizes what's available.
|
|
34
|
+
* - `"no"`: No relevant information found.
|
|
35
|
+
* - `synthesis`: Optional synthesized answer with inline citation markers
|
|
36
|
+
* [1], [2], etc. Present when answer_status is "yes" or "maybe".
|
|
37
|
+
* - `citations`: Array of source URLs used to construct the answer.
|
|
38
|
+
*/
|
|
39
|
+
export const ResearchKnowledgeSynthesisResponseSchema = Type.Object({
|
|
40
|
+
answer_status: AnswerStatusEnum,
|
|
41
|
+
synthesis: Type.Optional(Type.String({
|
|
42
|
+
description: 'Synthesized answer with inline citation markers [1], [2], etc.',
|
|
43
|
+
})),
|
|
44
|
+
citations: Type.Array(Type.String(), {
|
|
45
|
+
description: 'Source URLs used to construct the answer.',
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export type ResearchKnowledgeSynthesisResponse = Static<typeof ResearchKnowledgeSynthesisResponseSchema>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The schema cast as TSchema for use with agentic-repair.
|
|
53
|
+
*/
|
|
54
|
+
export const ResearchKnowledgeSynthesisResponseSchemaAsTSchema =
|
|
55
|
+
ResearchKnowledgeSynthesisResponseSchema as unknown as TSchema;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Cheap relevance-triage response.
|
|
59
|
+
*
|
|
60
|
+
* The triage LLM reads only the SHORT per-candidate descriptions (a few hundred
|
|
61
|
+
* chars each) — not the full rebuilt documents — and returns the 0-based indices
|
|
62
|
+
* of the candidates that are genuinely relevant to the user's question. An empty
|
|
63
|
+
* array means "nothing in the store is relevant" → the tool reports an instant
|
|
64
|
+
* miss WITHOUT rebuilding documents or running the full synthesis LLM.
|
|
65
|
+
*
|
|
66
|
+
* This replaces the earlier idea of an embedding-similarity threshold, which was
|
|
67
|
+
* shown to be neither model- nor store-size-robust (a large anisotropic vector
|
|
68
|
+
* space gives every query a spuriously-close nearest neighbour). Letting the LLM
|
|
69
|
+
* judge the descriptions is model-agnostic and adapts to any embedding model.
|
|
70
|
+
*/
|
|
71
|
+
export const KnowledgeRelevanceTriageSchema = Type.Object({
|
|
72
|
+
relevant_indices: Type.Array(Type.Integer(), {
|
|
73
|
+
description: '0-based indices of the candidate sources that are relevant to the question. Empty if none.',
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export type KnowledgeRelevanceTriage = Static<typeof KnowledgeRelevanceTriageSchema>;
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Research Tool Definition
|
|
3
|
+
*
|
|
4
|
+
* Defines the research tool that orchestrates web/internet research.
|
|
5
|
+
* Depth 1-3: AI-orchestrated multi-session research (coordinator → researchers → evaluator).
|
|
6
|
+
*
|
|
7
|
+
* Note: Depth 0 (quick mode) is only available via the SDK / CLI (`--depth 0`,
|
|
8
|
+
* which the agent skill can pass). The pi extension tool has minimum: 1.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
ToolDefinition,
|
|
13
|
+
AgentToolResult,
|
|
14
|
+
ExtensionContext,
|
|
15
|
+
AgentToolUpdateCallback,
|
|
16
|
+
} from '@earendil-works/pi-coding-agent';
|
|
17
|
+
import type { ExtendedExtensionContext } from '../types/extension-context.ts';
|
|
18
|
+
import type { ResearchDepth, CleanupContext } from '../types/index.ts';
|
|
19
|
+
import { Type, type Static } from 'typebox';
|
|
20
|
+
import { validateConfig, getConfig, type ConfigInterface } from '../config.ts';
|
|
21
|
+
import { tryGetServiceContainerFromCtx, getService } from '../core/service-registry.ts';
|
|
22
|
+
|
|
23
|
+
import { ServiceNames } from '../core/service-interfaces.ts';
|
|
24
|
+
import type { IResearchOrchestration, IResearchSynthesisService } from '../core/service-interfaces.ts';
|
|
25
|
+
import { metrics, MetricsRegistry, runWithRunRegistry } from '../utils/metrics.ts';
|
|
26
|
+
import { createResearchRunId, logger, createLogger, isVerboseFromEnv, runWithLogger } from '../logger.ts';
|
|
27
|
+
import { exportResearchReport, appendExportMessage } from '../utils/research-export.ts';
|
|
28
|
+
import { validateAndSanitizeQuery } from '../utils/input-validation.ts';
|
|
29
|
+
import { validateInitialLinks, MAX_INITIAL_LINKS, MAX_INITIAL_LINK_CHARS } from '../utils/url-utils.ts';
|
|
30
|
+
import { startResearchSession, registerSessionAbort, clearSteeringMessages, getPiActivePanels } from '../orchestration/session-state.ts';
|
|
31
|
+
import { createResearchTuiManager, hideWorkingIndicator } from '../tui/research-tui-manager.ts';
|
|
32
|
+
import { createCleanupFunction } from '../cleanup/research-cleanup.ts';
|
|
33
|
+
import { createResearchObserver, createObserverState, stopObserverWaveAnimation } from '../observers/research-observer-impl.ts';
|
|
34
|
+
import { HeadlessObserver } from '../orchestration/headless-observer.ts';
|
|
35
|
+
|
|
36
|
+
import { ensureFunctionalHealth, createHealthMonitor } from '../tui/research-health.ts';
|
|
37
|
+
import { ErrorTracker, runWithTracker, type ErrorReport } from '../utils/error-tracker.ts';
|
|
38
|
+
import { extractRunStats, buildResearchSummary } from '../utils/metrics-summary.ts';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Append a unified research summary to the result.
|
|
42
|
+
* Replaces the old separate error + scrape summaries.
|
|
43
|
+
*
|
|
44
|
+
* Design: No tables, no percentages — just counts.
|
|
45
|
+
* Emphasizes volume of work done. Errors are a concise footnote when present.
|
|
46
|
+
*/
|
|
47
|
+
function appendResearchSummary(
|
|
48
|
+
result: string,
|
|
49
|
+
metricsSnapshot: { counters?: Record<string, number>; histograms?: Record<string, any> },
|
|
50
|
+
errorReport: ErrorReport,
|
|
51
|
+
): string {
|
|
52
|
+
const stats = extractRunStats(metricsSnapshot as any);
|
|
53
|
+
|
|
54
|
+
if (!stats) {
|
|
55
|
+
// The run produced no meaningful metrics (hard failure before any research ran).
|
|
56
|
+
// The error tracker is the only remaining signal, so surface its count here. This
|
|
57
|
+
// path involves no scrape fallback chains, so it is not subject to the per-URL
|
|
58
|
+
// over-counting that made the tracker unsuitable as the normal error count.
|
|
59
|
+
if (errorReport.totalErrors > 0) {
|
|
60
|
+
return result + `\n\n---\n\n*${errorReport.totalErrors} error${errorReport.totalErrors > 1 ? 's' : ''} encountered during research.*`;
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// NOTE: stats.errors is intentionally left as extractRunStats computed it (genuine
|
|
66
|
+
// engine faults only). It is NOT overridden with errorReport.totalErrors: the
|
|
67
|
+
// tracker records ~5-6 entries per failed URL along the fetch→browser fallback
|
|
68
|
+
// chain, so using it here reported blocked/unavailable sources (already shown as
|
|
69
|
+
// "not scraped") as a large, inflated error count. Keeping the metric-derived count
|
|
70
|
+
// also makes the report footnote consistent with the SDK (getLastRunStats) and the
|
|
71
|
+
// session-metrics TUI, which both read extractRunStats().errors.
|
|
72
|
+
|
|
73
|
+
const summary = buildResearchSummary(stats);
|
|
74
|
+
if (!summary) {
|
|
75
|
+
if (errorReport.totalErrors > 0) {
|
|
76
|
+
return result + `\n\n---\n\n*${errorReport.totalErrors} error${errorReport.totalErrors > 1 ? 's' : ''} encountered during research.*`;
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result + `\n\n---\n\n${summary}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create the research tool definition
|
|
86
|
+
*/
|
|
87
|
+
export function createResearchTool(iface?: ConfigInterface): ToolDefinition {
|
|
88
|
+
const parameters = Type.Object({
|
|
89
|
+
query: Type.String({
|
|
90
|
+
description: 'The research topic or query to investigate.',
|
|
91
|
+
}),
|
|
92
|
+
depth: Type.Optional(Type.Integer({
|
|
93
|
+
minimum: 1,
|
|
94
|
+
maximum: 3,
|
|
95
|
+
description: [
|
|
96
|
+
'Research depth (1-3).',
|
|
97
|
+
'1: Normal (coordinated, thorough).',
|
|
98
|
+
'2: Deep (multi-round, exhaustive).',
|
|
99
|
+
'3: Ultra (maximum depth, extreme rigor).',
|
|
100
|
+
].join('\n'),
|
|
101
|
+
default: 1,
|
|
102
|
+
})),
|
|
103
|
+
model: Type.Optional(Type.String({
|
|
104
|
+
description: 'Optional model ID override for coordination.',
|
|
105
|
+
})),
|
|
106
|
+
excludeTools: Type.Optional(Type.Array(Type.String(), {
|
|
107
|
+
description: 'List of internal tools to disable (e.g., search, scrape, security_search).',
|
|
108
|
+
})),
|
|
109
|
+
// Bounded exactly like the CLI's --initial-links flag (validated again in
|
|
110
|
+
// execute via the shared validateInitialLinks — the schema alone is not
|
|
111
|
+
// enforced on every caller path).
|
|
112
|
+
initialLinks: Type.Optional(Type.Array(Type.String({ maxLength: MAX_INITIAL_LINK_CHARS }), {
|
|
113
|
+
maxItems: MAX_INITIAL_LINKS,
|
|
114
|
+
description: `Optional seed URLs to investigate before (or instead of) web search. http(s) only; at most ${MAX_INITIAL_LINKS} URLs of up to ${MAX_INITIAL_LINK_CHARS} characters each.`,
|
|
115
|
+
})),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
type ResearchParams = Static<typeof parameters>;
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
name: 'research',
|
|
122
|
+
label: 'Research',
|
|
123
|
+
description:
|
|
124
|
+
'Perform multi-source web research using search, scraping, and specialized databases.',
|
|
125
|
+
promptSnippet: 'Execute comprehensive web research',
|
|
126
|
+
parameters,
|
|
127
|
+
renderShell: 'self',
|
|
128
|
+
executionMode: 'parallel',
|
|
129
|
+
prepareArguments: (args: unknown) => {
|
|
130
|
+
const rawArgs = args as Record<string, unknown>;
|
|
131
|
+
const normalized: Record<string, unknown> = {
|
|
132
|
+
query: rawArgs['query'] ?? '',
|
|
133
|
+
model: rawArgs['model'],
|
|
134
|
+
excludeTools: rawArgs['excludeTools'],
|
|
135
|
+
initialLinks: rawArgs['initialLinks'],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const rawDepth = rawArgs['depth'];
|
|
139
|
+
if (rawDepth !== undefined && rawDepth !== null) {
|
|
140
|
+
if (typeof rawDepth === 'string') {
|
|
141
|
+
const parsed = parseInt(rawDepth, 10);
|
|
142
|
+
normalized['depth'] = isNaN(parsed) ? 1 : Math.max(1, Math.min(3, parsed));
|
|
143
|
+
} else if (typeof rawDepth === 'number') {
|
|
144
|
+
normalized['depth'] = Math.max(1, Math.min(3, rawDepth));
|
|
145
|
+
} else {
|
|
146
|
+
normalized['depth'] = 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return normalized;
|
|
151
|
+
},
|
|
152
|
+
async execute(
|
|
153
|
+
_toolCallId: string,
|
|
154
|
+
params: unknown,
|
|
155
|
+
aborted: AbortSignal | undefined,
|
|
156
|
+
onUpdate: AgentToolUpdateCallback<any>,
|
|
157
|
+
ctx: ExtensionContext,
|
|
158
|
+
): Promise<AgentToolResult<unknown>> {
|
|
159
|
+
// 1. System Readiness Check
|
|
160
|
+
const container = tryGetServiceContainerFromCtx(ctx);
|
|
161
|
+
if (!container.isReady && process.env['PI_RESEARCH_FORCE_READY'] !== 'true') {
|
|
162
|
+
return {
|
|
163
|
+
content: [{
|
|
164
|
+
type: 'text',
|
|
165
|
+
text: '**Research system is not ready.**\n\nOne or more critical services failed to initialize during startup. Please check the logs for error details or try restarting the extension.\n\nYou can also run `/research-config health` to diagnose the issue.'
|
|
166
|
+
}],
|
|
167
|
+
details: { error: 'system_not_ready' }
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const { query, depth: rawDepth, model: modelId, excludeTools: paramExcludeTools, initialLinks } = params as ResearchParams;
|
|
172
|
+
|
|
173
|
+
// Enforce the same bounds the CLI applies to --initial-links: these links are
|
|
174
|
+
// templated verbatim into the researcher prompt as trusted seed evidence, so
|
|
175
|
+
// they must be http(s)-only, length-bounded, and count-capped on this path too
|
|
176
|
+
// (the TypeBox schema declares the bounds, but is not enforced by every caller).
|
|
177
|
+
if (initialLinks && initialLinks.length > 0) {
|
|
178
|
+
const linkError = validateInitialLinks(initialLinks);
|
|
179
|
+
if (linkError) {
|
|
180
|
+
return {
|
|
181
|
+
content: [{ type: 'text', text: `Error: invalid initialLinks — ${linkError}.` }],
|
|
182
|
+
details: { error: 'invalid_initial_links' },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const depth = rawDepth ?? Math.max(1, getConfig(ctx.cwd, iface).DEFAULT_RESEARCH_DEPTH) as 1 | 2 | 3;
|
|
188
|
+
const eCtx = ctx as ExtendedExtensionContext;
|
|
189
|
+
const parentExcludeTools = eCtx.excludeTools || [];
|
|
190
|
+
const excludeTools = [...new Set([...(paramExcludeTools || []), ...parentExcludeTools])];
|
|
191
|
+
|
|
192
|
+
const researchId = createResearchRunId();
|
|
193
|
+
const piSessionId = eCtx.sessionId || eCtx.sessionManager?.getSessionId() || 'default';
|
|
194
|
+
logger.debug(`[research] Initializing session IDs: piSessionId=${piSessionId}, researchId=${researchId}`);
|
|
195
|
+
|
|
196
|
+
const internalAbort = new AbortController();
|
|
197
|
+
let tuiManager: ReturnType<typeof createResearchTuiManager> | null = null;
|
|
198
|
+
let healthMonitorInstance: ReturnType<typeof createHealthMonitor> | null = null;
|
|
199
|
+
let observer: any;
|
|
200
|
+
let observerState: any;
|
|
201
|
+
let panelState: any;
|
|
202
|
+
// Assigned only after the TUI panel and observers are wired up. A failure
|
|
203
|
+
// before that point (e.g. the pre-run health check) leaves it undefined,
|
|
204
|
+
// so teardownUi() guards before calling it.
|
|
205
|
+
let cleanup: (() => Promise<void>) | undefined;
|
|
206
|
+
// Removes the listener attached to the parent abort signal (below), so a
|
|
207
|
+
// normally-completing run does not leave a handler (closing over
|
|
208
|
+
// internalAbort) attached to a parent signal that may outlive this call.
|
|
209
|
+
let detachAbortListener: (() => void) | undefined;
|
|
210
|
+
|
|
211
|
+
// Single idempotent UI teardown. Runs on every exit path — including a
|
|
212
|
+
// failure that happens before the inner research loop begins (model
|
|
213
|
+
// resolution, health check, observer setup) — so the on-screen research
|
|
214
|
+
// panel is never orphaned empty. The inner finally and the outer finally
|
|
215
|
+
// both call this; the guard makes the second call a no-op.
|
|
216
|
+
let uiTornDown = false;
|
|
217
|
+
const teardownUi = async (): Promise<void> => {
|
|
218
|
+
if (uiTornDown) return;
|
|
219
|
+
uiTornDown = true;
|
|
220
|
+
|
|
221
|
+
if (healthMonitorInstance) {
|
|
222
|
+
try { healthMonitorInstance.stop(); } catch (e) { logger.error('[research] Health monitor stop failed:', e); }
|
|
223
|
+
}
|
|
224
|
+
if (observerState && panelState) {
|
|
225
|
+
try { stopObserverWaveAnimation(observerState, panelState); } catch (e) { logger.error('[research] Stop wave animation failed:', e); }
|
|
226
|
+
}
|
|
227
|
+
// Steering messages are shared per pi-session. Only reset them when THIS is
|
|
228
|
+
// the last active run — otherwise a finishing run wipes a concurrent sibling
|
|
229
|
+
// run's queued/active steering mid-flight. This run's panel is still
|
|
230
|
+
// registered here (endResearchSession runs later in cleanup()), so a count
|
|
231
|
+
// of <= 1 means no other run is active.
|
|
232
|
+
try {
|
|
233
|
+
if (getPiActivePanels(piSessionId).length <= 1) clearSteeringMessages(piSessionId);
|
|
234
|
+
} catch (e) { logger.error('[research] Clear steering messages failed:', e); }
|
|
235
|
+
if (detachAbortListener) {
|
|
236
|
+
try { detachAbortListener(); } catch { /* best-effort */ } finally { detachAbortListener = undefined; }
|
|
237
|
+
}
|
|
238
|
+
if (cleanup) {
|
|
239
|
+
try { await cleanup(); } catch (cleanupError) { logger.error('[research] Cleanup failed:', cleanupError); }
|
|
240
|
+
}
|
|
241
|
+
// Disposes the master TUI widget — this is what removes the panel from
|
|
242
|
+
// the screen. Must always run or the empty panel leaks.
|
|
243
|
+
if (tuiManager) {
|
|
244
|
+
try { tuiManager.dispose(); } catch (disposeError) { logger.error('[research] TUI dispose failed:', disposeError); }
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// A query is always required — initialLinks seed URLs for a query but cannot
|
|
249
|
+
// replace it (validateAndSanitizeQuery below rejects an empty query anyway;
|
|
250
|
+
// failing here keeps the message clear and matches the CLI's up-front check).
|
|
251
|
+
if (!query) {
|
|
252
|
+
return { content: [{ type: 'text', text: 'Error: research requires a query; initialLinks seed URLs for a query but cannot replace it.' }], details: {} };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Each run gets its own isolated registry; session-level counter is incremented
|
|
256
|
+
// here, outside the run context, so it lands in the session registry.
|
|
257
|
+
const runRegistry = new MetricsRegistry();
|
|
258
|
+
const runStartedAt = Date.now();
|
|
259
|
+
metrics.increment('session_runs_started_total');
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const researchRunResult = await runWithRunRegistry<{ result: string; tokens: number; researchId: string }>(runRegistry, () =>
|
|
263
|
+
logger.runCapturingStderr(async () => {
|
|
264
|
+
const config = getConfig(ctx.cwd, iface);
|
|
265
|
+
validateConfig(config);
|
|
266
|
+
|
|
267
|
+
const sanitizedQuery = validateAndSanitizeQuery(query);
|
|
268
|
+
|
|
269
|
+
// Get orchestration service
|
|
270
|
+
const orch = await getService<IResearchOrchestration>(ServiceNames.RESEARCH_ORCHESTRATION, ctx, container);
|
|
271
|
+
|
|
272
|
+
// Resolve model using centralized service logic
|
|
273
|
+
const selectedModel = await orch.resolveResearchModel({
|
|
274
|
+
ctx,
|
|
275
|
+
query: sanitizedQuery,
|
|
276
|
+
model: modelId ? { id: modelId } as any : undefined,
|
|
277
|
+
config,
|
|
278
|
+
sessionId: piSessionId,
|
|
279
|
+
researchId,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
logger.info(`[research] Resolved research model: ${selectedModel.provider}/${selectedModel.id}`);
|
|
283
|
+
|
|
284
|
+
// Setup TUI or Headless Observer based on context
|
|
285
|
+
if (ctx.mode === 'tui' && ctx.hasUI) {
|
|
286
|
+
tuiManager = createResearchTuiManager({
|
|
287
|
+
piSessionId,
|
|
288
|
+
researchId,
|
|
289
|
+
query: sanitizedQuery,
|
|
290
|
+
modelId: selectedModel.id,
|
|
291
|
+
}, { ctx });
|
|
292
|
+
|
|
293
|
+
tuiManager.initializePanel();
|
|
294
|
+
panelState = tuiManager.panelState;
|
|
295
|
+
|
|
296
|
+
// Setup cleanup BEFORE the functional health check. The panel is
|
|
297
|
+
// already registered in session state (createResearchTuiManager →
|
|
298
|
+
// registerSessionPanel above), so if the health check throws — e.g.
|
|
299
|
+
// "the browser could not load a page in time" on a flaky connection —
|
|
300
|
+
// teardownUi() must have a cleanup function to call. Otherwise the
|
|
301
|
+
// panel is never removed from session state and leaks as a permanent
|
|
302
|
+
// "ghost" panel that stacks higher with every retry. (Root cause of
|
|
303
|
+
// observed multi-panel ghost stacking on repeated health-check failures.)
|
|
304
|
+
const cleanupCtx: CleanupContext = {
|
|
305
|
+
researchId: startResearchSession(piSessionId, researchId),
|
|
306
|
+
piSessionId,
|
|
307
|
+
masterWidgetId: tuiManager.masterWidgetId,
|
|
308
|
+
panelState,
|
|
309
|
+
waveTimer: null,
|
|
310
|
+
unsubOrder: null,
|
|
311
|
+
};
|
|
312
|
+
registerSessionAbort(piSessionId, cleanupCtx.researchId, internalAbort);
|
|
313
|
+
|
|
314
|
+
cleanup = createCleanupFunction(cleanupCtx, { ctx });
|
|
315
|
+
const { updateUnsubOrder } = await import('../cleanup/research-cleanup.ts');
|
|
316
|
+
updateUnsubOrder(cleanupCtx, tuiManager.unsubOrder);
|
|
317
|
+
|
|
318
|
+
// Perform health check — cleanup is now wired, so a failure here is
|
|
319
|
+
// torn down cleanly (panel removed) instead of leaking a ghost panel.
|
|
320
|
+
await ensureFunctionalHealth({
|
|
321
|
+
panelState,
|
|
322
|
+
onUpdate: () => tuiManager?.debouncedRefresh(),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Start health monitor for periodic checks
|
|
326
|
+
healthMonitorInstance = createHealthMonitor();
|
|
327
|
+
healthMonitorInstance.start();
|
|
328
|
+
|
|
329
|
+
// Setup TUI observer
|
|
330
|
+
observerState = createObserverState();
|
|
331
|
+
observer = createResearchObserver(
|
|
332
|
+
{ panelState, debouncedRefresh: () => tuiManager?.debouncedRefresh(), renderImmediate: () => tuiManager?.renderImmediate(), researchComplexity: depth ?? 1 },
|
|
333
|
+
observerState
|
|
334
|
+
);
|
|
335
|
+
} else {
|
|
336
|
+
// Headless mode (CLI / SDK)
|
|
337
|
+
observer = new HeadlessObserver({ enableLogging: true });
|
|
338
|
+
|
|
339
|
+
const sessionResearchId = startResearchSession(piSessionId, researchId);
|
|
340
|
+
registerSessionAbort(piSessionId, sessionResearchId, internalAbort);
|
|
341
|
+
|
|
342
|
+
cleanup = async () => {
|
|
343
|
+
clearSteeringMessages(piSessionId);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// Minimal stub — only stopObserverWaveAnimation touches panelState in the
|
|
347
|
+
// headless path (it clears wave fields). Run totals come from the metrics
|
|
348
|
+
// registry, so no token/cost state is needed here.
|
|
349
|
+
panelState = {};
|
|
350
|
+
observerState = {};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Handle abort signal. Track the handler so teardownUi() can detach it
|
|
354
|
+
// on normal completion (the parent signal may outlive this tool call).
|
|
355
|
+
if (aborted) {
|
|
356
|
+
const onParentAbort = () => internalAbort.abort();
|
|
357
|
+
aborted.addEventListener('abort', onParentAbort, { once: true });
|
|
358
|
+
detachAbortListener = () => aborted.removeEventListener('abort', onParentAbort);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Setup scoped logging — use AsyncLocalStorage so concurrent runs don't bleed
|
|
362
|
+
const researchLogger = createLogger({ researchRunId: researchId, verbose: isVerboseFromEnv() });
|
|
363
|
+
|
|
364
|
+
// Hide working indicator
|
|
365
|
+
hideWorkingIndicator(ctx);
|
|
366
|
+
|
|
367
|
+
// Use runWithTracker to ensure all errors tracked within this async branch
|
|
368
|
+
// are isolated to this research run's tracker.
|
|
369
|
+
const sessionTracker = new ErrorTracker();
|
|
370
|
+
const researchRunResult = await runWithTracker(sessionTracker, () => runWithLogger(researchLogger, async () => {
|
|
371
|
+
try {
|
|
372
|
+
// Run research via orchestration service
|
|
373
|
+
const result = await orch.runResearch({
|
|
374
|
+
ctx,
|
|
375
|
+
query: sanitizedQuery || (initialLinks?.[0] ?? 'Initial Links Research'),
|
|
376
|
+
depth: (depth ?? 1) as ResearchDepth,
|
|
377
|
+
model: selectedModel,
|
|
378
|
+
observer,
|
|
379
|
+
onUpdate,
|
|
380
|
+
sessionId: piSessionId,
|
|
381
|
+
researchId,
|
|
382
|
+
excludeTools,
|
|
383
|
+
initialLinks,
|
|
384
|
+
// Pass the interface-resolved config (getConfig(cwd, iface) at the top
|
|
385
|
+
// of this handler) so the RUN honors the same `pi.env` overlay already
|
|
386
|
+
// applied to model/depth/export. Without this the orchestrator falls
|
|
387
|
+
// back to getConfig(cwd) with no interface, silently ignoring a
|
|
388
|
+
// pi.env DISABLED_TOOLS / TIMEOUT_MS / concurrency override for the run.
|
|
389
|
+
config,
|
|
390
|
+
}, internalAbort.signal);
|
|
391
|
+
|
|
392
|
+
// Stop wave animation
|
|
393
|
+
if (observerState && panelState) stopObserverWaveAnimation(observerState, panelState);
|
|
394
|
+
|
|
395
|
+
// Append unified research summary (stats + errors). Tokens/cost are read
|
|
396
|
+
// from the run-scoped metrics registry — the single source of truth that
|
|
397
|
+
// is populated identically in TUI and headless mode (the old panelState
|
|
398
|
+
// tally was TUI-only and stayed 0 for every headless/SDK/print-mode run,
|
|
399
|
+
// and even in TUI omitted coordinator+evaluator spend). extractRunStats
|
|
400
|
+
// returns null only when nothing meaningful ran → 0.
|
|
401
|
+
const runSnapshot = runRegistry.getSnapshot();
|
|
402
|
+
const runStats = extractRunStats(runSnapshot);
|
|
403
|
+
const runTokens = runStats?.tokens ?? 0;
|
|
404
|
+
const runCost = runStats?.cost ?? 0;
|
|
405
|
+
const errorReport = sessionTracker.getReport();
|
|
406
|
+
const resultWithSummaries = appendResearchSummary(result, runSnapshot, errorReport);
|
|
407
|
+
|
|
408
|
+
let finalResult = resultWithSummaries;
|
|
409
|
+
const exportCfg = getConfig(ctx.cwd, iface);
|
|
410
|
+
if (exportCfg.RESEARCH_REPORT_EXPORT_ENABLED) {
|
|
411
|
+
const exportPath = await exportResearchReport(sanitizedQuery, resultWithSummaries, (depth ?? 1) <= 1 ? 'quick' : 'deep', ctx.cwd, exportCfg.RESEARCH_REPORT_EXPORT_DIR);
|
|
412
|
+
if (exportPath) {
|
|
413
|
+
finalResult = appendExportMessage(resultWithSummaries, exportPath, runCost);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Append research metadata (model used) at the very end, after metrics/summaries
|
|
418
|
+
const synthesisService = await getService<IResearchSynthesisService>(ServiceNames.RESEARCH_SYNTHESIS_SERVICE, ctx, container);
|
|
419
|
+
finalResult = synthesisService.appendMetadata(finalResult, selectedModel!.id);
|
|
420
|
+
|
|
421
|
+
return { result: finalResult, tokens: runTokens, researchId };
|
|
422
|
+
} catch (error) {
|
|
423
|
+
if (aborted?.aborted || internalAbort.signal.aborted) {
|
|
424
|
+
return { result: 'Research cancelled.', tokens: 0, researchId };
|
|
425
|
+
}
|
|
426
|
+
throw error;
|
|
427
|
+
} finally {
|
|
428
|
+
await teardownUi();
|
|
429
|
+
}
|
|
430
|
+
}));
|
|
431
|
+
return researchRunResult;
|
|
432
|
+
})); // end runCapturingStderr / runWithRunRegistry
|
|
433
|
+
|
|
434
|
+
// Snapshot the run registry
|
|
435
|
+
metrics.recordRunSummary({
|
|
436
|
+
runId: researchId,
|
|
437
|
+
startedAt: runStartedAt,
|
|
438
|
+
completedAt: Date.now(),
|
|
439
|
+
durationMs: Date.now() - runStartedAt,
|
|
440
|
+
status: 'success',
|
|
441
|
+
snapshot: runRegistry.getSnapshot(),
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
return { content: [{ type: 'text', text: researchRunResult.result }], details: { totalTokens: researchRunResult.tokens, researchId: researchRunResult.researchId } };
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (aborted?.aborted || internalAbort.signal.aborted) {
|
|
447
|
+
metrics.recordRunSummary({
|
|
448
|
+
runId: researchId,
|
|
449
|
+
startedAt: runStartedAt,
|
|
450
|
+
completedAt: Date.now(),
|
|
451
|
+
durationMs: Date.now() - runStartedAt,
|
|
452
|
+
status: 'cancelled',
|
|
453
|
+
snapshot: runRegistry.getSnapshot(),
|
|
454
|
+
});
|
|
455
|
+
return { content: [{ type: 'text', text: 'Research cancelled.' }], details: {} };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
metrics.recordRunSummary({
|
|
459
|
+
runId: researchId,
|
|
460
|
+
startedAt: runStartedAt,
|
|
461
|
+
completedAt: Date.now(),
|
|
462
|
+
durationMs: Date.now() - runStartedAt,
|
|
463
|
+
status: 'error',
|
|
464
|
+
snapshot: runRegistry.getSnapshot(),
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const errMsg = String(error).toLowerCase();
|
|
468
|
+
|
|
469
|
+
// Handle rate limits gracefully
|
|
470
|
+
if (errMsg.includes('429') || errMsg.includes('rate limit') || errMsg.includes('too many requests') || errMsg.includes('quota')) {
|
|
471
|
+
logger.warn('[research] Run halted gracefully due to rate limit:', error);
|
|
472
|
+
if (ctx.hasUI) {
|
|
473
|
+
ctx.ui.notify('Research halted: API rate limit reached', 'warning');
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
content: [{
|
|
477
|
+
type: 'text',
|
|
478
|
+
text: `[SYSTEM MESSAGE]: The research operation was halted gracefully because an API rate limit (HTTP 429) was reached. Please inform the user that the operation was stopped due to provider rate limits and they should wait a moment before trying again.\n\nDetails: ${String(error)}`
|
|
479
|
+
}],
|
|
480
|
+
details: {}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
logger.error('[research] run failed', error);
|
|
485
|
+
return { content: [{ type: 'text', text: `Research failed: ${String(error)}` }], details: {} };
|
|
486
|
+
} finally {
|
|
487
|
+
// Guarantees panel teardown even when the run fails before the inner
|
|
488
|
+
// research loop is reached (e.g. health-check failure), where the inner
|
|
489
|
+
// finally never executes. Idempotent: a no-op if teardown already ran.
|
|
490
|
+
await teardownUi();
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|