@lincoln504/pi-research 0.1.13 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +55 -138
- package/dist/cli.mjs +23375 -0
- package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
- package/dist/prompts/research-knowledge-search-extractor.md +41 -0
- package/dist/prompts/research-tool-usage.md +92 -0
- package/dist/prompts/researcher.md +89 -0
- package/dist/prompts/system-coordinator.md +75 -0
- package/dist/prompts/system-lead-evaluator.md +118 -0
- package/dist/thread-worker.mjs +2611 -0
- package/dist/webgpu-probe.mjs +80 -0
- package/docs/AGENT-SKILL.md +130 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +211 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +122 -0
- package/docs/THIRD-PARTY-NOTICES.md +66 -0
- package/package.json +102 -36
- package/scripts/build.cjs +140 -0
- package/scripts/cleanup.cjs +149 -0
- package/scripts/prepare.cjs +28 -0
- package/scripts/setup.cjs +134 -0
- package/skills/pi-research/README.md +90 -0
- package/skills/pi-research/SKILL.md +102 -0
- package/skills/pi-research/references/configuration.md +77 -0
- package/skills/pi-research/scripts/run.mjs +237 -0
- package/src/cleanup/research-cleanup.ts +140 -0
- package/src/cli.ts +1195 -0
- package/src/config.ts +1032 -155
- package/src/constants.ts +115 -84
- package/src/core/health-check-service.ts +265 -0
- package/src/core/interfaces/health-check-interfaces.ts +50 -0
- package/src/core/interfaces/knowledge-interfaces.ts +190 -0
- package/src/core/interfaces/observer-interfaces.ts +67 -0
- package/src/core/interfaces/orchestration-interfaces.ts +86 -0
- package/src/core/interfaces/planning-interfaces.ts +100 -0
- package/src/core/interfaces/process-interfaces.ts +37 -0
- package/src/core/interfaces/research-plan-types.ts +52 -0
- package/src/core/interfaces/scheduler-interfaces.ts +91 -0
- package/src/core/interfaces/service-names.ts +39 -0
- package/src/core/interfaces/state-manager-interfaces.ts +29 -0
- package/src/core/interfaces/steering-interfaces.ts +31 -0
- package/src/core/llm/agentic-repair.ts +169 -0
- package/src/core/llm/inject-date.ts +52 -0
- package/src/core/llm/llm-timeout.ts +64 -0
- package/src/core/llm/llm-utils.ts +86 -0
- package/src/core/llm/model-registry-factory.ts +287 -0
- package/src/core/llm/pi-ai-completion.ts +29 -0
- package/src/core/llm/prompts.ts +50 -0
- package/src/core/llm/research-model-resolver.ts +91 -0
- package/src/core/planning-constants.ts +13 -0
- package/src/core/planning-service.ts +649 -0
- package/src/core/planning-utils.ts +299 -0
- package/src/core/scheduler-factory.ts +48 -0
- package/src/core/scheduler-service.ts +123 -0
- package/src/core/service-initialization.ts +194 -0
- package/src/core/service-interfaces.ts +48 -0
- package/src/core/service-registry.ts +659 -0
- package/src/healthcheck/index.ts +271 -265
- package/src/healthcheck/registry.ts +130 -0
- package/src/index.ts +639 -50
- package/src/infrastructure/browser/browser-cleanup.ts +309 -0
- package/src/infrastructure/browser/browser-client.ts +199 -0
- package/src/infrastructure/browser/browser-error-utils.ts +160 -0
- package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
- package/src/infrastructure/browser/browser-server.ts +174 -0
- package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
- package/src/infrastructure/browser/cleanup-utils.ts +155 -0
- package/src/infrastructure/browser/client-agent.ts +31 -0
- package/src/infrastructure/browser/config.ts +288 -0
- package/src/infrastructure/browser/ensure-browser.ts +205 -0
- package/src/infrastructure/browser/index.ts +37 -0
- package/src/infrastructure/browser/priority-task-queue.ts +226 -0
- package/src/infrastructure/browser/scheduler-factory.ts +354 -0
- package/src/infrastructure/browser/task-execution-service.ts +209 -0
- package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
- package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
- package/src/infrastructure/browser/thread-worker-types.ts +25 -0
- package/src/infrastructure/browser/thread-worker.mjs +2611 -0
- package/src/infrastructure/browser/thread-worker.ts +168 -0
- package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
- package/src/infrastructure/embedding/embedding-client.ts +205 -0
- package/src/infrastructure/embedding/embedding-factory.ts +324 -0
- package/src/infrastructure/embedding/embedding-server.ts +535 -0
- package/src/infrastructure/file-lock-service.ts +592 -0
- package/src/infrastructure/gpu-resource-service.ts +191 -0
- package/src/infrastructure/knowledge-store-service.ts +475 -0
- package/src/infrastructure/metrics-service.ts +129 -0
- package/src/infrastructure/process-lifecycle-service.ts +253 -0
- package/src/infrastructure/scheduler-factory-service.ts +58 -0
- package/src/infrastructure/service-initialization.ts +292 -0
- package/src/infrastructure/state/state-backup-manager.ts +290 -0
- package/src/infrastructure/state/state-browser-api.ts +62 -0
- package/src/infrastructure/state/state-browser-manager.ts +70 -0
- package/src/infrastructure/state/state-manager-service.ts +264 -0
- package/src/infrastructure/state/state-manager.ts +547 -0
- package/src/infrastructure/state/state-metrics.ts +92 -0
- package/src/infrastructure/state/state-migration.ts +103 -0
- package/src/infrastructure/state/state-path-configuration.ts +119 -0
- package/src/infrastructure/state/state-session-api.ts +94 -0
- package/src/infrastructure/state/state-session-manager.ts +127 -0
- package/src/infrastructure/state/state-validator.ts +93 -0
- package/src/infrastructure/types/state-types.ts +85 -0
- package/src/knowledge/chunker.ts +157 -0
- package/src/knowledge/embedder-init.ts +413 -0
- package/src/knowledge/embedder-utils.ts +296 -0
- package/src/knowledge/embedder.ts +592 -0
- package/src/knowledge/index.ts +126 -0
- package/src/knowledge/lancedb-loader.ts +35 -0
- package/src/knowledge/migration.ts +17 -0
- package/src/knowledge/model-config.ts +132 -0
- package/src/knowledge/onnx-env.ts +48 -0
- package/src/knowledge/store-operations.ts +291 -0
- package/src/knowledge/store-schema.ts +79 -0
- package/src/knowledge/store.ts +1251 -0
- package/src/knowledge/transformers-loader.ts +31 -0
- package/src/knowledge/webgpu-probe.mjs +80 -0
- package/src/knowledge/webgpu-viability.ts +255 -0
- package/src/knowledge/writer-queue.ts +252 -0
- package/src/logger.ts +316 -122
- package/src/observers/research-observer-impl.ts +459 -0
- package/src/orchestration/deep-research-orchestrator.ts +524 -773
- package/src/orchestration/deep-research-types.ts +47 -35
- package/src/orchestration/headless-observer.ts +114 -0
- package/src/orchestration/orchestration-types.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +463 -0
- package/src/orchestration/research-orchestration-service.ts +549 -0
- package/src/orchestration/research-session-service.ts +178 -0
- package/src/orchestration/research-synthesis-service.ts +348 -0
- package/src/orchestration/researcher-executor.ts +394 -0
- package/src/orchestration/researcher.ts +115 -42
- package/src/orchestration/service-initialization.ts +109 -0
- package/src/orchestration/session-context.ts +7 -3
- package/src/orchestration/session-state.ts +672 -0
- package/src/prompts/research-knowledge-relevance-triage.md +35 -0
- package/src/prompts/research-knowledge-search-extractor.md +41 -0
- package/src/prompts/research-tool-usage.md +63 -16
- package/src/prompts/researcher.md +59 -136
- package/src/prompts/system-coordinator.md +64 -13
- package/src/prompts/system-lead-evaluator.md +82 -189
- package/src/research-config.ts +872 -0
- package/src/sdk.ts +847 -0
- package/src/security/cisa-kev.ts +72 -9
- package/src/security/cvss.ts +92 -0
- package/src/security/ecosystem.ts +45 -0
- package/src/security/github-advisories.ts +168 -428
- package/src/security/github-advisory-types.ts +282 -0
- package/src/security/index.ts +63 -177
- package/src/security/nvd-client.ts +550 -0
- package/src/security/nvd-types.ts +179 -0
- package/src/security/osv-client.ts +246 -0
- package/src/security/osv-types.ts +285 -0
- package/src/security/types.ts +67 -53
- package/src/skill-install/skill-installer.ts +519 -0
- package/src/stackexchange/index.ts +81 -27
- package/src/stackexchange/output/compact.ts +7 -6
- package/src/stackexchange/output/table.ts +11 -7
- package/src/stackexchange/queries.ts +0 -10
- package/src/stackexchange/rest-client.ts +141 -66
- package/src/stackexchange/types.ts +0 -17
- package/src/tool.ts +15 -423
- package/src/tools/grep.ts +32 -310
- package/src/tools/health-tool-definition.ts +101 -0
- package/src/tools/index.ts +38 -9
- package/src/tools/research-knowledge-search.ts +974 -0
- package/src/tools/research-knowledge-types.ts +77 -0
- package/src/tools/research-tool-definition.ts +494 -0
- package/src/tools/scrape.ts +286 -388
- package/src/tools/search.ts +98 -153
- package/src/tools/security.ts +107 -80
- package/src/tools/stackexchange.ts +58 -35
- package/src/tools/youtube-transcript.ts +163 -0
- package/src/tui/knowledge-search-panel.ts +86 -0
- package/src/tui/pulse.ts +86 -0
- package/src/tui/research-health.ts +124 -0
- package/src/tui/research-panel-color-utils.ts +340 -0
- package/src/tui/research-panel-format.ts +54 -0
- package/src/tui/research-panel-state.ts +404 -0
- package/src/tui/research-panel-wave.ts +280 -0
- package/src/tui/research-panel.ts +339 -435
- package/src/tui/research-tui-manager.ts +233 -0
- package/src/tui/tui-controller.ts +137 -0
- package/src/types/extension-context.ts +18 -49
- package/src/types/index.ts +61 -0
- package/src/types/llm.ts +68 -29
- package/src/types/research-panel-types.ts +61 -0
- package/src/utils/circuit-breaker.ts +146 -0
- package/src/utils/citation-utils.ts +97 -0
- package/src/utils/disk-space-checker.ts +55 -0
- package/src/utils/error-tracker.ts +265 -0
- package/src/utils/host-config.ts +45 -0
- package/src/utils/input-validation.ts +8 -10
- package/src/utils/json-utils.ts +150 -16
- package/src/utils/llm-usage.ts +54 -0
- package/src/utils/log-rotation.ts +125 -0
- package/src/utils/log-utils.ts +159 -0
- package/src/utils/make-resource-loader.ts +3 -1
- package/src/utils/metrics-summary.ts +459 -0
- package/src/utils/metrics.ts +302 -0
- package/src/utils/research-export.ts +28 -12
- package/src/utils/safe-unref.ts +31 -0
- package/src/utils/shared-links.ts +195 -90
- package/src/utils/shutdown-manager.ts +65 -0
- package/src/utils/stdio-capture.ts +407 -0
- package/src/utils/text-utils.ts +258 -11
- package/src/utils/tool-usage-tracker.ts +83 -16
- package/src/utils/url-utils.ts +255 -0
- package/src/utils/user-agent.ts +28 -0
- package/src/web-research/browser-search.ts +179 -0
- package/src/web-research/retry-utils.ts +173 -77
- package/src/web-research/scraper-types.ts +72 -0
- package/src/web-research/scraper-utils.ts +434 -0
- package/src/web-research/search.ts +78 -218
- package/src/web-research/types.ts +9 -24
- package/src/web-research/utils.ts +2 -143
- package/src/web-research/web-scraper.ts +477 -0
- package/src/youtube/index.ts +154 -0
- package/src/youtube/potoken.ts +259 -0
- package/src/youtube/transcript-client.ts +390 -0
- package/src/youtube/video-id.ts +118 -0
- package/config/searxng/default-settings.yml +0 -189
- package/config/searxng/limiter.toml +0 -34
- package/config/tooling/eslint.config.js +0 -73
- package/config/tooling/vitest.config.integration.ts +0 -17
- package/config/tooling/vitest.config.ts +0 -40
- package/config/tooling/vitest.config.unit.ts +0 -12
- package/docs/assets/README-banner.jpg +0 -0
- package/scripts/setup.js +0 -212
- package/src/infrastructure/searxng-lifecycle.ts +0 -644
- package/src/infrastructure/searxng-manager.ts +0 -1183
- package/src/infrastructure/state-manager.ts +0 -918
- package/src/orchestration/deep-research-reducer.ts +0 -129
- package/src/orchestration/id-utils.ts +0 -63
- package/src/security/nvd.ts +0 -532
- package/src/security/osv.ts +0 -487
- package/src/stackexchange/cache.ts +0 -111
- package/src/stackexchange/output/index.ts +0 -7
- package/src/stackexchange/output/json.ts +0 -21
- package/src/tui/searxng-status.ts +0 -80
- package/src/utils/inject-date.ts +0 -29
- package/src/utils/research-error.ts +0 -205
- package/src/utils/searxng-config.ts +0 -94
- package/src/utils/session-state.ts +0 -295
- package/src/web-research/dependencies.ts +0 -20
- package/src/web-research/scrapers.ts +0 -935
- package/tsconfig.json +0 -40
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Research Observer Implementation
|
|
3
|
+
*
|
|
4
|
+
* Implements the ResearchObserver interface to handle research lifecycle events:
|
|
5
|
+
* - Research planning
|
|
6
|
+
* - Searching and scraping
|
|
7
|
+
* - Researcher lifecycle
|
|
8
|
+
* - Evaluation
|
|
9
|
+
* - Completion and errors
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ResearchObserver } from '../core/interfaces/observer-interfaces.ts';
|
|
13
|
+
|
|
14
|
+
import { getUnitsPerResearcher, LEAD_EVAL_UNITS } from '../constants.ts';
|
|
15
|
+
import type { ResearchPanelState } from '../tui/research-panel.ts';
|
|
16
|
+
import { tuiPulse } from '../tui/pulse.ts';
|
|
17
|
+
import {
|
|
18
|
+
addSlice,
|
|
19
|
+
activateSlice,
|
|
20
|
+
completeSlice,
|
|
21
|
+
removeSlice,
|
|
22
|
+
flashSlice,
|
|
23
|
+
updateSliceTokens,
|
|
24
|
+
updateSliceStatus,
|
|
25
|
+
reactivateSlice,
|
|
26
|
+
clearCompletedResearchers,
|
|
27
|
+
} from '../tui/research-panel.ts';
|
|
28
|
+
|
|
29
|
+
export interface ObserverContext {
|
|
30
|
+
panelState: ResearchPanelState;
|
|
31
|
+
debouncedRefresh: () => void;
|
|
32
|
+
/**
|
|
33
|
+
* Immediate (non-debounced) render for animation frames. Optional so
|
|
34
|
+
* headless/test contexts can omit it; falls back to debouncedRefresh.
|
|
35
|
+
*/
|
|
36
|
+
renderImmediate?: () => void;
|
|
37
|
+
researchComplexity: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ObserverState {
|
|
41
|
+
progressCredits: Map<string, number>;
|
|
42
|
+
quickSliceLabel: string;
|
|
43
|
+
idToNumberMap: Map<string, string>;
|
|
44
|
+
unsubPulse: (() => void) | null;
|
|
45
|
+
/** Global pulse frame captured when the current search began, so each
|
|
46
|
+
* search's wave starts at the left edge (frame 0) regardless of the
|
|
47
|
+
* never-reset global pulse counter. */
|
|
48
|
+
waveBaseFrame: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a ResearchObserver implementation
|
|
53
|
+
*/
|
|
54
|
+
export function createResearchObserver(
|
|
55
|
+
ctx: ObserverContext,
|
|
56
|
+
state: ObserverState
|
|
57
|
+
): ResearchObserver {
|
|
58
|
+
const {
|
|
59
|
+
panelState,
|
|
60
|
+
debouncedRefresh,
|
|
61
|
+
} = ctx;
|
|
62
|
+
|
|
63
|
+
// Drive animation frames through the immediate render path so they are not
|
|
64
|
+
// swallowed by the state-refresh debounce. Falls back to debouncedRefresh
|
|
65
|
+
// when no immediate path is wired (headless/tests).
|
|
66
|
+
const renderFrame = ctx.renderImmediate ?? debouncedRefresh;
|
|
67
|
+
|
|
68
|
+
const {
|
|
69
|
+
progressCredits,
|
|
70
|
+
} = state;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
onStart: (query, complexity) => {
|
|
74
|
+
if (complexity === 0) {
|
|
75
|
+
// Truncate at a CODEPOINT boundary (Array.from iterates codepoints), not a
|
|
76
|
+
// code-unit index: query.slice(0, 20) can split a surrogate pair (emoji,
|
|
77
|
+
// rare CJK), emitting a lone surrogate that renders as � and corrupts the
|
|
78
|
+
// panel's width math downstream.
|
|
79
|
+
const codepoints = Array.from(query);
|
|
80
|
+
const truncatedQuery = codepoints.length > 20 ? codepoints.slice(0, 20).join('') + '...' : query;
|
|
81
|
+
state.quickSliceLabel = `researching: ${truncatedQuery}`;
|
|
82
|
+
addSlice(panelState, state.quickSliceLabel, state.quickSliceLabel, false);
|
|
83
|
+
activateSlice(panelState, state.quickSliceLabel);
|
|
84
|
+
updateSliceStatus(panelState, state.quickSliceLabel, 'starting...', debouncedRefresh);
|
|
85
|
+
|
|
86
|
+
const units = getUnitsPerResearcher();
|
|
87
|
+
panelState.progress = { expected: units, made: 0 };
|
|
88
|
+
}
|
|
89
|
+
// Quick research: researcher is running → steering is acceptable
|
|
90
|
+
panelState.steeringAcceptable = true;
|
|
91
|
+
debouncedRefresh();
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
onPlanningStart: (attempt) => {
|
|
95
|
+
if (attempt === 1) {
|
|
96
|
+
addSlice(panelState, 'coord', `coordinator`, false);
|
|
97
|
+
activateSlice(panelState, 'coord');
|
|
98
|
+
}
|
|
99
|
+
const status = attempt > 1 ? `planning (retry ${attempt - 1})` : 'planning';
|
|
100
|
+
updateSliceStatus(panelState, 'coord', status, debouncedRefresh);
|
|
101
|
+
// Keep steering acceptable: the coordinator LLM cannot be interrupted, but a
|
|
102
|
+
// message entered now is queued (not injected mid-call) and consumed at the
|
|
103
|
+
// next round boundary. Forcing it false here diverted it to a separate pi
|
|
104
|
+
// follow-up turn that never reached this run's evaluator.
|
|
105
|
+
panelState.steeringAcceptable = true;
|
|
106
|
+
debouncedRefresh();
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
onPlanningProgress: (status) => {
|
|
110
|
+
updateSliceStatus(panelState, 'coord', status, debouncedRefresh);
|
|
111
|
+
debouncedRefresh();
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
onPlanningTokens: (tokens, cost) => {
|
|
115
|
+
// Per-slice display only. Run totals are owned by the metrics registry
|
|
116
|
+
// (see research-tool-definition.ts → extractRunStats), not panelState.
|
|
117
|
+
updateSliceTokens(panelState, 'coord', tokens, cost);
|
|
118
|
+
debouncedRefresh();
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
onPlanningSuccess: (plan) => {
|
|
122
|
+
updateSliceStatus(panelState, 'coord', 'ready', debouncedRefresh);
|
|
123
|
+
completeSlice(panelState, 'coord');
|
|
124
|
+
const unitsPerResearcher = getUnitsPerResearcher();
|
|
125
|
+
const count = plan.researchers?.length || 0;
|
|
126
|
+
const units = (count * unitsPerResearcher) + LEAD_EVAL_UNITS;
|
|
127
|
+
panelState.progress = { expected: units, made: 0 };
|
|
128
|
+
panelState.title = plan.title?.trim() || 'Research';
|
|
129
|
+
// Coordinator done — about to start search then researchers → steering acceptable
|
|
130
|
+
panelState.steeringAcceptable = true;
|
|
131
|
+
debouncedRefresh();
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
onRoundStart: (round) => {
|
|
135
|
+
// Mark that we need to clear completed slices once the next round's evaluation or search starts.
|
|
136
|
+
// This keeps previous round findings visible during the evaluation phase.
|
|
137
|
+
if (round > 1) {
|
|
138
|
+
panelState.needsClear = true;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
onSearchStart: (_queries) => {
|
|
143
|
+
// Do NOT clear the previous round's finished boxes here. They stay visible
|
|
144
|
+
// (greyed) throughout this round's search so the panel never collapses to a
|
|
145
|
+
// single lone box mid-wave. The deferred clear runs atomically in
|
|
146
|
+
// onResearcherStart, so every old box (researchers + evaluator) disappears
|
|
147
|
+
// at once and the new round populates in their place. (needsClear is set by
|
|
148
|
+
// onRoundStart and consumed there.)
|
|
149
|
+
|
|
150
|
+
// Bind the "searching..." status to a real, phase-appropriate box only:
|
|
151
|
+
// - quick mode: the single quick-researcher slice
|
|
152
|
+
// - round 1: the coordinator slice (still present before researchers)
|
|
153
|
+
// - rounds 2+: NONE. The coordinator is gone and the evaluator must NOT
|
|
154
|
+
// be resurrected as a lone search box (that is the regression this
|
|
155
|
+
// guards against). The animated header wave is the search indicator.
|
|
156
|
+
let sliceId: string | null = null;
|
|
157
|
+
if (state.quickSliceLabel) {
|
|
158
|
+
sliceId = state.quickSliceLabel;
|
|
159
|
+
} else if (panelState.slices.has('coord')) {
|
|
160
|
+
sliceId = 'coord';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (sliceId && panelState.slices.has(sliceId)) {
|
|
164
|
+
reactivateSlice(panelState, sliceId);
|
|
165
|
+
activateSlice(panelState, sliceId); // Ensure not queued
|
|
166
|
+
updateSliceStatus(panelState, sliceId, 'searching...', debouncedRefresh);
|
|
167
|
+
}
|
|
168
|
+
panelState.isSearching = true;
|
|
169
|
+
|
|
170
|
+
// Search phase — steering is acceptable (will be consumed at next round boundary)
|
|
171
|
+
panelState.steeringAcceptable = true;
|
|
172
|
+
|
|
173
|
+
// Start wave animation via global pulse subscription.
|
|
174
|
+
// Capture the current global frame as the baseline so this search's wave
|
|
175
|
+
// starts at frame 0 (left edge). Position is frame % PERIOD_FRAMES, and
|
|
176
|
+
// the global counter is never reset, so without this each search would
|
|
177
|
+
// begin at an arbitrary point in the cycle.
|
|
178
|
+
state.waveBaseFrame = tuiPulse.getFrame();
|
|
179
|
+
panelState.waveFrame = 0;
|
|
180
|
+
|
|
181
|
+
if (state.unsubPulse) state.unsubPulse();
|
|
182
|
+
state.unsubPulse = tuiPulse.subscribe((globalFrame) => {
|
|
183
|
+
if (!panelState.isSearching) {
|
|
184
|
+
if (state.unsubPulse) {
|
|
185
|
+
state.unsubPulse();
|
|
186
|
+
state.unsubPulse = null;
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
panelState.waveFrame = globalFrame - state.waveBaseFrame;
|
|
191
|
+
// Animation frame: render immediately. pi-tui's render scheduler
|
|
192
|
+
// (16ms throttle + line-level diff) coalesces and caps these, so the
|
|
193
|
+
// wave updates every frame without the 100ms state-refresh debounce
|
|
194
|
+
// dropping frames.
|
|
195
|
+
renderFrame();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
debouncedRefresh();
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
onSearchProgress: (count) => {
|
|
202
|
+
// Only the coordinator (round 1) or quick slice carries a textual search
|
|
203
|
+
// count. Rounds 2+ have no search box (header wave only) — see onSearchStart.
|
|
204
|
+
let sliceId: string | null = null;
|
|
205
|
+
if (state.quickSliceLabel) sliceId = state.quickSliceLabel;
|
|
206
|
+
else if (panelState.slices.has('coord')) sliceId = 'coord';
|
|
207
|
+
|
|
208
|
+
const status = `${count} results`;
|
|
209
|
+
if (sliceId) updateSliceStatus(panelState, sliceId, status, debouncedRefresh);
|
|
210
|
+
debouncedRefresh();
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
onSearchComplete: (count) => {
|
|
214
|
+
panelState.isSearching = false;
|
|
215
|
+
|
|
216
|
+
// Stop wave animation
|
|
217
|
+
if (state.unsubPulse) {
|
|
218
|
+
state.unsubPulse();
|
|
219
|
+
state.unsubPulse = null;
|
|
220
|
+
}
|
|
221
|
+
panelState.waveFrame = undefined;
|
|
222
|
+
panelState.waveColors = undefined; // Clear persistent colors for next search
|
|
223
|
+
|
|
224
|
+
// Mirror onSearchStart: only complete a box that actually hosted the search
|
|
225
|
+
// (coordinator in round 1, quick slice in quick mode). Rounds 2+ have none.
|
|
226
|
+
let sliceId: string | null = null;
|
|
227
|
+
if (state.quickSliceLabel) sliceId = state.quickSliceLabel;
|
|
228
|
+
else if (panelState.slices.has('coord')) sliceId = 'coord';
|
|
229
|
+
|
|
230
|
+
if (sliceId) {
|
|
231
|
+
updateSliceStatus(panelState, sliceId, `${count} results`, debouncedRefresh);
|
|
232
|
+
// In quick mode the single researcher keeps working (scraping) after search
|
|
233
|
+
// completes — completing its slice here would mute it and swallow the per-URL
|
|
234
|
+
// scrape flashes/status pops the wiring exists to show. Only the coordinator's
|
|
235
|
+
// round-1 search box is genuinely done at this point; the quick slice is
|
|
236
|
+
// completed later by onResearcherComplete/onResearcherFailure.
|
|
237
|
+
if (sliceId === 'coord') completeSlice(panelState, sliceId);
|
|
238
|
+
}
|
|
239
|
+
debouncedRefresh();
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
onResearcherStart: (id, _name, _goal, _roundNumber) => {
|
|
243
|
+
if (panelState.slices.get('coord')?.completed) removeSlice(panelState, 'coord');
|
|
244
|
+
|
|
245
|
+
// Atomic round transition: this is the single point where the previous
|
|
246
|
+
// round's finished boxes (researchers AND evaluator) are cleared, right as
|
|
247
|
+
// the new round's first researcher appears — so they all vanish together and
|
|
248
|
+
// the new round populates in their place, never leaving a lone box on screen.
|
|
249
|
+
// clearCompletedResearchers already removes the completed 'eval'; the explicit
|
|
250
|
+
// removeSlice is a defensive backstop for the rare path where needsClear is
|
|
251
|
+
// unset (e.g. round 1 has no prior eval, so it is simply a no-op there).
|
|
252
|
+
if (panelState.slices.get('eval')?.completed) removeSlice(panelState, 'eval');
|
|
253
|
+
if (panelState.needsClear) {
|
|
254
|
+
clearCompletedResearchers(panelState);
|
|
255
|
+
panelState.needsClear = false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Map internal hierarchical ID to sequential display number for TUI
|
|
259
|
+
const sliceId = id === 'quick' ? state.quickSliceLabel : id;
|
|
260
|
+
|
|
261
|
+
if (!panelState.slices.has(sliceId)) {
|
|
262
|
+
let label: string;
|
|
263
|
+
if (id === 'quick') {
|
|
264
|
+
label = state.quickSliceLabel;
|
|
265
|
+
} else {
|
|
266
|
+
if (!state.idToNumberMap.has(id)) {
|
|
267
|
+
state.idToNumberMap.set(id, (state.idToNumberMap.size + 1).toString());
|
|
268
|
+
}
|
|
269
|
+
label = state.idToNumberMap.get(id)!;
|
|
270
|
+
}
|
|
271
|
+
addSlice(panelState, sliceId, label, true);
|
|
272
|
+
}
|
|
273
|
+
activateSlice(panelState, sliceId);
|
|
274
|
+
updateSliceStatus(panelState, sliceId, 'starting...', debouncedRefresh);
|
|
275
|
+
// Researcher phase — steering is acceptable (will be consumed at next round boundary)
|
|
276
|
+
panelState.steeringAcceptable = true;
|
|
277
|
+
debouncedRefresh();
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
onResearcherTokensHint: (id, inputTokens) => {
|
|
281
|
+
const sliceId = id === 'quick' ? state.quickSliceLabel : id;
|
|
282
|
+
const slice = panelState.slices.get(sliceId);
|
|
283
|
+
if (slice && inputTokens > (slice.tokens || 0)) {
|
|
284
|
+
slice.tokens = inputTokens;
|
|
285
|
+
}
|
|
286
|
+
debouncedRefresh();
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
onResearcherProgress: (id, status, tokens, cost) => {
|
|
290
|
+
const sliceId = id === 'quick' ? state.quickSliceLabel : id;
|
|
291
|
+
const unitsPerResearcher = getUnitsPerResearcher();
|
|
292
|
+
|
|
293
|
+
if (status !== undefined) {
|
|
294
|
+
if (status.startsWith('done:')) {
|
|
295
|
+
const toolName = status.slice(5);
|
|
296
|
+
// Only clear status if it matches the current tool or if it's the specific tool that finished
|
|
297
|
+
// For now, we clear it to signal progress, but preserve it if it's null
|
|
298
|
+
updateSliceStatus(panelState, sliceId, undefined, debouncedRefresh);
|
|
299
|
+
if (panelState.progress) {
|
|
300
|
+
const current = progressCredits.get(id) ?? 0;
|
|
301
|
+
// Increment for the first tool call (setup/search) OR any scrape batch
|
|
302
|
+
const shouldIncrement = (current === 0) || (toolName === 'scrape');
|
|
303
|
+
if (shouldIncrement && current + 1 <= unitsPerResearcher) {
|
|
304
|
+
panelState.progress.made += 1;
|
|
305
|
+
progressCredits.set(id, current + 1);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} else if (status) {
|
|
309
|
+
updateSliceStatus(panelState, sliceId, status, debouncedRefresh);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (tokens !== undefined && cost !== undefined) {
|
|
313
|
+
// Per-slice display only; run totals live in the metrics registry.
|
|
314
|
+
updateSliceTokens(panelState, sliceId, tokens, cost);
|
|
315
|
+
}
|
|
316
|
+
debouncedRefresh();
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
onResearcherComplete: (id, _report) => {
|
|
320
|
+
const sliceId = id === 'quick' ? state.quickSliceLabel : id;
|
|
321
|
+
if (panelState.progress) {
|
|
322
|
+
const unitsPerResearcher = getUnitsPerResearcher();
|
|
323
|
+
const current = progressCredits.get(id) ?? 0;
|
|
324
|
+
const remaining = unitsPerResearcher - current;
|
|
325
|
+
if (remaining > 0) {
|
|
326
|
+
panelState.progress.made += remaining;
|
|
327
|
+
progressCredits.set(id, unitsPerResearcher);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
completeSlice(panelState, sliceId);
|
|
331
|
+
debouncedRefresh();
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
onResearcherFailure: (id, _error) => {
|
|
335
|
+
const sliceId = id === 'quick' ? state.quickSliceLabel : id;
|
|
336
|
+
if (panelState.progress) {
|
|
337
|
+
const unitsPerResearcher = getUnitsPerResearcher();
|
|
338
|
+
const current = progressCredits.get(id) ?? 0;
|
|
339
|
+
const remaining = unitsPerResearcher - current;
|
|
340
|
+
if (remaining > 0) {
|
|
341
|
+
panelState.progress.made += remaining;
|
|
342
|
+
progressCredits.set(id, unitsPerResearcher);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Terminal failed outcome — completeSlice(failed) marks it so the render draws it
|
|
346
|
+
// distinctly (error color + "failed"), rather than the transient status field which
|
|
347
|
+
// a completed slice suppresses (making a failure look identical to a success).
|
|
348
|
+
completeSlice(panelState, sliceId, true);
|
|
349
|
+
debouncedRefresh();
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
onToolResult: (researcherId, success) => {
|
|
353
|
+
const sliceId = researcherId === 'quick' ? state.quickSliceLabel : researcherId;
|
|
354
|
+
flashSlice(panelState, sliceId, success ? 'green' : 'red', debouncedRefresh);
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
onEvaluationStart: (_round) => {
|
|
358
|
+
addSlice(panelState, 'eval', 'eval', false);
|
|
359
|
+
activateSlice(panelState, 'eval');
|
|
360
|
+
updateSliceStatus(panelState, 'eval', 'evaluating', debouncedRefresh);
|
|
361
|
+
// Keep steering acceptable during evaluation. The running evaluator call
|
|
362
|
+
// already captured its inputs, so a message entered now is queued and acted
|
|
363
|
+
// on at the NEXT round start (the whole point of mid-run steering). Forcing
|
|
364
|
+
// it false here is exactly when users steer — and diverted that steering to
|
|
365
|
+
// a separate pi follow-up turn instead of the current run's next round.
|
|
366
|
+
panelState.steeringAcceptable = true;
|
|
367
|
+
debouncedRefresh();
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
onEvaluationProgress: (status) => {
|
|
371
|
+
updateSliceStatus(panelState, 'eval', status, debouncedRefresh);
|
|
372
|
+
debouncedRefresh();
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
onEvaluationTokens: (tokens, cost) => {
|
|
376
|
+
// Per-slice display only; run totals live in the metrics registry.
|
|
377
|
+
updateSliceTokens(panelState, 'eval', tokens, cost);
|
|
378
|
+
debouncedRefresh();
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
onEvaluationDecision: (action, plan, round) => {
|
|
382
|
+
completeSlice(panelState, 'eval');
|
|
383
|
+
// Only clear completed researchers when returning final synthesis
|
|
384
|
+
// On delegation, researchers stay visible while new round researchers are added
|
|
385
|
+
if (action === 'synthesize') {
|
|
386
|
+
clearCompletedResearchers(panelState);
|
|
387
|
+
}
|
|
388
|
+
if (panelState.progress) {
|
|
389
|
+
const key = `eval.round.${round ?? panelState.slices.size}`;
|
|
390
|
+
if (!progressCredits.has(key)) {
|
|
391
|
+
panelState.progress.made += LEAD_EVAL_UNITS;
|
|
392
|
+
progressCredits.set(key, LEAD_EVAL_UNITS);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (action === 'synthesize') {
|
|
396
|
+
if (panelState.progress) panelState.progress.made = panelState.progress.expected;
|
|
397
|
+
// Final synthesis — steering no longer acceptable
|
|
398
|
+
panelState.steeringAcceptable = false;
|
|
399
|
+
} else {
|
|
400
|
+
// Delegation: prepare for new round's search → researchers → steering acceptable
|
|
401
|
+
if (plan?.researchers && plan.researchers.length > 0 && panelState.progress) {
|
|
402
|
+
const unitsPerResearcher = getUnitsPerResearcher();
|
|
403
|
+
panelState.progress.expected += (plan.researchers.length * unitsPerResearcher) + LEAD_EVAL_UNITS;
|
|
404
|
+
}
|
|
405
|
+
panelState.steeringAcceptable = true;
|
|
406
|
+
}
|
|
407
|
+
debouncedRefresh();
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
onSynthesisStart: () => {
|
|
411
|
+
// Final synthesis has begun (deep: forced mustSynthesize call; quick: the
|
|
412
|
+
// researcher session settled and the steering-consume poller stopped).
|
|
413
|
+
// There is NO next round boundary left to consume a queued message, so
|
|
414
|
+
// steering must stop being "acceptable" — otherwise a steer typed now is
|
|
415
|
+
// queued with an affirmative "will steer the next research round" toast
|
|
416
|
+
// and then destroyed at teardown. With the flag off, the input handler's
|
|
417
|
+
// fall-through forwards it to pi with an accurate toast instead.
|
|
418
|
+
panelState.steeringAcceptable = false;
|
|
419
|
+
debouncedRefresh();
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
onComplete: () => {
|
|
423
|
+
if (panelState.progress) panelState.progress.made = panelState.progress.expected;
|
|
424
|
+
panelState.steeringAcceptable = false;
|
|
425
|
+
debouncedRefresh();
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
onError: () => {
|
|
429
|
+
if (panelState.progress) panelState.progress.made = panelState.progress.expected;
|
|
430
|
+
panelState.steeringAcceptable = false;
|
|
431
|
+
debouncedRefresh();
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Create initial observer state
|
|
438
|
+
*/
|
|
439
|
+
export function createObserverState(): ObserverState {
|
|
440
|
+
return {
|
|
441
|
+
progressCredits: new Map<string, number>(),
|
|
442
|
+
quickSliceLabel: '',
|
|
443
|
+
idToNumberMap: new Map<string, string>(),
|
|
444
|
+
unsubPulse: null,
|
|
445
|
+
waveBaseFrame: 0,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Stop wave animation in the observer
|
|
451
|
+
*/
|
|
452
|
+
export function stopObserverWaveAnimation(state: ObserverState, panelState: ResearchPanelState): void {
|
|
453
|
+
if (state.unsubPulse) {
|
|
454
|
+
state.unsubPulse();
|
|
455
|
+
state.unsubPulse = null;
|
|
456
|
+
}
|
|
457
|
+
panelState.waveFrame = undefined;
|
|
458
|
+
panelState.waveColors = undefined;
|
|
459
|
+
}
|