@selesai/code 0.1.19 → 0.2.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/dist/agents/architect.md +4 -3
- package/dist/agents/builder.md +5 -4
- package/dist/agents/commentator.md +3 -2
- package/dist/agents/explorer.md +4 -4
- package/dist/agents/recapper.md +4 -3
- package/dist/config.d.ts +6 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -9
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +3 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +18 -24
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +2 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +8 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +25 -11
- package/dist/core/skills.js.map +1 -1
- package/dist/defaults/models.json +143 -39
- package/dist/defaults/settings.json +2 -29
- package/dist/extensions/package.json +5 -4
- package/dist/extensions/pi-intercom/index.ts +12 -5
- package/dist/extensions/pi-powerline-footer/index.ts +4 -1
- package/dist/extensions/pi-powerline-footer/tps.ts +286 -0
- package/dist/extensions/pi-powerline-footer/welcome.ts +47 -46
- package/dist/extensions/pi-subagents/README.md +12 -5
- package/dist/extensions/pi-subagents/agents/context-builder.md +2 -2
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -5
- package/dist/extensions/pi-subagents/package.json +8 -5
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +1 -1
- package/dist/extensions/pi-subagents/src/agents/agents.ts +3 -3
- package/dist/extensions/pi-subagents/src/extension/control-notices.ts +1 -1
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +1 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +29 -2
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +72 -13
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +85 -41
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +46 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +1 -3
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +18 -18
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +24 -24
- package/dist/extensions/pi-subagents/src/runs/shared/spawn-timing.ts +46 -0
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -4
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +1 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +6 -6
- package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -3
- package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -1
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
- package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +1 -1
- package/dist/extensions/pi-subagents/src/tui/render.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/spawn-timing.test.ts +45 -0
- package/dist/extensions/pi-subagents/tsconfig.runner.json +17 -0
- package/dist/extensions/pi-web-agent/package.json +31 -0
- package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -0
- package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -0
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -0
- package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -0
- package/dist/extensions/pi-web-agent/src/changelog-notice.ts +133 -0
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -0
- package/dist/extensions/pi-web-agent/src/extension.ts +126 -0
- package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -0
- package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -0
- package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -0
- package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -0
- package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -0
- package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -0
- package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -0
- package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -0
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -0
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -0
- package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -0
- package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -0
- package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -0
- package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -0
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +208 -0
- package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -0
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -0
- package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -0
- package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -0
- package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -0
- package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -0
- package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -0
- package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -0
- package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -0
- package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -0
- package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -0
- package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -0
- package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -0
- package/dist/extensions/pi-web-agent/src/types.ts +88 -0
- package/dist/extensions/ponytail/index.js +189 -0
- package/dist/extensions/ponytail/package.json +8 -0
- package/dist/extensions/ponytail/ponytail-config.cjs +122 -0
- package/dist/extensions/ponytail/ponytail-instructions.cjs +94 -0
- package/dist/extensions/ponytail/test/extension.test.js +167 -0
- package/dist/extensions/ponytail/test/helpers.test.js +92 -0
- package/dist/extensions/question/constants.ts +38 -0
- package/dist/extensions/question/dialog-adapter.ts +74 -0
- package/dist/extensions/question/helpers.ts +102 -0
- package/dist/extensions/question/index.ts +671 -0
- package/dist/extensions/question/navigation.ts +14 -0
- package/dist/extensions/question/package.json +18 -0
- package/dist/extensions/question/question-list.ts +498 -0
- package/dist/extensions/question/row-layout.ts +162 -0
- package/dist/extensions/question/schemas.ts +69 -0
- package/dist/extensions/question/selection-mode.ts +49 -0
- package/dist/extensions/question/shortcuts.ts +53 -0
- package/dist/extensions/question/tests/helpers.test.ts +178 -0
- package/dist/extensions/question/tests/question-list.test.ts +230 -0
- package/dist/extensions/question/tests/row-layout.test.ts +136 -0
- package/dist/extensions/question/tests/shortcuts.test.ts +85 -0
- package/dist/extensions/question/tests/ui-protocol.test.ts +143 -0
- package/dist/extensions/question/tui-adapter.ts +33 -0
- package/dist/extensions/question/types.ts +102 -0
- package/dist/extensions/question/ui-protocol.ts +46 -0
- package/dist/extensions/tokenin-onboarding.ts +265 -2
- package/dist/extensions/web-agent-onboarding.ts +222 -0
- package/dist/extensions/workflow/adapter.ts +588 -0
- package/dist/extensions/workflow/extension.ts +24 -0
- package/dist/extensions/workflow/modes/prototype.ts +156 -0
- package/dist/extensions/workflow/modes/quick.ts +138 -0
- package/dist/extensions/workflow/package.json +17 -0
- package/dist/extensions/workflow/state-machine.ts +386 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +57 -13
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/mime.d.ts.map +1 -1
- package/dist/utils/mime.js +24 -1
- package/dist/utils/mime.js.map +1 -1
- package/docs/docs.json +4 -0
- package/docs/workflows.md +206 -0
- package/package.json +5 -1
- package/README.md +0 -198
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import type { WebFetchHeadlessResponse, WebFetchResponse } from '../types.js';
|
|
2
|
+
import { rankEvidence } from './evidence-ranker.js';
|
|
3
|
+
import { planSearchQueries } from './query-planner.js';
|
|
4
|
+
import { classifySourceProfile } from './source-profile.js';
|
|
5
|
+
import { extractDirectUrls } from './direct-url.js';
|
|
6
|
+
import type {
|
|
7
|
+
ResearchEvidence,
|
|
8
|
+
ResearchGap,
|
|
9
|
+
ResearchLowValueOutcome,
|
|
10
|
+
ResearchOrchestratorDecision,
|
|
11
|
+
ResearchWorkerResult
|
|
12
|
+
} from './research-types.js';
|
|
13
|
+
import { decideNextResearchStep } from './stop-decider.js';
|
|
14
|
+
import { analyzeEvidenceQuality, type EvidenceCaveatReason } from './evidence-quality.js';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_MAX_PASSES = 3;
|
|
17
|
+
const DEFAULT_MAX_FETCHES_PER_PASS = 4;
|
|
18
|
+
const DEFAULT_MAX_HEADLESS_ATTEMPTS = 2;
|
|
19
|
+
|
|
20
|
+
function classifyEvidenceUrl(url: string): ResearchEvidence['sourceKind'] {
|
|
21
|
+
return classifySourceProfile(url).sourceKind;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function summarizeText(text: string, maxLength = 180): string {
|
|
25
|
+
return text.replace(/\s+/g, ' ').trim().slice(0, maxLength);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isBotCheckContent({ title = '', text }: { title?: string; text: string }) {
|
|
29
|
+
return /performing security verification|security service|verify you are not a bot|just a moment|checking your browser/i.test(
|
|
30
|
+
`${title}\n${text}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function evidenceFromFetch(result: WebFetchResponse): ResearchEvidence | null {
|
|
35
|
+
if (result.status !== 'ok' || !result.content?.text.trim()) return null;
|
|
36
|
+
if (isBotCheckContent({ title: result.content.title, text: result.content.text })) return null;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
title: result.content.title ?? result.url,
|
|
40
|
+
url: result.url,
|
|
41
|
+
sourceKind: classifyEvidenceUrl(result.url),
|
|
42
|
+
method: result.metadata.method,
|
|
43
|
+
summary: summarizeText(result.content.text),
|
|
44
|
+
supports: [summarizeText(result.content.text, 120)]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function evidenceFromHeadless(result: WebFetchHeadlessResponse): ResearchEvidence | null {
|
|
49
|
+
if (result.status !== 'ok' || !result.content?.text.trim()) return null;
|
|
50
|
+
if (isBotCheckContent({ title: result.content.title, text: result.content.text })) return null;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
title: result.content.title ?? result.url,
|
|
54
|
+
url: result.url,
|
|
55
|
+
sourceKind: classifyEvidenceUrl(result.url),
|
|
56
|
+
method: 'headless',
|
|
57
|
+
summary: summarizeText(result.content.text),
|
|
58
|
+
supports: [summarizeText(result.content.text, 120)]
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function combinedWorkerPass({
|
|
63
|
+
lastPass,
|
|
64
|
+
previousQueries,
|
|
65
|
+
allGaps,
|
|
66
|
+
allLowValueOutcomes,
|
|
67
|
+
exhaustedBudget
|
|
68
|
+
}: {
|
|
69
|
+
lastPass?: ResearchWorkerResult;
|
|
70
|
+
previousQueries: string[];
|
|
71
|
+
allGaps: ResearchGap[];
|
|
72
|
+
allLowValueOutcomes: ResearchLowValueOutcome[];
|
|
73
|
+
exhaustedBudget: boolean;
|
|
74
|
+
}): ResearchWorkerResult {
|
|
75
|
+
return {
|
|
76
|
+
searchQueries: lastPass?.searchQueries ?? previousQueries,
|
|
77
|
+
evidence: lastPass?.evidence ?? [],
|
|
78
|
+
gaps: allGaps,
|
|
79
|
+
lowValueOutcomes: allLowValueOutcomes,
|
|
80
|
+
suggestedHeadlessUrl: lastPass?.suggestedHeadlessUrl,
|
|
81
|
+
exhaustedBudget
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function directUnreadableMessage(url: string) {
|
|
86
|
+
return classifySourceProfile(url).kind === 'forum-thread'
|
|
87
|
+
? `Thread source could not be read reliably: ${url}`
|
|
88
|
+
: `Direct URL could not be read reliably: ${url}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function shouldRetryDirectWithHeadless(result: WebFetchResponse, evidence: ResearchEvidence | null) {
|
|
92
|
+
if (result.status === 'needs_headless') return true;
|
|
93
|
+
if (result.status !== 'ok' || evidence) return false;
|
|
94
|
+
return classifySourceProfile(result.url).shouldPreferHeadlessWhenWeak;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildMetadata({
|
|
98
|
+
previousQueries,
|
|
99
|
+
allEvidence,
|
|
100
|
+
allGaps,
|
|
101
|
+
allLowValueOutcomes,
|
|
102
|
+
headlessAttempts,
|
|
103
|
+
exhaustedBudget,
|
|
104
|
+
caveatReasons = []
|
|
105
|
+
}: {
|
|
106
|
+
previousQueries: string[];
|
|
107
|
+
allEvidence: ResearchEvidence[];
|
|
108
|
+
allGaps: ResearchGap[];
|
|
109
|
+
allLowValueOutcomes: ResearchLowValueOutcome[];
|
|
110
|
+
headlessAttempts: number;
|
|
111
|
+
exhaustedBudget: boolean;
|
|
112
|
+
caveatReasons?: EvidenceCaveatReason[];
|
|
113
|
+
}) {
|
|
114
|
+
return {
|
|
115
|
+
searchPasses: previousQueries.length,
|
|
116
|
+
fetchedPages: allEvidence.length + allGaps.length + allLowValueOutcomes.length,
|
|
117
|
+
headlessAttempts,
|
|
118
|
+
exhaustedBudget,
|
|
119
|
+
caveatReasons
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function decisionForAnswer({
|
|
124
|
+
action,
|
|
125
|
+
query,
|
|
126
|
+
ranked,
|
|
127
|
+
exhaustedBudget
|
|
128
|
+
}: {
|
|
129
|
+
action: 'answer' | 'answer-with-caveat';
|
|
130
|
+
query: string;
|
|
131
|
+
ranked: ResearchEvidence[];
|
|
132
|
+
exhaustedBudget: boolean;
|
|
133
|
+
}): ResearchOrchestratorDecision {
|
|
134
|
+
if (action === 'answer') {
|
|
135
|
+
return {
|
|
136
|
+
action: 'answer',
|
|
137
|
+
rationale: 'Adaptive research gathered enough strong evidence.',
|
|
138
|
+
approvedEvidence: ranked
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
action: 'research-again',
|
|
144
|
+
rationale: exhaustedBudget ? 'Research budget exhausted; answer with caveat.' : 'Evidence has quality caveats; answer with caveat.',
|
|
145
|
+
followupQuery: query
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function createResearchOrchestrator({
|
|
150
|
+
worker,
|
|
151
|
+
fetchDirect,
|
|
152
|
+
headlessFetch
|
|
153
|
+
}: {
|
|
154
|
+
worker: {
|
|
155
|
+
run: (input: {
|
|
156
|
+
query: string;
|
|
157
|
+
maxSearchRounds: number;
|
|
158
|
+
maxFetches: number;
|
|
159
|
+
}) => Promise<ResearchWorkerResult>;
|
|
160
|
+
};
|
|
161
|
+
fetchDirect?: (input: { url: string }) => Promise<WebFetchResponse>;
|
|
162
|
+
headlessFetch: (input: { url: string }) => Promise<WebFetchHeadlessResponse>;
|
|
163
|
+
}) {
|
|
164
|
+
return {
|
|
165
|
+
async run({ query }: { query: string }) {
|
|
166
|
+
const allEvidence: ResearchEvidence[] = [];
|
|
167
|
+
const allGaps: ResearchGap[] = [];
|
|
168
|
+
const allLowValueOutcomes: ResearchLowValueOutcome[] = [];
|
|
169
|
+
const previousQueries: string[] = [];
|
|
170
|
+
const suggestedHeadlessUrls: string[] = [];
|
|
171
|
+
let headlessAttempts = 0;
|
|
172
|
+
let lastPass: ResearchWorkerResult | undefined;
|
|
173
|
+
|
|
174
|
+
if (fetchDirect) {
|
|
175
|
+
for (const url of extractDirectUrls(query).slice(0, 3)) {
|
|
176
|
+
const directResult = await fetchDirect({ url });
|
|
177
|
+
const directEvidence = evidenceFromFetch(directResult);
|
|
178
|
+
if (directEvidence) {
|
|
179
|
+
allEvidence.push(directEvidence);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (shouldRetryDirectWithHeadless(directResult, directEvidence)) {
|
|
184
|
+
if (headlessAttempts < DEFAULT_MAX_HEADLESS_ATTEMPTS) {
|
|
185
|
+
headlessAttempts++;
|
|
186
|
+
const headlessResult = await headlessFetch({ url: directResult.url });
|
|
187
|
+
const headlessEvidence = evidenceFromHeadless(headlessResult);
|
|
188
|
+
if (headlessEvidence) {
|
|
189
|
+
allEvidence.push(headlessEvidence);
|
|
190
|
+
} else {
|
|
191
|
+
allGaps.push({ kind: 'fetch-failed', message: directUnreadableMessage(directResult.url) });
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
allGaps.push({ kind: 'fetch-failed', message: directUnreadableMessage(directResult.url) });
|
|
195
|
+
}
|
|
196
|
+
} else if (directResult.status !== 'ok') {
|
|
197
|
+
allGaps.push({
|
|
198
|
+
kind: 'fetch-failed',
|
|
199
|
+
message: directResult.error?.message ?? `Direct URL fetch failed for ${directResult.url}`
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
allGaps.push({ kind: 'fetch-failed', message: directUnreadableMessage(directResult.url) });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (let passIndex = 0; passIndex < DEFAULT_MAX_PASSES; passIndex++) {
|
|
208
|
+
const queries = planSearchQueries({
|
|
209
|
+
originalQuery: query,
|
|
210
|
+
passIndex,
|
|
211
|
+
previousQueries,
|
|
212
|
+
gaps: allGaps.map((gap) => gap.message)
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
for (const plannedQuery of queries) {
|
|
216
|
+
previousQueries.push(plannedQuery);
|
|
217
|
+
const pass = await worker.run({
|
|
218
|
+
query: plannedQuery,
|
|
219
|
+
maxSearchRounds: 1,
|
|
220
|
+
maxFetches: DEFAULT_MAX_FETCHES_PER_PASS
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
lastPass = pass;
|
|
224
|
+
allEvidence.push(...pass.evidence);
|
|
225
|
+
allGaps.push(...pass.gaps);
|
|
226
|
+
allLowValueOutcomes.push(...pass.lowValueOutcomes);
|
|
227
|
+
if (pass.suggestedHeadlessUrl) suggestedHeadlessUrls.push(pass.suggestedHeadlessUrl);
|
|
228
|
+
|
|
229
|
+
const ranked = rankEvidence(allEvidence.filter((item) => item.sourceKind !== 'package-page'));
|
|
230
|
+
const quality = analyzeEvidenceQuality({
|
|
231
|
+
evidence: ranked,
|
|
232
|
+
gaps: allGaps,
|
|
233
|
+
lowValueOutcomes: allLowValueOutcomes
|
|
234
|
+
});
|
|
235
|
+
const decision = decideNextResearchStep({
|
|
236
|
+
evidence: ranked,
|
|
237
|
+
suggestedHeadlessUrls,
|
|
238
|
+
passIndex,
|
|
239
|
+
maxPasses: DEFAULT_MAX_PASSES,
|
|
240
|
+
headlessAttempts,
|
|
241
|
+
maxHeadlessAttempts: DEFAULT_MAX_HEADLESS_ATTEMPTS,
|
|
242
|
+
quality
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (decision.action === 'headless') {
|
|
246
|
+
headlessAttempts++;
|
|
247
|
+
const headlessResult = await headlessFetch({ url: decision.url });
|
|
248
|
+
const headlessEvidence = evidenceFromHeadless(headlessResult);
|
|
249
|
+
if (headlessEvidence) {
|
|
250
|
+
allEvidence.push(headlessEvidence);
|
|
251
|
+
const updatedRanked = rankEvidence(allEvidence.filter((item) => item.sourceKind !== 'package-page'));
|
|
252
|
+
const updatedQuality = analyzeEvidenceQuality({
|
|
253
|
+
evidence: updatedRanked,
|
|
254
|
+
gaps: allGaps,
|
|
255
|
+
lowValueOutcomes: allLowValueOutcomes
|
|
256
|
+
});
|
|
257
|
+
const updatedDecision = decideNextResearchStep({
|
|
258
|
+
evidence: updatedRanked,
|
|
259
|
+
suggestedHeadlessUrls: [],
|
|
260
|
+
passIndex,
|
|
261
|
+
maxPasses: DEFAULT_MAX_PASSES,
|
|
262
|
+
headlessAttempts,
|
|
263
|
+
maxHeadlessAttempts: DEFAULT_MAX_HEADLESS_ATTEMPTS,
|
|
264
|
+
quality: updatedQuality
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const exhaustedBudget = updatedDecision.action !== 'answer' && passIndex + 1 >= DEFAULT_MAX_PASSES;
|
|
268
|
+
return {
|
|
269
|
+
decision: decisionForAnswer({
|
|
270
|
+
action: updatedDecision.action === 'answer' ? 'answer' : 'answer-with-caveat',
|
|
271
|
+
query,
|
|
272
|
+
ranked: updatedRanked,
|
|
273
|
+
exhaustedBudget
|
|
274
|
+
}),
|
|
275
|
+
evidence: updatedRanked,
|
|
276
|
+
workerPass: combinedWorkerPass({
|
|
277
|
+
lastPass,
|
|
278
|
+
previousQueries,
|
|
279
|
+
allGaps,
|
|
280
|
+
allLowValueOutcomes,
|
|
281
|
+
exhaustedBudget
|
|
282
|
+
}),
|
|
283
|
+
metadata: buildMetadata({
|
|
284
|
+
previousQueries,
|
|
285
|
+
allEvidence,
|
|
286
|
+
allGaps,
|
|
287
|
+
allLowValueOutcomes,
|
|
288
|
+
headlessAttempts,
|
|
289
|
+
exhaustedBudget,
|
|
290
|
+
caveatReasons: updatedQuality.caveatReasons
|
|
291
|
+
})
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
decision: {
|
|
297
|
+
action: 'escalate-headless',
|
|
298
|
+
rationale: 'One high-value page is worth a single orchestrator-approved headless retry.',
|
|
299
|
+
url: decision.url,
|
|
300
|
+
approvedEvidence: ranked
|
|
301
|
+
} satisfies ResearchOrchestratorDecision,
|
|
302
|
+
evidence: ranked,
|
|
303
|
+
workerPass: combinedWorkerPass({
|
|
304
|
+
lastPass,
|
|
305
|
+
previousQueries,
|
|
306
|
+
allGaps,
|
|
307
|
+
allLowValueOutcomes,
|
|
308
|
+
exhaustedBudget: false
|
|
309
|
+
}),
|
|
310
|
+
metadata: buildMetadata({
|
|
311
|
+
previousQueries,
|
|
312
|
+
allEvidence,
|
|
313
|
+
allGaps,
|
|
314
|
+
allLowValueOutcomes,
|
|
315
|
+
headlessAttempts,
|
|
316
|
+
exhaustedBudget: false,
|
|
317
|
+
caveatReasons: quality.caveatReasons
|
|
318
|
+
})
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (decision.action === 'answer' || decision.action === 'answer-with-caveat') {
|
|
323
|
+
const exhaustedBudget = decision.action === 'answer-with-caveat' && passIndex + 1 >= DEFAULT_MAX_PASSES;
|
|
324
|
+
return {
|
|
325
|
+
decision: decisionForAnswer({ action: decision.action, query, ranked, exhaustedBudget }),
|
|
326
|
+
evidence: ranked,
|
|
327
|
+
workerPass: combinedWorkerPass({
|
|
328
|
+
lastPass,
|
|
329
|
+
previousQueries,
|
|
330
|
+
allGaps,
|
|
331
|
+
allLowValueOutcomes,
|
|
332
|
+
exhaustedBudget
|
|
333
|
+
}),
|
|
334
|
+
metadata: buildMetadata({
|
|
335
|
+
previousQueries,
|
|
336
|
+
allEvidence,
|
|
337
|
+
allGaps,
|
|
338
|
+
allLowValueOutcomes,
|
|
339
|
+
headlessAttempts,
|
|
340
|
+
exhaustedBudget,
|
|
341
|
+
caveatReasons: quality.caveatReasons
|
|
342
|
+
})
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const ranked = rankEvidence(allEvidence.filter((item) => item.sourceKind !== 'package-page'));
|
|
349
|
+
const quality = analyzeEvidenceQuality({
|
|
350
|
+
evidence: ranked,
|
|
351
|
+
gaps: allGaps,
|
|
352
|
+
lowValueOutcomes: allLowValueOutcomes
|
|
353
|
+
});
|
|
354
|
+
return {
|
|
355
|
+
decision: decisionForAnswer({ action: 'answer-with-caveat', query, ranked, exhaustedBudget: true }),
|
|
356
|
+
evidence: ranked,
|
|
357
|
+
workerPass: combinedWorkerPass({
|
|
358
|
+
lastPass,
|
|
359
|
+
previousQueries,
|
|
360
|
+
allGaps,
|
|
361
|
+
allLowValueOutcomes,
|
|
362
|
+
exhaustedBudget: true
|
|
363
|
+
}),
|
|
364
|
+
metadata: buildMetadata({
|
|
365
|
+
previousQueries,
|
|
366
|
+
allEvidence,
|
|
367
|
+
allGaps,
|
|
368
|
+
allLowValueOutcomes,
|
|
369
|
+
headlessAttempts,
|
|
370
|
+
exhaustedBudget: true,
|
|
371
|
+
caveatReasons: quality.caveatReasons
|
|
372
|
+
})
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type ResearchSourceKind =
|
|
2
|
+
| 'official-docs'
|
|
3
|
+
| 'official-api'
|
|
4
|
+
| 'official-discussion'
|
|
5
|
+
| 'community'
|
|
6
|
+
| 'issue-thread'
|
|
7
|
+
| 'package-page'
|
|
8
|
+
| 'other';
|
|
9
|
+
|
|
10
|
+
export type ResearchMethod = 'search' | 'http' | 'headless' | 'firecrawl';
|
|
11
|
+
|
|
12
|
+
export type ResearchEvidence = {
|
|
13
|
+
title: string;
|
|
14
|
+
url: string;
|
|
15
|
+
sourceKind: ResearchSourceKind;
|
|
16
|
+
method: Exclude<ResearchMethod, 'search'>;
|
|
17
|
+
summary: string;
|
|
18
|
+
supports: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ResearchGap = {
|
|
22
|
+
kind: 'needs-more-evidence' | 'conflict' | 'fetch-failed';
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ResearchLowValueOutcome = {
|
|
27
|
+
kind: 'empty-search' | 'bot-check' | 'low-value-page' | 'duplicate-evidence';
|
|
28
|
+
url?: string;
|
|
29
|
+
message: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ResearchWorkerResult = {
|
|
33
|
+
searchQueries: string[];
|
|
34
|
+
evidence: ResearchEvidence[];
|
|
35
|
+
gaps: ResearchGap[];
|
|
36
|
+
lowValueOutcomes: ResearchLowValueOutcome[];
|
|
37
|
+
suggestedHeadlessUrl?: string;
|
|
38
|
+
exhaustedBudget: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ResearchRunMetadata = {
|
|
42
|
+
searchPasses: number;
|
|
43
|
+
fetchedPages: number;
|
|
44
|
+
headlessAttempts: number;
|
|
45
|
+
exhaustedBudget: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type ResearchOrchestratorDecision =
|
|
49
|
+
| {
|
|
50
|
+
action: 'answer';
|
|
51
|
+
rationale: string;
|
|
52
|
+
approvedEvidence: ResearchEvidence[];
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
action: 'research-again';
|
|
56
|
+
rationale: string;
|
|
57
|
+
followupQuery: string;
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
action: 'escalate-headless';
|
|
61
|
+
rationale: string;
|
|
62
|
+
url: string;
|
|
63
|
+
approvedEvidence: ResearchEvidence[];
|
|
64
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { WebFetchResponse, WebSearchResponse } from '../types.js';
|
|
2
|
+
import { selectCandidates } from './candidate-selector.js';
|
|
3
|
+
import { classifySourceProfile } from './source-profile.js';
|
|
4
|
+
import type {
|
|
5
|
+
ResearchEvidence,
|
|
6
|
+
ResearchGap,
|
|
7
|
+
ResearchLowValueOutcome,
|
|
8
|
+
ResearchSourceKind,
|
|
9
|
+
ResearchWorkerResult
|
|
10
|
+
} from './research-types.js';
|
|
11
|
+
|
|
12
|
+
function classifySource(url: string): ResearchSourceKind {
|
|
13
|
+
return classifySourceProfile(url).sourceKind;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function summarizeText(text: string, maxLength = 180): string {
|
|
17
|
+
return text.replace(/\s+/g, ' ').trim().slice(0, maxLength);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isBotCheckContent({ title = '', text }: { title?: string; text: string }) {
|
|
21
|
+
return /performing security verification|security service|verify you are not a bot|just a moment|checking your browser/i.test(
|
|
22
|
+
`${title}\n${text}`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function evidenceFromFetch(fetched: WebFetchResponse, fallbackTitle: string) {
|
|
27
|
+
const content = fetched.content;
|
|
28
|
+
if (fetched.status !== 'ok' || !content) return null;
|
|
29
|
+
if (isBotCheckContent({ title: content.title, text: content.text })) return null;
|
|
30
|
+
|
|
31
|
+
const sourceKind = classifySource(fetched.url);
|
|
32
|
+
if (sourceKind === 'package-page') {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
title: content.title ?? fallbackTitle,
|
|
38
|
+
url: fetched.url,
|
|
39
|
+
sourceKind,
|
|
40
|
+
method: fetched.metadata.method,
|
|
41
|
+
summary: summarizeText(content.text),
|
|
42
|
+
supports: [summarizeText(content.text, 120)]
|
|
43
|
+
} satisfies ResearchEvidence;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function lowValueOutcomeFromFetch(fetched: WebFetchResponse): ResearchLowValueOutcome | null {
|
|
47
|
+
if (fetched.status !== 'ok' || !fetched.content) return null;
|
|
48
|
+
|
|
49
|
+
if (isBotCheckContent({ title: fetched.content.title, text: fetched.content.text })) {
|
|
50
|
+
return {
|
|
51
|
+
kind: 'bot-check',
|
|
52
|
+
url: fetched.url,
|
|
53
|
+
message: 'Fetched page showed a bot-check or security verification page.'
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (classifySource(fetched.url) !== 'package-page') return null;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
kind: 'low-value-page',
|
|
61
|
+
url: fetched.url,
|
|
62
|
+
message: 'Fetched page did not add strong research evidence.'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createResearchWorker({
|
|
67
|
+
search,
|
|
68
|
+
fetchPage
|
|
69
|
+
}: {
|
|
70
|
+
search: (input: { query: string }) => Promise<WebSearchResponse>;
|
|
71
|
+
fetchPage: (input: { url: string }) => Promise<WebFetchResponse>;
|
|
72
|
+
}) {
|
|
73
|
+
return {
|
|
74
|
+
async run({
|
|
75
|
+
query,
|
|
76
|
+
maxSearchRounds,
|
|
77
|
+
maxFetches
|
|
78
|
+
}: {
|
|
79
|
+
query: string;
|
|
80
|
+
maxSearchRounds: number;
|
|
81
|
+
maxFetches: number;
|
|
82
|
+
}): Promise<ResearchWorkerResult> {
|
|
83
|
+
const searchQueries = [query];
|
|
84
|
+
const evidence: ResearchEvidence[] = [];
|
|
85
|
+
const gaps: ResearchGap[] = [];
|
|
86
|
+
const lowValueOutcomes: ResearchLowValueOutcome[] = [];
|
|
87
|
+
let suggestedHeadlessUrl: string | undefined;
|
|
88
|
+
|
|
89
|
+
if (maxSearchRounds <= 0 || maxFetches <= 0) {
|
|
90
|
+
return {
|
|
91
|
+
searchQueries: [],
|
|
92
|
+
evidence,
|
|
93
|
+
gaps: [{ kind: 'needs-more-evidence', message: 'Research worker budget was zero.' }],
|
|
94
|
+
lowValueOutcomes,
|
|
95
|
+
suggestedHeadlessUrl,
|
|
96
|
+
exhaustedBudget: true
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const searchResult = await search({ query });
|
|
101
|
+
if (searchResult.status !== 'ok') {
|
|
102
|
+
return {
|
|
103
|
+
searchQueries,
|
|
104
|
+
evidence,
|
|
105
|
+
gaps: [
|
|
106
|
+
{
|
|
107
|
+
kind: 'fetch-failed',
|
|
108
|
+
message: searchResult.error?.message ?? 'Search failed during research worker pass.'
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
lowValueOutcomes,
|
|
112
|
+
suggestedHeadlessUrl,
|
|
113
|
+
exhaustedBudget: false
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (searchResult.results.length === 0) {
|
|
118
|
+
return {
|
|
119
|
+
searchQueries,
|
|
120
|
+
evidence,
|
|
121
|
+
gaps,
|
|
122
|
+
lowValueOutcomes: [
|
|
123
|
+
{
|
|
124
|
+
kind: 'empty-search',
|
|
125
|
+
message: 'Search returned no results for this pass.'
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
suggestedHeadlessUrl,
|
|
129
|
+
exhaustedBudget: false
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const candidates = selectCandidates({
|
|
134
|
+
query,
|
|
135
|
+
results: searchResult.results,
|
|
136
|
+
seenUrls: new Set(evidence.map((item) => item.url)),
|
|
137
|
+
maxCandidates: maxFetches
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
for (const candidate of candidates) {
|
|
141
|
+
const fetched = await fetchPage({ url: candidate.url });
|
|
142
|
+
|
|
143
|
+
if (fetched.status === 'ok') {
|
|
144
|
+
const parsedEvidence = evidenceFromFetch(fetched, candidate.title);
|
|
145
|
+
if (parsedEvidence) {
|
|
146
|
+
evidence.push(parsedEvidence);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const lowValueOutcome = lowValueOutcomeFromFetch(fetched);
|
|
151
|
+
if (lowValueOutcome) {
|
|
152
|
+
lowValueOutcomes.push(lowValueOutcome);
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (fetched.status === 'needs_headless') {
|
|
158
|
+
if (!suggestedHeadlessUrl) {
|
|
159
|
+
suggestedHeadlessUrl = fetched.url;
|
|
160
|
+
}
|
|
161
|
+
gaps.push({ kind: 'fetch-failed', message: `HTTP fetch was weak for ${fetched.url}` });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
gaps.push({
|
|
166
|
+
kind: 'fetch-failed',
|
|
167
|
+
message: fetched.error?.message ?? `Fetch failed for ${candidate.url}`
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
searchQueries,
|
|
173
|
+
evidence,
|
|
174
|
+
gaps,
|
|
175
|
+
lowValueOutcomes,
|
|
176
|
+
suggestedHeadlessUrl,
|
|
177
|
+
exhaustedBudget: false
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|