@oh-my-pi/pi-coding-agent 14.2.0 → 14.3.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/CHANGELOG.md +59 -0
- package/package.json +19 -19
- package/src/cli/args.ts +10 -1
- package/src/cli/shell-cli.ts +15 -3
- package/src/config/settings-schema.ts +60 -1
- package/src/dap/session.ts +8 -2
- package/src/debug/system-info.ts +6 -2
- package/src/discovery/claude.ts +58 -36
- package/src/discovery/opencode.ts +20 -2
- package/src/edit/index.ts +3 -1
- package/src/edit/modes/chunk.ts +133 -53
- package/src/edit/modes/hashline.ts +36 -11
- package/src/edit/renderer.ts +98 -133
- package/src/edit/streaming.ts +351 -0
- package/src/exec/bash-executor.ts +60 -5
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/pi-protocol.ts +0 -2
- package/src/lsp/client.ts +22 -6
- package/src/lsp/defaults.json +2 -1
- package/src/lsp/index.ts +53 -10
- package/src/lsp/types.ts +2 -0
- package/src/modes/acp/acp-agent.ts +76 -2
- package/src/modes/components/assistant-message.ts +1 -34
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/tool-execution.ts +111 -101
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +0 -2
- package/src/modes/theme/mermaid-cache.ts +13 -52
- package/src/modes/theme/theme.ts +2 -2
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/chunk-edit.md +25 -22
- package/src/prompts/tools/gh-pr-push.md +2 -1
- package/src/prompts/tools/grep.md +4 -3
- package/src/prompts/tools/lsp.md +6 -0
- package/src/prompts/tools/read-chunk.md +46 -7
- package/src/prompts/tools/read.md +7 -4
- package/src/sdk.ts +8 -5
- package/src/session/agent-session.ts +36 -20
- package/src/session/session-manager.ts +228 -57
- package/src/session/streaming-output.ts +11 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +1 -0
- package/src/tools/ast-edit.ts +37 -2
- package/src/tools/bash.ts +75 -12
- package/src/tools/find.ts +19 -26
- package/src/tools/gh.ts +6 -16
- package/src/tools/grep.ts +94 -37
- package/src/tools/path-utils.ts +31 -3
- package/src/tools/resolve.ts +12 -3
- package/src/tools/sqlite-reader.ts +116 -3
- package/src/tools/vim.ts +1 -1
- package/src/web/search/providers/codex.ts +129 -6
package/src/tools/path-utils.ts
CHANGED
|
@@ -193,6 +193,7 @@ export interface ResolvedMultiSearchPath {
|
|
|
193
193
|
basePath: string;
|
|
194
194
|
glob?: string;
|
|
195
195
|
scopePath: string;
|
|
196
|
+
exactFilePaths?: string[];
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
export interface ResolvedMultiFindPattern {
|
|
@@ -438,6 +439,28 @@ async function areDelimitedTokensResolvable(
|
|
|
438
439
|
return true;
|
|
439
440
|
}
|
|
440
441
|
|
|
442
|
+
async function filterResolvableTokens(
|
|
443
|
+
tokens: string[],
|
|
444
|
+
cwd: string,
|
|
445
|
+
parseBasePath: (value: string) => string,
|
|
446
|
+
): Promise<string[]> {
|
|
447
|
+
const out: string[] = [];
|
|
448
|
+
for (const token of tokens) {
|
|
449
|
+
if (TOP_LEVEL_INTERNAL_URL_PREFIXES.some(prefix => token.startsWith(prefix))) continue;
|
|
450
|
+
const basePath = parseBasePath(token);
|
|
451
|
+
const resolvedBasePath = resolveToCwd(basePath, cwd);
|
|
452
|
+
if (await pathExists(resolvedBasePath)) {
|
|
453
|
+
out.push(token);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const resolvedExactPath = resolveToCwd(token, cwd);
|
|
457
|
+
if (await pathExists(resolvedExactPath)) {
|
|
458
|
+
out.push(token);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
|
|
441
464
|
async function splitDelimitedSearchInput(
|
|
442
465
|
rawInput: string,
|
|
443
466
|
cwd: string,
|
|
@@ -452,8 +475,11 @@ async function splitDelimitedSearchInput(
|
|
|
452
475
|
}
|
|
453
476
|
|
|
454
477
|
const commaSeparated = splitTopLevel(trimmed, "comma");
|
|
455
|
-
if (commaSeparated.length > 1
|
|
456
|
-
|
|
478
|
+
if (commaSeparated.length > 1) {
|
|
479
|
+
const resolvable = await filterResolvableTokens(commaSeparated, cwd, parseBasePath);
|
|
480
|
+
if (resolvable.length >= 1) {
|
|
481
|
+
return [...new Set(resolvable)];
|
|
482
|
+
}
|
|
457
483
|
}
|
|
458
484
|
|
|
459
485
|
const whitespaceSeparated = splitTopLevel(trimmed, "whitespace");
|
|
@@ -473,7 +499,7 @@ export async function resolveMultiSearchPath(
|
|
|
473
499
|
suffixGlob?: string,
|
|
474
500
|
): Promise<ResolvedMultiSearchPath | undefined> {
|
|
475
501
|
const pathItems = await splitDelimitedSearchInput(rawPath, cwd, value => parseSearchPath(value).basePath);
|
|
476
|
-
if (!pathItems || pathItems.length
|
|
502
|
+
if (!pathItems || pathItems.length < 1) {
|
|
477
503
|
return undefined;
|
|
478
504
|
}
|
|
479
505
|
|
|
@@ -486,6 +512,7 @@ export async function resolveMultiSearchPath(
|
|
|
486
512
|
}),
|
|
487
513
|
);
|
|
488
514
|
|
|
515
|
+
const allExactFiles = !suffixGlob && parsedItems.every(item => !item.parsedPath.glob && item.stat.isFile());
|
|
489
516
|
const commonBasePath = findCommonBasePath(parsedItems.map(item => item.absoluteBasePath));
|
|
490
517
|
const combinedPatterns = parsedItems.map(item => {
|
|
491
518
|
const relativeBasePath = normalizePosixPath(path.relative(commonBasePath, item.absoluteBasePath)) || ".";
|
|
@@ -507,6 +534,7 @@ export async function resolveMultiSearchPath(
|
|
|
507
534
|
basePath: commonBasePath,
|
|
508
535
|
glob: buildBraceUnion(combinedPatterns),
|
|
509
536
|
scopePath: toScopeDisplay(pathItems),
|
|
537
|
+
exactFilePaths: allExactFiles ? parsedItems.map(item => item.absoluteBasePath) : undefined,
|
|
510
538
|
};
|
|
511
539
|
}
|
|
512
540
|
|
package/src/tools/resolve.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface ResolveToolDetails {
|
|
|
23
23
|
reason: string;
|
|
24
24
|
sourceToolName?: string;
|
|
25
25
|
label?: string;
|
|
26
|
+
sourceResultDetails?: unknown;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function resolveReasonPreview(reason?: string): string | undefined {
|
|
@@ -67,14 +68,21 @@ export function queueResolveHandler(
|
|
|
67
68
|
onRejected: () => "requeue",
|
|
68
69
|
onInvoked: async (input: unknown) => {
|
|
69
70
|
const params = input as ResolveParams;
|
|
71
|
+
const withResolveDetails = (result: AgentToolResult<unknown>): AgentToolResult<ResolveToolDetails> => ({
|
|
72
|
+
...result,
|
|
73
|
+
details: {
|
|
74
|
+
...detailsFor(params),
|
|
75
|
+
...(result.details != null ? { sourceResultDetails: result.details } : {}),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
70
78
|
if (params.action === "apply") {
|
|
71
79
|
const result = await options.apply(params.reason);
|
|
72
|
-
return
|
|
80
|
+
return withResolveDetails(result);
|
|
73
81
|
}
|
|
74
82
|
if (params.action === "discard" && options.reject != null) {
|
|
75
83
|
const result = await options.reject(params.reason);
|
|
76
84
|
if (result != null) {
|
|
77
|
-
return
|
|
85
|
+
return withResolveDetails(result);
|
|
78
86
|
}
|
|
79
87
|
}
|
|
80
88
|
return {
|
|
@@ -154,9 +162,10 @@ export const resolveToolRenderer = {
|
|
|
154
162
|
const reason = replaceTabs(details?.reason?.trim() || "No reason provided");
|
|
155
163
|
const action = details?.action ?? "apply";
|
|
156
164
|
const isApply = action === "apply" && !result.isError;
|
|
165
|
+
const isFailedApply = action === "apply" && result.isError;
|
|
157
166
|
const bgColor = result.isError ? "error" : isApply ? "success" : "warning";
|
|
158
167
|
const icon = isApply ? uiTheme.status.success : uiTheme.status.error;
|
|
159
|
-
const verb = isApply ? "Accept" : "Discard";
|
|
168
|
+
const verb = isApply ? "Accept" : isFailedApply ? "Failed" : "Discard";
|
|
160
169
|
const separator = ": ";
|
|
161
170
|
const separatorIndex = label.indexOf(separator);
|
|
162
171
|
const sourceLabel = separatorIndex > 0 ? label.slice(0, separatorIndex).trim() : undefined;
|
|
@@ -252,6 +252,112 @@ function resolveOrderClause(order: string | undefined, columns: string[]): strin
|
|
|
252
252
|
return ` ORDER BY ${quoteSqliteIdentifier(column)} ${direction.toUpperCase()}`;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
const FORBIDDEN_WHERE_KEYWORDS = new Set([
|
|
256
|
+
"limit",
|
|
257
|
+
"offset",
|
|
258
|
+
"union",
|
|
259
|
+
"intersect",
|
|
260
|
+
"except",
|
|
261
|
+
"attach",
|
|
262
|
+
"detach",
|
|
263
|
+
"pragma",
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
const COMMENT_OR_TERMINATOR_ERROR =
|
|
267
|
+
"SQLite 'where' clause must not contain comments or statement terminators; use '?q=SELECT ...' for raw SQL";
|
|
268
|
+
const FORBIDDEN_KEYWORD_ERROR =
|
|
269
|
+
"SQLite 'where' clause must not contain LIMIT/OFFSET/UNION/INTERSECT/EXCEPT/ATTACH/DETACH/PRAGMA; use '?q=SELECT ...' for raw SQL";
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Scans a `where=` clause character-by-character, tracking single- and double-quoted
|
|
273
|
+
* string literals, and rejects SQL control syntax that would otherwise let the
|
|
274
|
+
* structured helper path escape the bound `LIMIT ? OFFSET ?` pagination:
|
|
275
|
+
*
|
|
276
|
+
* - comments (`--`, `/* ... *\/`) and statement terminators (`;`) outside quotes
|
|
277
|
+
* - pagination / attach / pragma keywords outside quotes
|
|
278
|
+
*
|
|
279
|
+
* Raw SQL remains available through `?q=SELECT ...`.
|
|
280
|
+
*/
|
|
281
|
+
function findWhereClauseViolation(sql: string): string | null {
|
|
282
|
+
let inSingleQuote = false;
|
|
283
|
+
let inDoubleQuote = false;
|
|
284
|
+
let tokenStart = -1;
|
|
285
|
+
let keywordViolation: string | null = null;
|
|
286
|
+
|
|
287
|
+
const flushToken = (end: number): void => {
|
|
288
|
+
if (tokenStart < 0 || keywordViolation) {
|
|
289
|
+
tokenStart = -1;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const token = sql.slice(tokenStart, end).toLowerCase();
|
|
293
|
+
tokenStart = -1;
|
|
294
|
+
if (FORBIDDEN_WHERE_KEYWORDS.has(token)) {
|
|
295
|
+
keywordViolation = FORBIDDEN_KEYWORD_ERROR;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
for (let index = 0; index <= sql.length; index++) {
|
|
300
|
+
const char = index < sql.length ? sql[index] : undefined;
|
|
301
|
+
const next = index + 1 < sql.length ? sql[index + 1] : undefined;
|
|
302
|
+
|
|
303
|
+
if (inSingleQuote) {
|
|
304
|
+
if (char === "'" && next === "'") {
|
|
305
|
+
index += 1;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (char === "'") {
|
|
309
|
+
inSingleQuote = false;
|
|
310
|
+
}
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (inDoubleQuote) {
|
|
314
|
+
if (char === '"' && next === '"') {
|
|
315
|
+
index += 1;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (char === '"') {
|
|
319
|
+
inDoubleQuote = false;
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const isIdent = char !== undefined && /[A-Za-z0-9_]/.test(char);
|
|
325
|
+
if (isIdent) {
|
|
326
|
+
if (tokenStart < 0) tokenStart = index;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
flushToken(index);
|
|
331
|
+
|
|
332
|
+
if (char === undefined) break;
|
|
333
|
+
if (char === "'") {
|
|
334
|
+
inSingleQuote = true;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (char === '"') {
|
|
338
|
+
inDoubleQuote = true;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (char === ";") return COMMENT_OR_TERMINATOR_ERROR;
|
|
342
|
+
if ((char === "-" && next === "-") || (char === "/" && next === "*") || (char === "*" && next === "/")) {
|
|
343
|
+
return COMMENT_OR_TERMINATOR_ERROR;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return keywordViolation;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function validateWhereClause(where: string | undefined): string | undefined {
|
|
351
|
+
if (!where) return undefined;
|
|
352
|
+
const trimmed = where.trim();
|
|
353
|
+
if (!trimmed) return undefined;
|
|
354
|
+
const violation = findWhereClauseViolation(trimmed);
|
|
355
|
+
if (violation) {
|
|
356
|
+
throw new ToolError(violation);
|
|
357
|
+
}
|
|
358
|
+
return trimmed;
|
|
359
|
+
}
|
|
360
|
+
|
|
255
361
|
function normalizeWriteValue(value: unknown, column: string): SqliteBinding {
|
|
256
362
|
if (value === null) return null;
|
|
257
363
|
if (
|
|
@@ -360,7 +466,7 @@ export function parseSqliteSelector(subPath: string, queryString: string): Sqlit
|
|
|
360
466
|
return { kind: "row", table, key };
|
|
361
467
|
}
|
|
362
468
|
|
|
363
|
-
const where = params.get("where")
|
|
469
|
+
const where = validateWhereClause(params.get("where") ?? undefined);
|
|
364
470
|
const order = params.get("order")?.trim() || undefined;
|
|
365
471
|
const hasQueryParams = params.has("limit") || params.has("offset") || order !== undefined || where !== undefined;
|
|
366
472
|
if (hasQueryParams) {
|
|
@@ -448,12 +554,19 @@ export function queryRows(
|
|
|
448
554
|
opts: { limit: number; offset: number; order?: string; where?: string },
|
|
449
555
|
): { columns: string[]; rows: Record<string, unknown>[]; totalCount: number } {
|
|
450
556
|
const columns = getTableColumns(db, table);
|
|
451
|
-
const
|
|
557
|
+
const validatedWhere = validateWhereClause(opts.where);
|
|
558
|
+
const whereClause = validatedWhere ? ` WHERE ${validatedWhere}` : "";
|
|
452
559
|
const orderClause = resolveOrderClause(opts.order, columns);
|
|
453
560
|
const countSql = `SELECT COUNT(*) AS count FROM ${quoteSqliteIdentifier(table)}${whereClause}`;
|
|
454
561
|
const selectSql = `SELECT * FROM ${quoteSqliteIdentifier(table)}${whereClause}${orderClause} LIMIT ? OFFSET ?`;
|
|
455
562
|
const totalCount = db.prepare<SqliteCountRow, []>(countSql).get()?.count ?? 0;
|
|
456
|
-
const
|
|
563
|
+
const statement = db.prepare<SqliteRow, SQLQueryBindings[]>(selectSql);
|
|
564
|
+
if (statement.paramsCount !== 2) {
|
|
565
|
+
throw new ToolError(
|
|
566
|
+
"SQLite where clause changed the expected pagination parameters; use q=SELECT ... for raw SQL",
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
const rows = statement.all(opts.limit, opts.offset);
|
|
457
570
|
return { columns, rows, totalCount };
|
|
458
571
|
}
|
|
459
572
|
|
package/src/tools/vim.ts
CHANGED
|
@@ -639,7 +639,7 @@ export class VimTool implements AgentTool<typeof vimSchema, VimToolDetails> {
|
|
|
639
639
|
|
|
640
640
|
await executeVimSteps(engine, steps, {
|
|
641
641
|
pauseLastStep: params.pause === true,
|
|
642
|
-
onKbdStep: emitUpdate ? () => emitUpdate() : undefined,
|
|
642
|
+
onKbdStep: emitUpdate ? () => emitUpdate(true) : undefined,
|
|
643
643
|
onInsertStep: emitUpdate ? () => emitUpdate(true) : undefined,
|
|
644
644
|
});
|
|
645
645
|
|
|
@@ -106,6 +106,125 @@ function isImagePlaceholderAnswer(text: string): boolean {
|
|
|
106
106
|
return text.trim().toLowerCase() === "(see attached image)";
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
function addSource(sources: SearchSource[], source: SearchSource): void {
|
|
110
|
+
if (!sources.some(existing => existing.url === source.url)) {
|
|
111
|
+
sources.push(source);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function countCharacter(text: string, target: string): number {
|
|
116
|
+
let count = 0;
|
|
117
|
+
for (const char of text) {
|
|
118
|
+
if (char === target) {
|
|
119
|
+
count += 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return count;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Strips prose punctuation and unmatched closing delimiters from extracted URLs.
|
|
127
|
+
* Codex often returns links in markdown or sentence text without structured annotations.
|
|
128
|
+
*/
|
|
129
|
+
function normalizeExtractedUrl(candidate: string): string | null {
|
|
130
|
+
let url = candidate.trim();
|
|
131
|
+
|
|
132
|
+
while (url.length > 0) {
|
|
133
|
+
const lastCharacter = url.at(-1);
|
|
134
|
+
if (!lastCharacter) break;
|
|
135
|
+
if (/[.,!?;:'"]/u.test(lastCharacter)) {
|
|
136
|
+
url = url.slice(0, -1);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (lastCharacter === ")" && countCharacter(url, ")") > countCharacter(url, "(")) {
|
|
140
|
+
url = url.slice(0, -1);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (lastCharacter === "]" && countCharacter(url, "]") > countCharacter(url, "[")) {
|
|
144
|
+
url = url.slice(0, -1);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (lastCharacter === "}" && countCharacter(url, "}") > countCharacter(url, "{")) {
|
|
148
|
+
url = url.slice(0, -1);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!/^https?:\/\//.test(url)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
return new URL(url).toString();
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function findMarkdownLinkUrlEnd(text: string, openParenIndex: number): number | null {
|
|
166
|
+
let depth = 0;
|
|
167
|
+
|
|
168
|
+
for (let index = openParenIndex; index < text.length; index += 1) {
|
|
169
|
+
const character = text[index];
|
|
170
|
+
if (!character || character === "\n") {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
if (character === "(") {
|
|
174
|
+
depth += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (character !== ")") {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
depth -= 1;
|
|
181
|
+
if (depth === 0) {
|
|
182
|
+
return index;
|
|
183
|
+
}
|
|
184
|
+
if (depth < 0) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Extracts citation sources from markdown links and bare URLs in the answer text.
|
|
194
|
+
* Used as a fallback when the Codex response omits `url_citation` annotations.
|
|
195
|
+
*/
|
|
196
|
+
function extractTextSources(text: string): SearchSource[] {
|
|
197
|
+
const sources: SearchSource[] = [];
|
|
198
|
+
|
|
199
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
200
|
+
if (text[index] !== "[") {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const titleEnd = text.indexOf("]", index + 1);
|
|
204
|
+
if (titleEnd === -1 || text[titleEnd + 1] !== "(") {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const urlEnd = findMarkdownLinkUrlEnd(text, titleEnd + 1);
|
|
208
|
+
if (urlEnd === null) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const title = text.slice(index + 1, titleEnd).trim();
|
|
212
|
+
const url = normalizeExtractedUrl(text.slice(titleEnd + 2, urlEnd));
|
|
213
|
+
if (url) {
|
|
214
|
+
addSource(sources, { title: title || url, url });
|
|
215
|
+
}
|
|
216
|
+
index = urlEnd;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const match of text.matchAll(/https?:\/\/\S+/g)) {
|
|
220
|
+
const url = normalizeExtractedUrl(match[0] ?? "");
|
|
221
|
+
if (!url) continue;
|
|
222
|
+
addSource(sources, { title: url, url });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return sources;
|
|
226
|
+
}
|
|
227
|
+
|
|
109
228
|
/**
|
|
110
229
|
* Extracts account ID from a Codex access token.
|
|
111
230
|
* @param accessToken - JWT access token
|
|
@@ -211,6 +330,7 @@ async function callCodexSearch(
|
|
|
211
330
|
search_context_size: options.searchContextSize ?? "high",
|
|
212
331
|
},
|
|
213
332
|
],
|
|
333
|
+
tool_choice: { type: "web_search" },
|
|
214
334
|
instructions: options.systemPrompt ?? DEFAULT_INSTRUCTIONS,
|
|
215
335
|
};
|
|
216
336
|
|
|
@@ -262,12 +382,7 @@ async function callCodexSearch(
|
|
|
262
382
|
for (const annotation of part.annotations) {
|
|
263
383
|
if (annotation.type === "url_citation" && annotation.url) {
|
|
264
384
|
// Deduplicate by URL
|
|
265
|
-
|
|
266
|
-
sources.push({
|
|
267
|
-
title: annotation.title ?? annotation.url,
|
|
268
|
-
url: annotation.url,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
385
|
+
addSource(sources, { title: annotation.title ?? annotation.url, url: annotation.url });
|
|
271
386
|
}
|
|
272
387
|
}
|
|
273
388
|
}
|
|
@@ -317,6 +432,14 @@ async function callCodexSearch(
|
|
|
317
432
|
? streamedAnswer
|
|
318
433
|
: finalAnswer;
|
|
319
434
|
|
|
435
|
+
// Fallback: when Codex omits url_citation annotations, scrape markdown links
|
|
436
|
+
// and bare URLs from the synthesized answer so callers still receive sources.
|
|
437
|
+
if (sources.length === 0 && answer.length > 0) {
|
|
438
|
+
for (const source of extractTextSources(answer)) {
|
|
439
|
+
addSource(sources, source);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
320
443
|
return {
|
|
321
444
|
answer,
|
|
322
445
|
sources,
|