Skip to content

Commit 4e72cae

Browse files
committed
Fix ctrl+c and hypotheses options
1 parent e09528f commit 4e72cae

29 files changed

Lines changed: 2595 additions & 277 deletions

src/config.ts

Lines changed: 222 additions & 36 deletions
Large diffs are not rendered by default.

src/core/analysis/paperAnalyzer.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export async function analyzePaperWithLlm(args: {
7171
paper: AnalysisCorpusRow;
7272
source: ResolvedPaperSource;
7373
maxAttempts?: number;
74+
abortSignal?: AbortSignal;
7475
onProgress?: (message: string) => void;
7576
}): Promise<PaperAnalysisResult> {
7677
const maxAttempts = Math.max(1, args.maxAttempts ?? 2);
@@ -81,6 +82,7 @@ export async function analyzePaperWithLlm(args: {
8182
args.onProgress?.(`Starting LLM analysis attempt ${attempt}/${maxAttempts}.`);
8283
const completion = await args.llm.complete(buildPaperAnalysisPrompt(args.paper, args.source), {
8384
systemPrompt: ANALYSIS_SYSTEM_PROMPT,
85+
abortSignal: args.abortSignal,
8486
onProgress: (event) => {
8587
emitLlmProgress(args.onProgress, event);
8688
}
@@ -94,6 +96,9 @@ export async function analyzePaperWithLlm(args: {
9496
rawJson: parsed
9597
};
9698
} catch (error) {
99+
if (isAbortError(error)) {
100+
throw error;
101+
}
97102
lastError = error instanceof Error ? error : new Error(String(error));
98103
args.onProgress?.(`Analysis attempt ${attempt}/${maxAttempts} failed: ${lastError.message}`);
99104
}
@@ -140,6 +145,9 @@ export async function analyzePaperWithResponsesPdf(args: {
140145
rawJson: parsed
141146
};
142147
} catch (error) {
148+
if (isAbortError(error)) {
149+
throw error;
150+
}
143151
lastError = error instanceof Error ? error : new Error(String(error));
144152
args.onProgress?.(`PDF analysis attempt ${attempt}/${maxAttempts} failed: ${lastError.message}`);
145153
}
@@ -151,11 +159,15 @@ export async function analyzePaperWithResponsesPdf(args: {
151159
export function shouldFallbackResponsesPdfToLocalText(error: unknown): boolean {
152160
const message = error instanceof Error ? error.message : String(error);
153161
return [
162+
/error while downloading/i,
154163
/timeout while downloading/i,
155164
/failed to download/i,
156165
/unable to download/i,
157166
/unable to fetch/i,
158167
/could not fetch/i,
168+
/upstream status code:\s*40[34]/i,
169+
/invalid_request_error/i,
170+
/param"\s*:\s*"url"/i,
159171
/file_url/i,
160172
/remote file/i
161173
].some((pattern) => pattern.test(message));
@@ -181,6 +193,14 @@ function emitLlmProgress(
181193
}
182194
}
183195

196+
function isAbortError(error: unknown): boolean {
197+
if (!(error instanceof Error)) {
198+
return false;
199+
}
200+
const message = error.message.toLowerCase();
201+
return message.includes("aborted") || message.includes("abort");
202+
}
203+
184204
export function buildPaperAnalysisPrompt(paper: AnalysisCorpusRow, source: ResolvedPaperSource): string {
185205
return [
186206
"Analyze the following paper and extract structured evidence.",

src/core/analysis/paperSelection.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export async function selectPapersForAnalysis(args: {
103103
corpusRows: AnalysisCorpusRow[];
104104
request: AnalysisSelectionRequest;
105105
onProgress?: (message: string) => void;
106+
abortSignal?: AbortSignal;
106107
}): Promise<PaperSelectionResult> {
107108
args.onProgress?.(
108109
`Deterministic pre-rank started for ${args.corpusRows.length} paper(s) using title/topic similarity, citation count, recency, and PDF availability.`
@@ -131,7 +132,7 @@ export async function selectPapersForAnalysis(args: {
131132
};
132133
}
133134

134-
const candidatePoolSize = Math.min(totalCandidates, Math.max(args.request.topN * 5, 50));
135+
const candidatePoolSize = Math.min(totalCandidates, Math.min(Math.max(args.request.topN * 3, 30), 90));
135136
const candidatePool = ranked.slice(0, candidatePoolSize);
136137
args.onProgress?.(
137138
`Preparing LLM rerank for ${candidatePool.length} candidate(s) to choose top ${args.request.topN}.`
@@ -148,7 +149,8 @@ export async function selectPapersForAnalysis(args: {
148149
args.runTopic,
149150
args.request.topN,
150151
candidatePool,
151-
args.onProgress
152+
args.onProgress,
153+
args.abortSignal
152154
);
153155
const rerankedIds = rerank.orderedPaperIds;
154156
const rerankOrder = new Map<string, number>(rerankedIds.map((paperId, index) => [paperId, index]));
@@ -253,12 +255,14 @@ async function rerankCandidates(
253255
runTopic: string,
254256
topN: number,
255257
candidates: RankedPaperCandidate[],
256-
onProgress?: (message: string) => void
258+
onProgress?: (message: string) => void,
259+
abortSignal?: AbortSignal
257260
): Promise<{ orderedPaperIds: string[]; applied: boolean; fallbackReason?: string }> {
258261
try {
259262
onProgress?.(`Submitting rerank request for ${candidates.length} candidate(s).`);
260263
const response = await llm.complete(buildRerankPrompt(referenceTitle, runTopic, topN, candidates), {
261264
systemPrompt: RERANK_SYSTEM_PROMPT,
265+
abortSignal,
262266
onProgress: (event) => {
263267
const text = event.text.trim();
264268
if (!text) {
@@ -290,6 +294,9 @@ async function rerankCandidates(
290294
applied: true
291295
};
292296
} catch (error) {
297+
if (isAbortError(error)) {
298+
throw error;
299+
}
293300
onProgress?.(`Rerank request failed: ${error instanceof Error ? error.message : String(error)}`);
294301
return {
295302
orderedPaperIds: candidates.map((candidate) => candidate.paper.paper_id),
@@ -299,6 +306,14 @@ async function rerankCandidates(
299306
}
300307
}
301308

309+
function isAbortError(error: unknown): boolean {
310+
if (!(error instanceof Error)) {
311+
return false;
312+
}
313+
const message = error.message.toLowerCase();
314+
return message.includes("aborted") || message.includes("abort");
315+
}
316+
302317
function buildRerankPrompt(
303318
referenceTitle: string,
304319
runTopic: string,

src/core/commands/naturalActionIntent.ts

Lines changed: 181 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { GraphNodeId, RunRecord } from "../../types.js";
2-
import { buildCollectSlashCommand } from "./naturalDeterministic.js";
2+
import {
3+
buildCollectSlashCommand,
4+
extractCollectRequestFromNatural,
5+
resolveNodeAlias
6+
} from "./naturalDeterministic.js";
37
import { CollectCommandRequest } from "./collectOptions.js";
48

59
export interface NaturalActionTextClient {
@@ -8,6 +12,7 @@ export interface NaturalActionTextClient {
812
sandboxMode?: string;
913
approvalPolicy?: string;
1014
systemPrompt?: string;
15+
reasoningEffort?: string;
1116
abortSignal?: AbortSignal;
1217
}): Promise<string>;
1318
}
@@ -62,7 +67,7 @@ interface StructuredAction {
6267
title?: string;
6368
}
6469

65-
const ACTION_EXTRACTION_TIMEOUT_MS = 30000;
70+
const ACTION_EXTRACTION_TIMEOUT_MS = 12000;
6671

6772
const ACTION_HINT_PATTERNS = [
6873
/(?:collect|gather|fetch|search|lookup|find|research|investigate|analy[sz]e|clear|remove|delete|jump|go\s+to|move\s+to|rename|change)/iu,
@@ -104,6 +109,11 @@ export function looksLikeStructuredActionRequest(text: string): boolean {
104109
export async function extractStructuredActionPlan(
105110
ctx: StructuredNaturalActionContext
106111
): Promise<StructuredNaturalActionPlan | undefined> {
112+
const fastPlan = extractFastStructuredActionPlan(ctx.input, ctx.runs, ctx.activeRunId);
113+
if (fastPlan) {
114+
return fastPlan;
115+
}
116+
107117
const activeRun = resolveActiveRun(ctx.runs, ctx.activeRunId);
108118
const prompt = buildStructuredActionPrompt(ctx.input, ctx.runs, activeRun?.id);
109119
ctx.onProgress?.("Extracting structured action intent...");
@@ -144,6 +154,174 @@ export async function extractStructuredActionPlan(
144154
};
145155
}
146156

157+
function extractFastStructuredActionPlan(
158+
input: string,
159+
runs: RunRecord[],
160+
activeRunId?: string
161+
): StructuredNaturalActionPlan | undefined {
162+
const targetRun = resolveActiveRun(runs, activeRunId);
163+
const clearThenFollowUp = extractFastClearThenFollowupActions(input, targetRun);
164+
const actions: StructuredAction[] =
165+
clearThenFollowUp ??
166+
(() => {
167+
const analyzeAction = extractFastAnalyzeAction(input);
168+
if (analyzeAction) {
169+
return [analyzeAction];
170+
}
171+
const collectRequest = extractCollectRequestFromNatural(input);
172+
if (collectRequest) {
173+
const collectAction = convertCollectRequestToStructuredAction(collectRequest, targetRun);
174+
return collectAction ? [collectAction] : [];
175+
}
176+
const clearAction = extractFastClearAction(input);
177+
if (clearAction) {
178+
return [clearAction];
179+
}
180+
const jumpAction = extractFastJumpAction(input);
181+
return jumpAction ? [jumpAction] : [];
182+
})();
183+
184+
if (actions.length === 0) {
185+
return undefined;
186+
}
187+
const commands = actions
188+
.map((action) => buildCommandForStructuredAction(action, targetRun?.id))
189+
.filter(Boolean) as string[];
190+
if (commands.length === 0) {
191+
return undefined;
192+
}
193+
return {
194+
lines: buildSummaryLines(actions, detectLanguage(input)),
195+
commands,
196+
displayActions: actions.map((action) => summarizeAction(action, detectLanguage(input))),
197+
targetRunId: targetRun?.id
198+
};
199+
}
200+
201+
function extractFastClearThenFollowupActions(
202+
raw: string,
203+
targetRun?: RunRecord
204+
): StructuredAction[] | undefined {
205+
const clearAction = extractFastClearAction(raw);
206+
if (!clearAction) {
207+
return undefined;
208+
}
209+
const remainder = extractFollowupAfterClear(raw);
210+
if (!remainder) {
211+
return undefined;
212+
}
213+
const analyzeAction = extractFastAnalyzeAction(remainder);
214+
if (analyzeAction) {
215+
return [clearAction, analyzeAction];
216+
}
217+
const collectRequest = extractCollectRequestFromNatural(remainder);
218+
if (collectRequest) {
219+
const collectAction = convertCollectRequestToStructuredAction(collectRequest, targetRun);
220+
if (collectAction) {
221+
return [clearAction, collectAction];
222+
}
223+
}
224+
const jumpAction = extractFastJumpAction(remainder);
225+
if (jumpAction) {
226+
return [clearAction, jumpAction];
227+
}
228+
return undefined;
229+
}
230+
231+
function extractFollowupAfterClear(raw: string): string | undefined {
232+
const normalized = raw.trim();
233+
const match = normalized.match(
234+
/(?:||||clear(?:\s+out)?|remove(?:\s+all)?|delete(?:\s+all)?)([\s\S]+)/iu
235+
);
236+
const remainder = match?.[1]?.trim();
237+
if (!remainder || remainder === normalized) {
238+
return undefined;
239+
}
240+
return remainder.replace(/^(?:|then|and\s+then)\s+/iu, "").trim() || undefined;
241+
}
242+
243+
function convertCollectRequestToStructuredAction(
244+
request: CollectCommandRequest,
245+
targetRun?: RunRecord
246+
): StructuredAction | undefined {
247+
return {
248+
type: "collect",
249+
query: normalizeCollectQueryReference(request.query, targetRun),
250+
limit: request.limit,
251+
additional: request.additional,
252+
filters: {
253+
last_years: request.filters.lastYears,
254+
year: request.filters.year,
255+
date_range: request.filters.dateRange,
256+
fields: request.filters.fieldsOfStudy,
257+
venues: request.filters.venues,
258+
publication_types: request.filters.publicationTypes,
259+
min_citations: request.filters.minCitationCount,
260+
open_access: request.filters.openAccessPdf
261+
},
262+
sort: {
263+
field: request.sort.field,
264+
order: request.sort.order
265+
},
266+
bibtex_mode: request.bibtexMode,
267+
dry_run: request.dryRun
268+
};
269+
}
270+
271+
function normalizeCollectQueryReference(query: string | undefined, targetRun?: RunRecord): string | undefined {
272+
const trimmed = query?.trim();
273+
if (!trimmed) {
274+
return trimmed;
275+
}
276+
if (/^(?:title|run title| title| |)$/iu.test(trimmed)) {
277+
return targetRun?.title ?? trimmed;
278+
}
279+
if (/^(?:topic|run topic| topic| |)$/iu.test(trimmed)) {
280+
return targetRun?.topic ?? trimmed;
281+
}
282+
return trimmed;
283+
}
284+
285+
function extractFastAnalyzeAction(raw: string): StructuredAction | undefined {
286+
const normalized = raw.trim();
287+
const lower = normalized.toLowerCase();
288+
const hasAnalyzeVerb = /|analy[sz]e/u.test(normalized);
289+
if (!hasAnalyzeVerb) {
290+
return undefined;
291+
}
292+
const topMatch =
293+
normalized.match(/\s*(\d+)\s*(?:||)?/u) ||
294+
lower.match(/\btop\s+(\d+)\b/u) ||
295+
normalized.match(/(\d+)\s*(?:|||papers?)\s*(?:)?[^.\n]{0,40}/u) ||
296+
normalized.match(/[^.\n]{0,40}(\d+)\s*(?:|||papers?)/u);
297+
const topN = toPositiveInt(topMatch?.[1]);
298+
return topN ? { type: "analyze_papers", top_n: topN } : undefined;
299+
}
300+
301+
function extractFastClearAction(raw: string): StructuredAction | undefined {
302+
const normalized = raw.trim();
303+
const hasPaperWord = /|paper|papers/u.test(normalized);
304+
const hasClearVerb =
305+
/|||||clear|remove|delete/u.test(normalized) &&
306+
/|||all/u.test(normalized);
307+
if (!hasPaperWord || !hasClearVerb) {
308+
return undefined;
309+
}
310+
return { type: "clear", node: "collect_papers" };
311+
}
312+
313+
function extractFastJumpAction(raw: string): StructuredAction | undefined {
314+
const node = resolveNodeAlias(raw);
315+
if (!node) {
316+
return undefined;
317+
}
318+
const hasMoveVerb = /||||go\s+to|move\s+to|jump/u.test(raw);
319+
if (!hasMoveVerb) {
320+
return undefined;
321+
}
322+
return { type: "jump", node, force: false };
323+
}
324+
147325
function buildStructuredActionPrompt(input: string, runs: RunRecord[], activeRunId?: string): string {
148326
const runsSnapshot = runs.slice(0, 8).map((run) => ({
149327
id: run.id,
@@ -537,6 +715,7 @@ async function runForTextWithTimeout(
537715
sandboxMode?: string;
538716
approvalPolicy?: string;
539717
systemPrompt?: string;
718+
reasoningEffort?: string;
540719
abortSignal?: AbortSignal;
541720
},
542721
timeoutMs: number

src/core/constraintProfile.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ function buildConstraintPrompt(run: RunRecord): string {
121121
"",
122122
"Normalization rules:",
123123
"- Keep collect defaults generic. Explicit slash options will override these defaults later.",
124+
"- Do not set publicationTypes for generic phrases like 'papers', 'recent papers', or 'articles'. Only set specific types such as Review when explicitly requested.",
124125
"- Use targetVenue only for actual venue or paper-style constraints.",
125126
"- Use toneHint and lengthHint only when clearly stated.",
126127
"- Put practical downstream implications into experiment.designNotes / implementationNotes / evaluationNotes.",

0 commit comments

Comments
 (0)