@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,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Collection System — two-tier design
|
|
3
|
+
*
|
|
4
|
+
* Session registry: accumulates infrastructure and coordination events for the
|
|
5
|
+
* lifetime of one Pi session. Never cleared between research runs; only an
|
|
6
|
+
* explicit clearSession() / "Reset metrics" action resets it.
|
|
7
|
+
*
|
|
8
|
+
* Run registry: one per research invocation. Created fresh at tool entry,
|
|
9
|
+
* discarded after the run. A RunSummary snapshot is kept in the session-level
|
|
10
|
+
* run history (capped at MAX_RUN_HISTORY) for display.
|
|
11
|
+
*
|
|
12
|
+
* All emit callsites use the same `metrics` export — the active registry is
|
|
13
|
+
* resolved from AsyncLocalStorage. Inside runWithRunRegistry() the run
|
|
14
|
+
* registry receives the writes; outside (startup, infrastructure init) the
|
|
15
|
+
* session registry does.
|
|
16
|
+
*
|
|
17
|
+
* Metric types:
|
|
18
|
+
* - Counter: monotonically increasing integer (request counts, error counts)
|
|
19
|
+
* - Gauge: point-in-time scalar (active workers, lock held flag)
|
|
20
|
+
* - Histogram: distribution of numeric observations (latency in ms)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
24
|
+
import { logger } from '../logger.ts';
|
|
25
|
+
|
|
26
|
+
export type Labels = Record<string, string>;
|
|
27
|
+
|
|
28
|
+
export interface IMetricHistogram {
|
|
29
|
+
count: number;
|
|
30
|
+
min: number;
|
|
31
|
+
max: number;
|
|
32
|
+
avg: number;
|
|
33
|
+
p50: number;
|
|
34
|
+
p90: number;
|
|
35
|
+
p95: number;
|
|
36
|
+
p99: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IMetricsSnapshot {
|
|
40
|
+
counters: Record<string, number>;
|
|
41
|
+
gauges: Record<string, number>;
|
|
42
|
+
histograms: Record<string, IMetricHistogram>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Immutable snapshot captured at the end of a single research run. */
|
|
46
|
+
export interface RunSummary {
|
|
47
|
+
readonly runId: string;
|
|
48
|
+
readonly startedAt: number;
|
|
49
|
+
readonly completedAt: number;
|
|
50
|
+
readonly durationMs: number;
|
|
51
|
+
readonly status: 'success' | 'error' | 'cancelled';
|
|
52
|
+
readonly snapshot: IMetricsSnapshot;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Core registry ──────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Low-level metrics store. Holds counters, gauges and histograms in memory.
|
|
59
|
+
* Not directly exported as the singleton — use the `metrics` export instead.
|
|
60
|
+
*/
|
|
61
|
+
export class MetricsRegistry {
|
|
62
|
+
private counters = new Map<string, number>();
|
|
63
|
+
private gauges = new Map<string, number>();
|
|
64
|
+
private histograms = new Map<string, {
|
|
65
|
+
values: number[];
|
|
66
|
+
pointer: number;
|
|
67
|
+
limit: number;
|
|
68
|
+
dirty: boolean;
|
|
69
|
+
cached: IMetricHistogram | null;
|
|
70
|
+
}>();
|
|
71
|
+
|
|
72
|
+
private serializeLabels(labels?: Labels): string {
|
|
73
|
+
if (!labels) return '';
|
|
74
|
+
const keys = Object.keys(labels).sort();
|
|
75
|
+
return keys.map(k => `${k}="${labels[k]}"`).join(',');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private getKey(name: string, labels?: Labels): string {
|
|
79
|
+
const labelStr = this.serializeLabels(labels);
|
|
80
|
+
return labelStr ? `${name}{${labelStr}}` : name;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public increment(name: string, value: number = 1, labels?: Labels): void {
|
|
84
|
+
const key = this.getKey(name, labels);
|
|
85
|
+
this.counters.set(key, (this.counters.get(key) ?? 0) + value);
|
|
86
|
+
if (name === 'llm_cost_total' || name === 'llm_tokens_total') {
|
|
87
|
+
logger.debug(`[Metrics] Incremented ${name} by ${value}${labels ? ` (labels: ${JSON.stringify(labels)})` : ''}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public setGauge(name: string, value: number, labels?: Labels): void {
|
|
92
|
+
this.gauges.set(this.getKey(name, labels), value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public observe(name: string, value: number, labels?: Labels): void {
|
|
96
|
+
// FIX (#15): Guard against NaN/Infinity which would corrupt histogram calculations
|
|
97
|
+
if (!Number.isFinite(value)) return;
|
|
98
|
+
const key = this.getKey(name, labels);
|
|
99
|
+
let entry = this.histograms.get(key);
|
|
100
|
+
if (!entry) {
|
|
101
|
+
entry = { values: [], pointer: 0, limit: 10_000, dirty: true, cached: null };
|
|
102
|
+
this.histograms.set(key, entry);
|
|
103
|
+
}
|
|
104
|
+
entry.dirty = true;
|
|
105
|
+
if (entry.values.length < entry.limit) {
|
|
106
|
+
entry.values.push(value);
|
|
107
|
+
} else {
|
|
108
|
+
entry.values[entry.pointer] = value;
|
|
109
|
+
entry.pointer = (entry.pointer + 1) % entry.limit;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public async measure<T>(name: string, action: () => Promise<T>, labels?: Labels): Promise<T> {
|
|
114
|
+
const start = process.hrtime.bigint();
|
|
115
|
+
try {
|
|
116
|
+
const result = await action();
|
|
117
|
+
this.observe(name, Number(process.hrtime.bigint() - start) / 1_000_000, labels);
|
|
118
|
+
return result;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
this.observe(name, Number(process.hrtime.bigint() - start) / 1_000_000, { ...labels, error: 'true' });
|
|
121
|
+
this.increment(`${name}_errors_total`, 1, labels);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private percentile(sorted: number[], pct: number): number {
|
|
127
|
+
const len = sorted.length;
|
|
128
|
+
if (len === 0) return 0;
|
|
129
|
+
if (len === 1) return sorted[0] ?? 0;
|
|
130
|
+
if (pct <= 0) return sorted[0] ?? 0;
|
|
131
|
+
if (pct >= 100) return sorted[len - 1] ?? 0;
|
|
132
|
+
const rank = (pct / 100) * (len - 1);
|
|
133
|
+
const lo = Math.floor(rank);
|
|
134
|
+
const hi = Math.ceil(rank);
|
|
135
|
+
const w = rank - lo;
|
|
136
|
+
return (sorted[lo] ?? 0) + ((sorted[hi] ?? 0) - (sorted[lo] ?? 0)) * w;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public getSnapshot(): IMetricsSnapshot {
|
|
140
|
+
const snap: IMetricsSnapshot = { counters: {}, gauges: {}, histograms: {} };
|
|
141
|
+
|
|
142
|
+
for (const [k, v] of this.counters) snap.counters[k] = v;
|
|
143
|
+
for (const [k, v] of this.gauges) snap.gauges[k] = v;
|
|
144
|
+
|
|
145
|
+
for (const [k, entry] of this.histograms) {
|
|
146
|
+
if (entry.values.length === 0) continue;
|
|
147
|
+
|
|
148
|
+
if (!entry.dirty && entry.cached) {
|
|
149
|
+
snap.histograms[k] = entry.cached;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const vals = entry.values;
|
|
154
|
+
const sorted = vals.slice().sort((a, b) => a - b);
|
|
155
|
+
const sum = sorted.reduce((a, b) => a + b, 0);
|
|
156
|
+
|
|
157
|
+
const histogram = {
|
|
158
|
+
count: sorted.length,
|
|
159
|
+
min: sorted[0] ?? 0,
|
|
160
|
+
max: sorted[sorted.length - 1] ?? 0,
|
|
161
|
+
avg: sum / sorted.length,
|
|
162
|
+
p50: this.percentile(sorted, 50),
|
|
163
|
+
p90: this.percentile(sorted, 90),
|
|
164
|
+
p95: this.percentile(sorted, 95),
|
|
165
|
+
p99: this.percentile(sorted, 99),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
entry.cached = histogram;
|
|
169
|
+
entry.dirty = false;
|
|
170
|
+
snap.histograms[k] = histogram;
|
|
171
|
+
}
|
|
172
|
+
return snap;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public clear(): void {
|
|
176
|
+
this.counters.clear();
|
|
177
|
+
this.gauges.clear();
|
|
178
|
+
this.histograms.clear();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Run-registry routing ───────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
/** AsyncLocalStorage slot that holds the active run-scoped MetricsRegistry. */
|
|
185
|
+
const runRegistryStorage = new AsyncLocalStorage<MetricsRegistry>();
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Execute `fn` with `registry` as the active run-scoped metrics target.
|
|
189
|
+
* Every `metrics.*` call inside `fn` — including all async continuations —
|
|
190
|
+
* will emit into `registry` rather than the session registry.
|
|
191
|
+
* The run context does NOT nest; a second runWithRunRegistry call inside an
|
|
192
|
+
* existing one replaces the active registry for that sub-scope.
|
|
193
|
+
*/
|
|
194
|
+
export function runWithRunRegistry<T>(
|
|
195
|
+
registry: MetricsRegistry,
|
|
196
|
+
fn: () => Promise<T>,
|
|
197
|
+
): Promise<T> {
|
|
198
|
+
return runRegistryStorage.run(registry, fn);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Returns the run-scoped registry if called inside a runWithRunRegistry context,
|
|
203
|
+
* undefined otherwise.
|
|
204
|
+
*/
|
|
205
|
+
export function getCurrentRunRegistry(): MetricsRegistry | undefined {
|
|
206
|
+
return runRegistryStorage.getStore();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Session-level manager ──────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
const MAX_RUN_HISTORY = 10;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Two-tier session-scoped metrics manager (the exported `metrics` singleton).
|
|
215
|
+
*
|
|
216
|
+
* Emit methods route to the run registry when inside a runWithRunRegistry
|
|
217
|
+
* context, falling back to the session registry otherwise:
|
|
218
|
+
*
|
|
219
|
+
* • Infrastructure events at startup / health-check time → session registry
|
|
220
|
+
* • Everything emitted during a research invocation → run registry
|
|
221
|
+
*
|
|
222
|
+
* Run registries are created by the research tool, passed to runWithRunRegistry,
|
|
223
|
+
* and discarded after the run. A RunSummary snapshot is appended to _runHistory
|
|
224
|
+
* by calling recordRunSummary() after the runWithRunRegistry call returns
|
|
225
|
+
* (i.e. outside the run context, so the write goes to session scope).
|
|
226
|
+
*/
|
|
227
|
+
class SessionMetrics {
|
|
228
|
+
private readonly _session = new MetricsRegistry();
|
|
229
|
+
private _sessionStartedAt = Date.now();
|
|
230
|
+
private _runHistory: RunSummary[] = [];
|
|
231
|
+
|
|
232
|
+
private getActive(): MetricsRegistry {
|
|
233
|
+
return runRegistryStorage.getStore() ?? this._session;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Emit API (routes to active registry) ──────────────────────────────
|
|
237
|
+
|
|
238
|
+
public increment(name: string, value: number = 1, labels?: Labels): void {
|
|
239
|
+
this.getActive().increment(name, value, labels);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
public setGauge(name: string, value: number, labels?: Labels): void {
|
|
243
|
+
this.getActive().setGauge(name, value, labels);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public observe(name: string, value: number, labels?: Labels): void {
|
|
247
|
+
this.getActive().observe(name, value, labels);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
public async measure<T>(name: string, action: () => Promise<T>, labels?: Labels): Promise<T> {
|
|
251
|
+
return this.getActive().measure(name, action, labels);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Session-level read API ─────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
/** Snapshot of the session registry (infrastructure / cross-run metrics). */
|
|
257
|
+
public getSessionSnapshot(): IMetricsSnapshot {
|
|
258
|
+
return this._session.getSnapshot();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Millisecond timestamp when this session started or was last reset. */
|
|
262
|
+
public getSessionStartedAt(): number {
|
|
263
|
+
return this._sessionStartedAt;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Ordered run summaries, oldest first. Capped at MAX_RUN_HISTORY entries.
|
|
268
|
+
* Returns a shallow copy so callers cannot mutate the internal list.
|
|
269
|
+
*/
|
|
270
|
+
public getRunHistory(): readonly RunSummary[] {
|
|
271
|
+
return [...this._runHistory];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Run lifecycle API ──────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Append a completed run's snapshot to the history.
|
|
278
|
+
* Call this AFTER runWithRunRegistry returns so the write lands in session
|
|
279
|
+
* scope. Automatically evicts the oldest entry when the cap is reached.
|
|
280
|
+
*/
|
|
281
|
+
public recordRunSummary(summary: RunSummary): void {
|
|
282
|
+
this._runHistory.push(summary);
|
|
283
|
+
if (this._runHistory.length > MAX_RUN_HISTORY) {
|
|
284
|
+
this._runHistory.shift();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Reset API ──────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Clear the session registry, run history, and session start timestamp.
|
|
292
|
+
* In-flight run registries are NOT affected.
|
|
293
|
+
*/
|
|
294
|
+
public clearSession(): void {
|
|
295
|
+
this._session.clear();
|
|
296
|
+
this._runHistory = [];
|
|
297
|
+
this._sessionStartedAt = Date.now();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Global session-scoped metrics instance. */
|
|
302
|
+
export const metrics = new SessionMetrics();
|
|
@@ -16,11 +16,9 @@
|
|
|
16
16
|
import { promises as fs } from 'node:fs';
|
|
17
17
|
import { homedir, tmpdir, platform } from 'node:os';
|
|
18
18
|
import { join, resolve, normalize, sep } from 'node:path';
|
|
19
|
+
import { randomBytes } from 'node:crypto';
|
|
19
20
|
import { logger } from '../logger.ts';
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
const LETTERS = 'abcdefghijklmnopqrstuvwxyz';
|
|
23
|
-
const DIGITS = '0123456789';
|
|
21
|
+
import { MAX_FILENAME_QUERY_LENGTH, MAX_EXPORT_RETRIES } from '../constants.ts';
|
|
24
22
|
|
|
25
23
|
// Subdirectory names to probe, in priority order. Matched case-insensitively.
|
|
26
24
|
const PREFERRED_SUBDIRS = [
|
|
@@ -135,7 +133,7 @@ function sanitizeQuery(query: string): string {
|
|
|
135
133
|
return query
|
|
136
134
|
.toLowerCase()
|
|
137
135
|
.trim()
|
|
138
|
-
.slice(0,
|
|
136
|
+
.slice(0, MAX_FILENAME_QUERY_LENGTH)
|
|
139
137
|
.replace(/[^a-z0-9\s-]/g, '')
|
|
140
138
|
.replace(/\s+/g, '-')
|
|
141
139
|
.replace(/-+/g, '-')
|
|
@@ -147,9 +145,7 @@ function sanitizeQuery(query: string): string {
|
|
|
147
145
|
* Format: letter + number (e.g., a3, b7, x9)
|
|
148
146
|
*/
|
|
149
147
|
function generateHash(): string {
|
|
150
|
-
|
|
151
|
-
const digit = DIGITS[Math.floor(Math.random() * DIGITS.length)]!;
|
|
152
|
-
return letter + digit;
|
|
148
|
+
return randomBytes(3).toString('hex'); // 6 hex chars ≈ 16M combinations
|
|
153
149
|
}
|
|
154
150
|
|
|
155
151
|
/**
|
|
@@ -166,10 +162,25 @@ export async function exportResearchReport(
|
|
|
166
162
|
result: string,
|
|
167
163
|
_mode: 'quick' | 'deep',
|
|
168
164
|
cwd?: string,
|
|
165
|
+
explicitDir?: string,
|
|
169
166
|
): Promise<string | null> {
|
|
170
167
|
const sanitizedQuery = sanitizeQuery(query);
|
|
171
168
|
const baseFilename = `pi-research-${sanitizedQuery}`;
|
|
172
|
-
|
|
169
|
+
// An explicit directory (PI_RESEARCH_REPORT_EXPORT_DIR) is used verbatim —
|
|
170
|
+
// created if needed, no cwd-relative "smart" probing — so a
|
|
171
|
+
// host can pin a stable location instead of writing into the agent's arbitrary cwd.
|
|
172
|
+
let targetDir: string;
|
|
173
|
+
if (explicitDir && explicitDir.trim()) {
|
|
174
|
+
targetDir = resolve(explicitDir.trim());
|
|
175
|
+
try {
|
|
176
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
177
|
+
} catch (error) {
|
|
178
|
+
logger.error(`[export] Could not create export dir "${targetDir}":`, error);
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
targetDir = await resolveExportDir(cwd ?? tmpdir());
|
|
183
|
+
}
|
|
173
184
|
|
|
174
185
|
for (let attempt = 0; attempt < MAX_EXPORT_RETRIES; attempt++) {
|
|
175
186
|
const hash = generateHash();
|
|
@@ -192,8 +203,13 @@ export async function exportResearchReport(
|
|
|
192
203
|
}
|
|
193
204
|
|
|
194
205
|
/**
|
|
195
|
-
* Append the saved-path footer to a research result string.
|
|
206
|
+
* Append the saved-path footer and total cost to a research result string.
|
|
196
207
|
*/
|
|
197
|
-
export function appendExportMessage(result: string, filepath: string): string {
|
|
198
|
-
|
|
208
|
+
export function appendExportMessage(result: string, filepath: string, totalCost?: number): string {
|
|
209
|
+
let footer = `\n\n---\n\nResearch report saved to: ${filepath}`;
|
|
210
|
+
if (totalCost !== undefined && totalCost > 0) {
|
|
211
|
+
const costStr = totalCost < 0.01 ? '<$0.01' : `$${totalCost.toFixed(4)}`.replace(/0+$/, '').replace(/\.$/, '');
|
|
212
|
+
footer += `\nTotal cost: ${costStr}`;
|
|
213
|
+
}
|
|
214
|
+
return result + footer;
|
|
199
215
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe unref utility for Node.js timers.
|
|
3
|
+
*
|
|
4
|
+
* Node's setTimeout returns NodeJS.Timeout which has unref(), but some
|
|
5
|
+
* TypeScript configurations don't expose it. This helper avoids the
|
|
6
|
+
* repeated `(timer as any).unref()` pattern.
|
|
7
|
+
*/
|
|
8
|
+
export function safeUnref(timer: NodeJS.Timeout | null | undefined): void {
|
|
9
|
+
if (timer && typeof (timer as any).unref === 'function') {
|
|
10
|
+
(timer as any).unref();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Await `promise`, but give up after `deadlineMs` (resolving `undefined`).
|
|
16
|
+
*
|
|
17
|
+
* Unlike a bare `Promise.race([promise, setTimeout])`, the deadline timer is
|
|
18
|
+
* unref'd AND cleared as soon as `promise` settles, so a losing timeout never
|
|
19
|
+
* holds the event loop open after teardown work completes. Rejections of
|
|
20
|
+
* `promise` propagate unchanged (matching Promise.race semantics).
|
|
21
|
+
*/
|
|
22
|
+
export function raceWithDeadline<T>(promise: Promise<T>, deadlineMs: number): Promise<T | undefined> {
|
|
23
|
+
return new Promise<T | undefined>((resolve, reject) => {
|
|
24
|
+
const timer = setTimeout(() => resolve(undefined), deadlineMs);
|
|
25
|
+
safeUnref(timer);
|
|
26
|
+
promise.then(
|
|
27
|
+
(value) => { clearTimeout(timer); resolve(value); },
|
|
28
|
+
(err) => { clearTimeout(timer); reject(err); },
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
}
|