@thurstonsand/pi-librarian 0.1.1 → 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/AGENTS.md +1 -1
- package/CONTEXT.md +10 -2
- package/README.md +31 -7
- package/RELEASE.md +33 -0
- package/docs/designs/02-continuable-runs.md +102 -0
- package/docs/designs/04-librarian-extra-tools.md +138 -0
- package/extensions/librarian/extra-tools.ts +130 -0
- package/extensions/librarian/github.ts +14 -6
- package/extensions/librarian/grep-app.ts +88 -5
- package/extensions/librarian/prompt.ts +1 -1
- package/extensions/librarian/resource-loader.ts +40 -0
- package/extensions/librarian/run.ts +97 -45
- package/extensions/librarian/settings.ts +22 -3
- package/extensions/librarian/tools/checkout-repo.ts +3 -18
- package/extensions/librarian/tools/provide-results.ts +19 -6
- package/extensions/librarian/tools/read-github-file.ts +12 -16
- package/extensions/librarian/tools/search-code.ts +3 -3
- package/extensions/librarian/tools/search-repos.ts +1 -10
- package/extensions/librarian/view.ts +70 -50
- package/extensions/librarian.ts +36 -21
- package/images/librarian-results.png +0 -0
- package/images/librarian-run.png +0 -0
- package/package.json +3 -2
|
@@ -2,7 +2,10 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { safeParseTypeBoxValue } from "../shared/typebox.ts";
|
|
3
3
|
|
|
4
4
|
const GREP_MCP_URL = "https://mcp.grep.app";
|
|
5
|
-
|
|
5
|
+
// Escalating per-attempt budgets: the upstream gateway kills stalled requests at ~15s, so
|
|
6
|
+
// early attempts abort fast (p90 success is ~500ms) instead of waiting out the full timeout.
|
|
7
|
+
const GREP_ATTEMPT_TIMEOUTS_MS = [3_000, 5_000, 7_000];
|
|
8
|
+
const GREP_RETRY_DELAY_MS = 300;
|
|
6
9
|
|
|
7
10
|
export class GrepError extends Error {
|
|
8
11
|
constructor(message: string) {
|
|
@@ -159,7 +162,12 @@ export function parseGrepMcpEvents(body: string): GrepCodeSearchResult {
|
|
|
159
162
|
continue;
|
|
160
163
|
}
|
|
161
164
|
if (result.isError) {
|
|
162
|
-
|
|
165
|
+
const message = (result.content ?? [])
|
|
166
|
+
.map((rawContent) => safeParseTypeBoxValue(MCP_TEXT_CONTENT_SCHEMA, rawContent)?.text)
|
|
167
|
+
.filter((text): text is string => text !== undefined)
|
|
168
|
+
.join("\n")
|
|
169
|
+
.trim();
|
|
170
|
+
throw new GrepError(message || "Grep MCP returned an error result.");
|
|
163
171
|
}
|
|
164
172
|
|
|
165
173
|
for (const rawContent of result.content ?? []) {
|
|
@@ -203,11 +211,45 @@ function buildGrepArguments(params: GrepCodeSearchParams): Record<string, unknow
|
|
|
203
211
|
return args;
|
|
204
212
|
}
|
|
205
213
|
|
|
206
|
-
|
|
214
|
+
function extractHtmlTitle(body: string): string | undefined {
|
|
215
|
+
const match = /<title[^>]*>(.*?)<\/title>/is.exec(body);
|
|
216
|
+
return match?.[1]?.replace(/\s+/g, " ").trim();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function formatHttpError(status: number, body: string): string {
|
|
220
|
+
return `Grep MCP returned ${status}: ${extractHtmlTitle(body) ?? body.slice(0, 200)}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function formatTransientError(error: unknown): string {
|
|
224
|
+
const cause =
|
|
225
|
+
error instanceof GrepTransientHttpError ? `returned ${error.status}` : "request failed";
|
|
226
|
+
return `Grep MCP ${cause} after retry (transient backend error). Retry, or simplify the query.`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function delay(milliseconds: number, signal: AbortSignal | undefined): Promise<void> {
|
|
230
|
+
if (signal?.aborted) {
|
|
231
|
+
return Promise.reject(signal.reason);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const timeout = setTimeout(resolve, milliseconds);
|
|
236
|
+
signal?.addEventListener(
|
|
237
|
+
"abort",
|
|
238
|
+
() => {
|
|
239
|
+
clearTimeout(timeout);
|
|
240
|
+
reject(signal.reason);
|
|
241
|
+
},
|
|
242
|
+
{ once: true },
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function searchCodeGrepAttempt(
|
|
207
248
|
params: GrepCodeSearchParams,
|
|
208
249
|
signal: AbortSignal | undefined,
|
|
250
|
+
timeoutMs: number,
|
|
209
251
|
): Promise<GrepCodeSearchResult> {
|
|
210
|
-
const timeoutSignal = AbortSignal.timeout(
|
|
252
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
211
253
|
const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
212
254
|
const response = await fetch(GREP_MCP_URL, {
|
|
213
255
|
method: "POST",
|
|
@@ -230,8 +272,49 @@ export async function searchCodeGrep(
|
|
|
230
272
|
|
|
231
273
|
const body = await response.text();
|
|
232
274
|
if (!response.ok) {
|
|
233
|
-
|
|
275
|
+
if (response.status >= 500) {
|
|
276
|
+
throw new GrepTransientHttpError(response.status, body);
|
|
277
|
+
}
|
|
278
|
+
throw new GrepError(formatHttpError(response.status, body));
|
|
234
279
|
}
|
|
235
280
|
|
|
236
281
|
return parseGrepMcpEvents(body);
|
|
237
282
|
}
|
|
283
|
+
|
|
284
|
+
// Do not extend GrepError: GrepError is the deterministic no-retry signal.
|
|
285
|
+
class GrepTransientHttpError extends Error {
|
|
286
|
+
readonly status: number;
|
|
287
|
+
|
|
288
|
+
constructor(status: number, body: string) {
|
|
289
|
+
super(formatHttpError(status, body));
|
|
290
|
+
this.name = "GrepTransientHttpError";
|
|
291
|
+
this.status = status;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function searchCodeGrep(
|
|
296
|
+
params: GrepCodeSearchParams,
|
|
297
|
+
signal: AbortSignal | undefined,
|
|
298
|
+
): Promise<GrepCodeSearchResult> {
|
|
299
|
+
if (signal?.aborted) {
|
|
300
|
+
throw signal.reason;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let transientError: unknown;
|
|
304
|
+
for (const [attempt, timeoutMs] of GREP_ATTEMPT_TIMEOUTS_MS.entries()) {
|
|
305
|
+
if (attempt > 0) {
|
|
306
|
+
await delay(GREP_RETRY_DELAY_MS, signal);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
return await searchCodeGrepAttempt(params, signal, timeoutMs);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
if (error instanceof GrepError || signal?.aborted) {
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
transientError = error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
throw new GrepError(formatTransientError(transientError));
|
|
320
|
+
}
|
|
@@ -21,7 +21,7 @@ Clones live under ${cacheDir}/repos/<owner>/<repo>. They are read-only research
|
|
|
21
21
|
|
|
22
22
|
## Citations
|
|
23
23
|
|
|
24
|
-
- Only cite files you actually read
|
|
24
|
+
- Only cite files you actually read within context.
|
|
25
25
|
- Cite as repo + file path + line range when you have one.
|
|
26
26
|
- If evidence is partial, say what is confirmed and what remains uncertain.
|
|
27
27
|
- If access fails (404/403, private repo), report that constraint plainly.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DefaultResourceLoader,
|
|
3
|
+
getAgentDir,
|
|
4
|
+
type LoadExtensionsResult,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
export function stripExtensionHooks(result: LoadExtensionsResult): LoadExtensionsResult {
|
|
8
|
+
return {
|
|
9
|
+
...result,
|
|
10
|
+
extensions: result.extensions.map((extension) => ({
|
|
11
|
+
...extension,
|
|
12
|
+
handlers: new Map(),
|
|
13
|
+
})),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LibrarianResourceLoaderOptions {
|
|
18
|
+
cacheDir: string;
|
|
19
|
+
extensionPaths: string[];
|
|
20
|
+
systemPromptOverride: (base: string | undefined) => string | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createLibrarianResourceLoader(
|
|
24
|
+
options: LibrarianResourceLoaderOptions,
|
|
25
|
+
): DefaultResourceLoader {
|
|
26
|
+
return new DefaultResourceLoader({
|
|
27
|
+
cwd: options.cacheDir,
|
|
28
|
+
agentDir: getAgentDir(),
|
|
29
|
+
noExtensions: true,
|
|
30
|
+
...(options.extensionPaths.length > 0
|
|
31
|
+
? { additionalExtensionPaths: options.extensionPaths }
|
|
32
|
+
: {}),
|
|
33
|
+
noSkills: true,
|
|
34
|
+
noPromptTemplates: true,
|
|
35
|
+
noThemes: true,
|
|
36
|
+
extensionsOverride: stripExtensionHooks,
|
|
37
|
+
skillsOverride: () => ({ skills: [], diagnostics: [] }),
|
|
38
|
+
systemPromptOverride: options.systemPromptOverride,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import type { AgentToolResult, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
3
4
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
4
5
|
import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
DefaultResourceLoader,
|
|
8
|
-
getAgentDir,
|
|
9
|
-
SessionManager,
|
|
10
|
-
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { type ExtraToolsResolution, LIBRARIAN_BASELINE_TOOL_NAMES } from "./extra-tools.ts";
|
|
11
8
|
import type { GitHubClientProvider } from "./github.ts";
|
|
12
9
|
import {
|
|
13
10
|
buildLibrarianSystemPrompt,
|
|
14
11
|
buildLibrarianUserPrompt,
|
|
15
12
|
buildProvideResultsReminder,
|
|
16
13
|
} from "./prompt.ts";
|
|
14
|
+
import { createLibrarianResourceLoader } from "./resource-loader.ts";
|
|
17
15
|
import { renderFindingsMarkdown } from "./results.ts";
|
|
18
16
|
import type { LibrarianSettings } from "./settings.ts";
|
|
19
17
|
import { createCheckoutRepoTool } from "./tools/checkout-repo.ts";
|
|
18
|
+
import { LIBRARIAN_RUN_TOOL_NAMES } from "./tools/names.ts";
|
|
20
19
|
import type { Findings } from "./tools/provide-results.ts";
|
|
21
20
|
import { createProvideResultsTool } from "./tools/provide-results.ts";
|
|
22
21
|
import { createReadGitHubFileTool } from "./tools/read-github-file.ts";
|
|
@@ -26,9 +25,12 @@ import { createSearchReposTool } from "./tools/search-repos.ts";
|
|
|
26
25
|
import { asRepoToolDetails, summarizeToolResult } from "./trace.ts";
|
|
27
26
|
|
|
28
27
|
const MAX_PROVIDE_RESULTS_REMINDERS = 3;
|
|
29
|
-
const
|
|
28
|
+
const DEFAULT_EXCLUDED_TOOLS = ["write", "edit"];
|
|
29
|
+
|
|
30
|
+
class LibrarianRunError extends Error {}
|
|
30
31
|
|
|
31
32
|
export type LibrarianRunStatus = "running" | "done" | "error" | "aborted";
|
|
33
|
+
type FailedLibrarianRunStatus = "error" | "aborted";
|
|
32
34
|
|
|
33
35
|
export interface TraceCall {
|
|
34
36
|
id: string;
|
|
@@ -49,6 +51,8 @@ export interface LibrarianRunDetails {
|
|
|
49
51
|
findings?: Findings;
|
|
50
52
|
checkouts: Record<string, string>;
|
|
51
53
|
error?: string;
|
|
54
|
+
runId?: string;
|
|
55
|
+
debugSessionPath?: string;
|
|
52
56
|
startedAt: number;
|
|
53
57
|
endedAt?: number;
|
|
54
58
|
}
|
|
@@ -57,14 +61,30 @@ export interface LibrarianRunOptions {
|
|
|
57
61
|
query: string;
|
|
58
62
|
repos: string[];
|
|
59
63
|
owners: string[];
|
|
64
|
+
continueFrom: string | undefined;
|
|
60
65
|
model: Model<Api>;
|
|
61
66
|
thinkingLevel: ThinkingLevel;
|
|
62
67
|
settings: LibrarianSettings;
|
|
68
|
+
extraTools: ExtraToolsResolution;
|
|
63
69
|
githubClient: GitHubClientProvider;
|
|
64
70
|
signal: AbortSignal | undefined;
|
|
65
71
|
onUpdate: ((details: LibrarianRunDetails) => void) | undefined;
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
async function openContinuedSession(
|
|
75
|
+
runId: string,
|
|
76
|
+
sessionsDir: string,
|
|
77
|
+
cacheDir: string,
|
|
78
|
+
fail: (status: FailedLibrarianRunStatus, content: string) => never,
|
|
79
|
+
): Promise<SessionManager> {
|
|
80
|
+
const sessions = await SessionManager.listAll(sessionsDir);
|
|
81
|
+
const sessionFile = sessions.find((candidate) => candidate.id === runId)?.path;
|
|
82
|
+
if (!sessionFile) {
|
|
83
|
+
fail("error", `Librarian run not found: ${runId}`);
|
|
84
|
+
}
|
|
85
|
+
return SessionManager.open(sessionFile, sessionsDir, cacheDir);
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
export async function runLibrarian(
|
|
69
89
|
options: LibrarianRunOptions,
|
|
70
90
|
): Promise<AgentToolResult<LibrarianRunDetails>> {
|
|
@@ -88,26 +108,45 @@ export async function runLibrarian(
|
|
|
88
108
|
options.onUpdate?.({ ...details, trace: [...details.trace] });
|
|
89
109
|
};
|
|
90
110
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
111
|
+
const finalizeDetails = (status: LibrarianRunStatus, content: string): string => {
|
|
112
|
+
const suffixes = [
|
|
113
|
+
...(details.runId ? [`run: ${details.runId}`] : []),
|
|
114
|
+
...(details.debugSessionPath ? [`debug_session: ${details.debugSessionPath}`] : []),
|
|
115
|
+
];
|
|
116
|
+
const resultContent = suffixes.length > 0 ? `${content}\n\n${suffixes.join("\n")}` : content;
|
|
96
117
|
details.status = status;
|
|
97
118
|
details.endedAt = Date.now();
|
|
98
|
-
if (
|
|
119
|
+
if (status !== "done") {
|
|
99
120
|
details.error = content.split("\n")[0] ?? content;
|
|
100
121
|
}
|
|
101
122
|
emit(true);
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
return resultContent;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const finish = (
|
|
127
|
+
status: LibrarianRunStatus,
|
|
128
|
+
content: string,
|
|
129
|
+
): AgentToolResult<LibrarianRunDetails> => ({
|
|
130
|
+
content: [{ type: "text", text: finalizeDetails(status, content) }],
|
|
131
|
+
details: { ...details, trace: [...details.trace] },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const fail = (status: FailedLibrarianRunStatus, content: string): never => {
|
|
135
|
+
throw new LibrarianRunError(finalizeDetails(status, content));
|
|
107
136
|
};
|
|
108
137
|
|
|
109
138
|
await fs.mkdir(options.settings.cacheDir, { recursive: true });
|
|
110
139
|
|
|
140
|
+
const sessionsDir = path.join(options.settings.cacheDir, "sessions");
|
|
141
|
+
const sessionManager = options.continueFrom
|
|
142
|
+
? await openContinuedSession(options.continueFrom, sessionsDir, options.settings.cacheDir, fail)
|
|
143
|
+
: SessionManager.create(options.settings.cacheDir, sessionsDir);
|
|
144
|
+
details.runId = sessionManager.getSessionId();
|
|
145
|
+
const debugSessionPath = sessionManager.getSessionFile();
|
|
146
|
+
if (options.settings.debug.persistRuns && debugSessionPath) {
|
|
147
|
+
details.debugSessionPath = debugSessionPath;
|
|
148
|
+
}
|
|
149
|
+
|
|
111
150
|
let findings: Findings | undefined;
|
|
112
151
|
const repoTools = [
|
|
113
152
|
createSearchReposTool(options.githubClient),
|
|
@@ -120,21 +159,27 @@ export async function runLibrarian(
|
|
|
120
159
|
}),
|
|
121
160
|
];
|
|
122
161
|
|
|
123
|
-
const resourceLoader =
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
noExtensions: true,
|
|
127
|
-
...(options.settings.extensions.length > 0
|
|
128
|
-
? { additionalExtensionPaths: options.settings.extensions }
|
|
129
|
-
: {}),
|
|
130
|
-
noSkills: true,
|
|
131
|
-
noPromptTemplates: true,
|
|
132
|
-
noThemes: true,
|
|
162
|
+
const resourceLoader = createLibrarianResourceLoader({
|
|
163
|
+
cacheDir: options.settings.cacheDir,
|
|
164
|
+
extensionPaths: options.extraTools.extensionPaths,
|
|
133
165
|
systemPromptOverride: () => buildLibrarianSystemPrompt(options.settings.cacheDir),
|
|
134
|
-
skillsOverride: () => ({ skills: [], diagnostics: [] }),
|
|
135
166
|
});
|
|
136
167
|
await resourceLoader.reload();
|
|
137
168
|
|
|
169
|
+
const extensionLoadErrors = resourceLoader.getExtensions().errors;
|
|
170
|
+
for (const [index, error] of extensionLoadErrors.entries()) {
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
details.trace.push({
|
|
173
|
+
id: `extension-load-${index}`,
|
|
174
|
+
name: "load_extension",
|
|
175
|
+
args: { path: error.path },
|
|
176
|
+
startedAt: now,
|
|
177
|
+
endedAt: now,
|
|
178
|
+
isError: true,
|
|
179
|
+
resultSummary: error.error,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
138
183
|
emit(true);
|
|
139
184
|
|
|
140
185
|
let session: AgentSession | undefined;
|
|
@@ -145,20 +190,21 @@ export async function runLibrarian(
|
|
|
145
190
|
const created = await createAgentSession({
|
|
146
191
|
cwd: options.settings.cacheDir,
|
|
147
192
|
resourceLoader,
|
|
148
|
-
sessionManager
|
|
193
|
+
sessionManager,
|
|
149
194
|
model: options.model,
|
|
150
195
|
thinkingLevel: options.thinkingLevel,
|
|
151
196
|
customTools: repoTools,
|
|
152
|
-
excludeTools:
|
|
197
|
+
excludeTools: DEFAULT_EXCLUDED_TOOLS.filter(
|
|
198
|
+
(toolName) => !options.extraTools.toolNames.includes(toolName),
|
|
199
|
+
),
|
|
153
200
|
});
|
|
154
201
|
session = created.session;
|
|
155
202
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
.
|
|
160
|
-
|
|
161
|
-
session.setActiveToolsByName(allToolNames);
|
|
203
|
+
session.setActiveToolsByName([
|
|
204
|
+
...LIBRARIAN_BASELINE_TOOL_NAMES,
|
|
205
|
+
...LIBRARIAN_RUN_TOOL_NAMES,
|
|
206
|
+
...options.extraTools.toolNames,
|
|
207
|
+
]);
|
|
162
208
|
|
|
163
209
|
unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
164
210
|
switch (event.type) {
|
|
@@ -198,7 +244,7 @@ export async function runLibrarian(
|
|
|
198
244
|
});
|
|
199
245
|
|
|
200
246
|
if (options.signal?.aborted) {
|
|
201
|
-
|
|
247
|
+
fail("aborted", "Librarian run aborted before it started.");
|
|
202
248
|
}
|
|
203
249
|
|
|
204
250
|
const activeSession = session;
|
|
@@ -214,30 +260,34 @@ export async function runLibrarian(
|
|
|
214
260
|
let reminders = 0;
|
|
215
261
|
while (!findings && !options.signal?.aborted && reminders < MAX_PROVIDE_RESULTS_REMINDERS) {
|
|
216
262
|
reminders += 1;
|
|
217
|
-
await session.prompt(buildProvideResultsReminder(), {
|
|
263
|
+
await session.prompt(buildProvideResultsReminder(), {
|
|
264
|
+
expandPromptTemplates: false,
|
|
265
|
+
});
|
|
218
266
|
}
|
|
219
267
|
|
|
220
268
|
if (options.signal?.aborted) {
|
|
221
|
-
|
|
269
|
+
fail("aborted", "Librarian run aborted.");
|
|
222
270
|
}
|
|
223
271
|
|
|
224
272
|
if (findings) {
|
|
225
273
|
details.findings = findings;
|
|
226
|
-
return finish("done", renderFindingsMarkdown(findings, details.checkouts)
|
|
274
|
+
return finish("done", renderFindingsMarkdown(findings, details.checkouts));
|
|
227
275
|
}
|
|
228
276
|
|
|
229
277
|
const lastText = session.getLastAssistantText() ?? "";
|
|
230
|
-
|
|
278
|
+
fail(
|
|
231
279
|
"error",
|
|
232
280
|
`Librarian did not report structured findings after ${MAX_PROVIDE_RESULTS_REMINDERS} reminders.${lastText ? `\n\nRaw final message:\n${lastText}` : ""}`,
|
|
233
|
-
true,
|
|
234
281
|
);
|
|
235
282
|
} catch (error) {
|
|
283
|
+
if (error instanceof LibrarianRunError) {
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
236
286
|
if (options.signal?.aborted) {
|
|
237
|
-
|
|
287
|
+
fail("aborted", "Librarian run aborted.");
|
|
238
288
|
}
|
|
239
289
|
const message = error instanceof Error ? error.message : String(error);
|
|
240
|
-
|
|
290
|
+
fail("error", `Librarian run failed: ${message}`);
|
|
241
291
|
} finally {
|
|
242
292
|
if (onAbort) {
|
|
243
293
|
options.signal?.removeEventListener("abort", onAbort);
|
|
@@ -245,4 +295,6 @@ export async function runLibrarian(
|
|
|
245
295
|
unsubscribe?.();
|
|
246
296
|
session?.dispose();
|
|
247
297
|
}
|
|
298
|
+
|
|
299
|
+
throw new Error("Unreachable librarian run state.");
|
|
248
300
|
}
|
|
@@ -18,8 +18,13 @@ const LIBRARIAN_FILE_SETTINGS_SCHEMA = Type.Object({
|
|
|
18
18
|
model: Type.Optional(Type.String()),
|
|
19
19
|
thinkingLevel: Type.Optional(THINKING_LEVEL_SCHEMA),
|
|
20
20
|
extensions: Type.Optional(Type.Array(Type.String())),
|
|
21
|
-
|
|
21
|
+
tools: Type.Optional(Type.Array(Type.String())),
|
|
22
22
|
cacheDir: Type.Optional(Type.String()),
|
|
23
|
+
debug: Type.Optional(
|
|
24
|
+
Type.Object({
|
|
25
|
+
persistRuns: Type.Optional(Type.Boolean()),
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
23
28
|
});
|
|
24
29
|
|
|
25
30
|
const ROOT_SETTINGS_SCHEMA = Type.Object({
|
|
@@ -43,8 +48,11 @@ export interface LibrarianSettings {
|
|
|
43
48
|
model: ModelReference | undefined;
|
|
44
49
|
thinkingLevel: ThinkingLevel | undefined;
|
|
45
50
|
extensions: string[];
|
|
46
|
-
|
|
51
|
+
tools: string[];
|
|
47
52
|
cacheDir: string;
|
|
53
|
+
debug: {
|
|
54
|
+
persistRuns: boolean;
|
|
55
|
+
};
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
export function getDefaultCacheDir(): string {
|
|
@@ -102,13 +110,24 @@ function normalizeExtensionPaths(values: string[] | undefined): string[] {
|
|
|
102
110
|
.map(expandHome);
|
|
103
111
|
}
|
|
104
112
|
|
|
113
|
+
function normalizeToolNames(values: string[] | undefined): string[] {
|
|
114
|
+
if (!values) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))];
|
|
119
|
+
}
|
|
120
|
+
|
|
105
121
|
export function resolveLibrarianSettings(fileSettings: LibrarianFileSettings): LibrarianSettings {
|
|
106
122
|
return {
|
|
107
123
|
model: parseModelReference(fileSettings.model),
|
|
108
124
|
thinkingLevel: fileSettings.thinkingLevel,
|
|
109
125
|
extensions: normalizeExtensionPaths(fileSettings.extensions),
|
|
110
|
-
|
|
126
|
+
tools: normalizeToolNames(fileSettings.tools),
|
|
111
127
|
cacheDir: normalizeCacheDir(fileSettings.cacheDir),
|
|
128
|
+
debug: {
|
|
129
|
+
persistRuns: fileSettings.debug?.persistRuns ?? false,
|
|
130
|
+
},
|
|
112
131
|
};
|
|
113
132
|
}
|
|
114
133
|
|
|
@@ -64,24 +64,9 @@ export function createCheckoutRepoTool(cacheDir: string) {
|
|
|
64
64
|
: error instanceof Error
|
|
65
65
|
? error.message
|
|
66
66
|
: String(error);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
type: "text",
|
|
71
|
-
text: `${message}\nIf the repo name is uncertain, resolve it with search_repos first.`,
|
|
72
|
-
},
|
|
73
|
-
],
|
|
74
|
-
details: {
|
|
75
|
-
kind: "checkout_repo",
|
|
76
|
-
repo: params.repo,
|
|
77
|
-
path: "",
|
|
78
|
-
headSha: "",
|
|
79
|
-
ref: params.ref ?? "",
|
|
80
|
-
fileCount: 0,
|
|
81
|
-
reusedClone: false,
|
|
82
|
-
},
|
|
83
|
-
isError: true,
|
|
84
|
-
};
|
|
67
|
+
throw new Error(
|
|
68
|
+
`${message}\nIf the repo name is uncertain, resolve it with search_repos first.`,
|
|
69
|
+
);
|
|
85
70
|
}
|
|
86
71
|
},
|
|
87
72
|
});
|
|
@@ -3,10 +3,16 @@ import { type Static, Type } from "typebox";
|
|
|
3
3
|
import { LIBRARIAN_TOOL_NAMES } from "./names.ts";
|
|
4
4
|
|
|
5
5
|
export const LocationSchema = Type.Object({
|
|
6
|
-
repo: Type.String({
|
|
7
|
-
|
|
6
|
+
repo: Type.String({
|
|
7
|
+
description: "Repository as owner/repo or a repository URL.",
|
|
8
|
+
}),
|
|
9
|
+
file: Type.String({
|
|
10
|
+
description: "Validated file path within the repository.",
|
|
11
|
+
}),
|
|
8
12
|
lines: Type.Optional(
|
|
9
|
-
Type.String({
|
|
13
|
+
Type.String({
|
|
14
|
+
description: 'Line range like "80-140" or a single line like "42".',
|
|
15
|
+
}),
|
|
10
16
|
),
|
|
11
17
|
note: Type.String({ description: "Relevance of the file." }),
|
|
12
18
|
});
|
|
@@ -16,7 +22,8 @@ export const FindingsSchema = Type.Object({
|
|
|
16
22
|
description: "1-3 sentence direct answer to the query. No preamble.",
|
|
17
23
|
}),
|
|
18
24
|
locations: Type.Array(LocationSchema, {
|
|
19
|
-
description:
|
|
25
|
+
description:
|
|
26
|
+
"Evidence source-file locations. Include only repository files you validated by reading.",
|
|
20
27
|
}),
|
|
21
28
|
description: Type.Optional(
|
|
22
29
|
Type.String({
|
|
@@ -50,9 +57,15 @@ export function createProvideResultsTool(onFindings: (findings: Findings) => voi
|
|
|
50
57
|
onFindings(params);
|
|
51
58
|
return {
|
|
52
59
|
content: [
|
|
53
|
-
{
|
|
60
|
+
{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: "Findings recorded. You are done; do not call more tools.",
|
|
63
|
+
},
|
|
54
64
|
],
|
|
55
|
-
details: {
|
|
65
|
+
details: {
|
|
66
|
+
kind: "provide_results",
|
|
67
|
+
locationCount: params.locations.length,
|
|
68
|
+
},
|
|
56
69
|
terminate: true,
|
|
57
70
|
};
|
|
58
71
|
},
|
|
@@ -22,8 +22,10 @@ const ReadGitHubFileParams = Type.Object({
|
|
|
22
22
|
Type.String({ description: "Branch, tag, or commit sha. Default: the default branch." }),
|
|
23
23
|
),
|
|
24
24
|
range: Type.Optional(
|
|
25
|
-
Type.
|
|
25
|
+
Type.Array(Type.Number({ minimum: 1 }), {
|
|
26
26
|
description: "[startLine, endLine] (1-based, inclusive) for large files.",
|
|
27
|
+
minItems: 2,
|
|
28
|
+
maxItems: 2,
|
|
27
29
|
}),
|
|
28
30
|
),
|
|
29
31
|
});
|
|
@@ -42,6 +44,11 @@ export function createReadGitHubFileTool(githubClient: GitHubClientProvider) {
|
|
|
42
44
|
parameters: ReadGitHubFileParams,
|
|
43
45
|
|
|
44
46
|
async execute(_toolCallId, params) {
|
|
47
|
+
const range = params.range ? (params.range as [number, number]) : undefined;
|
|
48
|
+
if (range && range[1] < range[0]) {
|
|
49
|
+
throw new Error("range end must be greater than or equal to range start.");
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
try {
|
|
46
53
|
const github = await githubClient();
|
|
47
54
|
const contents = await github.readContents({
|
|
@@ -72,9 +79,9 @@ export function createReadGitHubFileTool(githubClient: GitHubClientProvider) {
|
|
|
72
79
|
let end = allLines.length;
|
|
73
80
|
let clipNote = "";
|
|
74
81
|
|
|
75
|
-
if (
|
|
76
|
-
start = Math.min(
|
|
77
|
-
end = Math.min(
|
|
82
|
+
if (range) {
|
|
83
|
+
start = Math.min(range[0], allLines.length);
|
|
84
|
+
end = Math.min(range[1], allLines.length);
|
|
78
85
|
} else if (allLines.length > MAX_LINES_WITHOUT_RANGE) {
|
|
79
86
|
end = MAX_LINES_WITHOUT_RANGE;
|
|
80
87
|
clipNote = `\n... clipped at line ${MAX_LINES_WITHOUT_RANGE} of ${allLines.length}; pass range to read further.`;
|
|
@@ -104,18 +111,7 @@ export function createReadGitHubFileTool(githubClient: GitHubClientProvider) {
|
|
|
104
111
|
: error instanceof Error
|
|
105
112
|
? error.message
|
|
106
113
|
: String(error);
|
|
107
|
-
|
|
108
|
-
content: [{ type: "text", text: message }],
|
|
109
|
-
details: {
|
|
110
|
-
kind: "read_github_file",
|
|
111
|
-
owner: params.owner,
|
|
112
|
-
repo: params.repo,
|
|
113
|
-
path: params.path,
|
|
114
|
-
lineCount: 0,
|
|
115
|
-
isDirectory: false,
|
|
116
|
-
},
|
|
117
|
-
isError: true,
|
|
118
|
-
};
|
|
114
|
+
throw new Error(message);
|
|
119
115
|
}
|
|
120
116
|
},
|
|
121
117
|
});
|
|
@@ -14,12 +14,11 @@ export interface SearchCodeDetails {
|
|
|
14
14
|
const SearchCodeParams = Type.Object({
|
|
15
15
|
pattern: Type.String({
|
|
16
16
|
description:
|
|
17
|
-
"Public code search pattern.
|
|
17
|
+
"Public code search pattern. Use literal code patterns as they appear in files, not keywords: e.g. `useState(` rather than `react hook state`.",
|
|
18
18
|
}),
|
|
19
19
|
regex: Type.Optional(
|
|
20
20
|
Type.Boolean({
|
|
21
|
-
description:
|
|
22
|
-
"Treat pattern as a regular expression. Grep supports multi-line regex with (?s).",
|
|
21
|
+
description: "Treat `pattern` as a regular expression instead of literals.",
|
|
23
22
|
}),
|
|
24
23
|
),
|
|
25
24
|
repo: Type.Optional(
|
|
@@ -62,6 +61,7 @@ export const searchCodeTool = defineTool<typeof SearchCodeParams, SearchCodeDeta
|
|
|
62
61
|
description: "Cross-repo code search over public source code.",
|
|
63
62
|
promptSnippet: "Search code across repos",
|
|
64
63
|
promptGuidelines: [
|
|
64
|
+
"When `regex` is true, `pattern` can also use `(?s)` for multi-line patterns.",
|
|
65
65
|
"Results are CANDIDATES to verify via checkout_repo/read_github_file — never cite them directly.",
|
|
66
66
|
"This searches public GitHub code only. Use search_github_code when private repository access matters.",
|
|
67
67
|
],
|
|
@@ -85,16 +85,7 @@ export function createSearchReposTool(githubClient: GitHubClientProvider) {
|
|
|
85
85
|
: error instanceof Error
|
|
86
86
|
? error.message
|
|
87
87
|
: String(error);
|
|
88
|
-
|
|
89
|
-
content: [{ type: "text", text: message }],
|
|
90
|
-
details: {
|
|
91
|
-
kind: "search_repos",
|
|
92
|
-
query: params.query,
|
|
93
|
-
resultCount: 0,
|
|
94
|
-
totalCount: 0,
|
|
95
|
-
},
|
|
96
|
-
isError: true,
|
|
97
|
-
};
|
|
88
|
+
throw new Error(message);
|
|
98
89
|
}
|
|
99
90
|
},
|
|
100
91
|
});
|