@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.14
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 +63 -0
- package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-za420td8.md} +63 -0
- package/dist/cli.js +3658 -3627
- package/dist/types/advisor/delta-split.d.ts +24 -0
- package/dist/types/advisor/runtime.d.ts +2 -2
- package/dist/types/async/job-manager.d.ts +8 -1
- package/dist/types/cli/update-cli.d.ts +53 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-resolver.d.ts +15 -2
- package/dist/types/config/settings-schema.d.ts +14 -0
- package/dist/types/discovery/agents-md.d.ts +10 -1
- package/dist/types/eval/runner-cache.d.ts +12 -0
- package/dist/types/extensibility/extensions/runner.d.ts +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +15 -4
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +4 -1
- package/dist/types/lib/xai-http.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +8 -5
- package/dist/types/modes/components/agent-hub-renderer.d.ts +6 -1
- package/dist/types/modes/components/status-line/types.d.ts +4 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -4
- package/dist/types/modes/interactive-mode.d.ts +17 -8
- package/dist/types/modes/types.d.ts +7 -10
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +24 -3
- package/dist/types/session/messages.d.ts +20 -0
- package/dist/types/session/retry-fallback-chains.d.ts +13 -0
- package/dist/types/session/session-advisors.d.ts +1 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-tools.d.ts +35 -5
- package/dist/types/session/turn-recovery.d.ts +34 -5
- package/dist/types/slash-commands/types.d.ts +5 -1
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/tools/approval.d.ts +7 -0
- package/dist/types/tools/builtin-names.d.ts +1 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/think.d.ts +38 -0
- package/dist/types/tools/todo.d.ts +14 -15
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/local-date.d.ts +2 -0
- package/dist/types/vibe/runtime.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/brave.d.ts +8 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
- package/dist/types/web/search/providers/jina.d.ts +3 -3
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +4 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
- package/package.json +13 -13
- package/src/advisor/delta-split.ts +98 -0
- package/src/advisor/runtime.ts +321 -69
- package/src/async/job-manager.ts +14 -3
- package/src/cli/gallery-fixtures/agentic.ts +16 -0
- package/src/cli/plugin-cli.ts +30 -2
- package/src/cli/update-cli.ts +259 -24
- package/src/config/keybindings.ts +52 -9
- package/src/config/model-resolver.ts +19 -3
- package/src/config/settings-schema.ts +16 -0
- package/src/cursor.ts +10 -5
- package/src/discovery/agents-md.ts +61 -23
- package/src/eval/jl/kernel.ts +2 -20
- package/src/eval/py/kernel.ts +2 -20
- package/src/eval/rb/kernel.ts +2 -20
- package/src/eval/runner-cache.ts +41 -0
- package/src/exec/non-interactive-env.ts +14 -3
- package/src/extensibility/extensions/loader.ts +5 -2
- package/src/extensibility/extensions/runner.ts +184 -66
- package/src/extensibility/extensions/types.ts +26 -2
- package/src/extensibility/extensions/wrapper.ts +13 -7
- package/src/extensibility/plugins/marketplace/manager.ts +6 -2
- package/src/hindsight/client.ts +1 -1
- package/src/lib/xai-http.ts +0 -4
- package/src/lsp/client.ts +2 -0
- package/src/lsp/servers.ts +1 -1
- package/src/mcp/tool-bridge.ts +15 -6
- package/src/modes/components/agent-hub-renderer.ts +9 -3
- package/src/modes/components/agent-hub.ts +2 -1
- package/src/modes/components/status-line/component.ts +58 -6
- package/src/modes/components/status-line/segments.ts +12 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/user-message.ts +20 -5
- package/src/modes/controllers/event-controller.ts +48 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -7
- package/src/modes/controllers/input-controller.ts +25 -6
- package/src/modes/controllers/selector-controller.ts +5 -0
- package/src/modes/interactive-mode.ts +315 -129
- package/src/modes/rpc/rpc-frame.ts +13 -5
- package/src/modes/theme/tui-adapters.ts +4 -5
- package/src/modes/types.ts +13 -7
- package/src/modes/utils/hotkeys-markdown.ts +10 -6
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/registry/persisted-agents.ts +43 -8
- package/src/sdk.ts +146 -9
- package/src/session/agent-session-types.ts +4 -0
- package/src/session/agent-session.ts +91 -10
- package/src/session/messages.ts +98 -28
- package/src/session/retry-fallback-chains.ts +14 -0
- package/src/session/session-advisors.ts +32 -15
- package/src/session/session-history-format.ts +15 -1
- package/src/session/session-maintenance.ts +8 -8
- package/src/session/session-manager.ts +6 -2
- package/src/session/session-tools.ts +355 -183
- package/src/session/turn-recovery.ts +225 -47
- package/src/slash-commands/builtin-modes.ts +41 -12
- package/src/slash-commands/types.ts +5 -1
- package/src/task/executor.ts +92 -45
- package/src/task/structured-subagent.ts +5 -5
- package/src/tools/approval.ts +44 -10
- package/src/tools/builtin-names.ts +1 -2
- package/src/tools/fetch.ts +21 -2
- package/src/tools/image-gen.ts +6 -8
- package/src/tools/index.ts +8 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/think.ts +62 -0
- package/src/tools/todo.ts +70 -26
- package/src/tools/tts.ts +3 -2
- package/src/tools/write.ts +7 -3
- package/src/utils/local-date.ts +13 -0
- package/src/utils/tools-manager.ts +2 -2
- package/src/vibe/runtime.ts +22 -14
- package/src/web/kagi.ts +91 -34
- package/src/web/parallel.ts +11 -2
- package/src/web/scrapers/crates-io.ts +2 -2
- package/src/web/scrapers/discogs.ts +2 -2
- package/src/web/scrapers/docs-rs.ts +2 -2
- package/src/web/scrapers/github.ts +2 -2
- package/src/web/scrapers/musicbrainz.ts +1 -2
- package/src/web/scrapers/pubmed.ts +2 -2
- package/src/web/scrapers/sec-edgar.ts +2 -2
- package/src/web/search/providers/brave.ts +121 -46
- package/src/web/search/providers/codex.ts +88 -12
- package/src/web/search/providers/exa.ts +45 -10
- package/src/web/search/providers/firecrawl.ts +53 -11
- package/src/web/search/providers/gemini.ts +139 -27
- package/src/web/search/providers/jina.ts +48 -25
- package/src/web/search/providers/parallel.ts +23 -9
- package/src/web/search/providers/perplexity.ts +24 -7
- package/src/web/search/providers/searxng.ts +77 -1
- package/src/web/search/providers/tavily.ts +23 -22
- package/src/web/search/providers/tinyfish.ts +44 -10
- package/src/web/search/providers/xai.ts +85 -14
package/src/tools/todo.ts
CHANGED
|
@@ -236,6 +236,13 @@ export function todoMatchesAnyDescription(content: string, descriptions: readonl
|
|
|
236
236
|
return false;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
/** Whether a todo is settled: completed or deliberately abandoned. Shared so
|
|
240
|
+
* the collapsed viewport, the HUD progress counters, and the HUD's closed-todo
|
|
241
|
+
* auto-clear can never disagree about what "done" hides. */
|
|
242
|
+
export function isClosedTodo<T extends { status: TodoStatus }>(task: T): boolean {
|
|
243
|
+
return task.status === "completed" || task.status === "abandoned";
|
|
244
|
+
}
|
|
245
|
+
|
|
239
246
|
/**
|
|
240
247
|
* A todo the collapsed viewport treats as current work: the literal
|
|
241
248
|
* `in_progress` task or a pending task a live subagent is executing. Both
|
|
@@ -254,36 +261,33 @@ export interface CollapsedTodoSelection<T> {
|
|
|
254
261
|
}
|
|
255
262
|
|
|
256
263
|
/**
|
|
257
|
-
*
|
|
264
|
+
* Closed rows kept directly above the open window so finishing a task is
|
|
265
|
+
* visible as it happens. Without this the collapsed viewport only ever renders
|
|
266
|
+
* unchecked boxes while a phase has open work: every completion silently
|
|
267
|
+
* removes a row, so a plan mid-flight looks untouched, and the card's
|
|
268
|
+
* completion strike animation (`completedTasks` → {@link TODO_STRIKE_TOTAL_FRAMES})
|
|
269
|
+
* animated a row that was never rendered.
|
|
270
|
+
*/
|
|
271
|
+
const COLLAPSED_CLOSED_CONTEXT = 1;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Rows to show for a display base already reduced to the relevant tasks.
|
|
258
275
|
*
|
|
259
|
-
*
|
|
260
|
-
* 1. While the phase has open work, completed/abandoned tasks are omitted. A
|
|
261
|
-
* phase with no open tasks left falls back to its closed tasks so the sticky
|
|
262
|
-
* HUD's closed-todo persistence still has something to render.
|
|
263
|
-
* 2. Every active task (in-progress, or pending matched to a live subagent) is
|
|
276
|
+
* 1. Every active task (in-progress, or pending matched to a live subagent) is
|
|
264
277
|
* placed at the head in stable todo order — never dropped for lying outside
|
|
265
278
|
* an ordinary window.
|
|
266
|
-
*
|
|
279
|
+
* 2. Remaining rows up to `cap` are filled with the pending tasks that follow
|
|
267
280
|
* the first active one, in todo order (falling back to leading pending tasks
|
|
268
281
|
* when no active task exists), so a freshly-promoted task leads the preview.
|
|
269
|
-
*
|
|
282
|
+
* 3. When active tasks alone exceed `cap`, only the first `cap` active tasks are
|
|
270
283
|
* shown and the summary counts the hidden *active* todos, never replacing
|
|
271
284
|
* them with unrelated pending rows.
|
|
272
|
-
*
|
|
273
|
-
* The summary otherwise counts the remaining tasks in the display base. Returns
|
|
274
|
-
* the whole base with an empty summary when it already fits.
|
|
275
285
|
*/
|
|
276
|
-
|
|
277
|
-
|
|
286
|
+
function selectWithinCap<T extends { status: TodoStatus }>(
|
|
287
|
+
base: T[],
|
|
278
288
|
isMatched: (task: T) => boolean,
|
|
279
289
|
cap: number,
|
|
280
290
|
): CollapsedTodoSelection<T> {
|
|
281
|
-
const open = tasks.filter(
|
|
282
|
-
task => task.status === "pending" || task.status === "in_progress" || task.status === "blocked",
|
|
283
|
-
);
|
|
284
|
-
// No open work: fall back to the closed tasks so a settled phase still
|
|
285
|
-
// renders (HUD closed-todo persistence). Closed tasks are never active.
|
|
286
|
-
const base = open.length > 0 ? open : tasks;
|
|
287
291
|
if (base.length <= cap) return { items: base, summary: "" };
|
|
288
292
|
|
|
289
293
|
const active = base.filter(task => isActiveTodo(task, isMatched));
|
|
@@ -312,6 +316,33 @@ export function selectCollapsedTodos<T extends { status: TodoStatus }>(
|
|
|
312
316
|
return { items, summary: hidden > 0 ? formatMoreItems(hidden, "todo") : "" };
|
|
313
317
|
}
|
|
314
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Walking-viewport selection for a phase's collapsed todo preview (#5873).
|
|
321
|
+
*
|
|
322
|
+
* Applied to `tasks` in todo order: the open tasks run through
|
|
323
|
+
* {@link selectWithinCap}, led by the last {@link COLLAPSED_CLOSED_CONTEXT}
|
|
324
|
+
* closed tasks in todo order so a checked row remains visible even when callers
|
|
325
|
+
* complete work out of sequence. The lead is additive — it never costs an open
|
|
326
|
+
* row — and a phase with no open work left falls back to its closed tasks so the
|
|
327
|
+
* sticky HUD's closed-todo persistence still has something to render.
|
|
328
|
+
*
|
|
329
|
+
* `summary` counts the open tasks that did not fit; the closed lead is context,
|
|
330
|
+
* not part of the budget.
|
|
331
|
+
*/
|
|
332
|
+
export function selectCollapsedTodos<T extends { status: TodoStatus }>(
|
|
333
|
+
tasks: T[],
|
|
334
|
+
isMatched: (task: T) => boolean,
|
|
335
|
+
cap: number,
|
|
336
|
+
): CollapsedTodoSelection<T> {
|
|
337
|
+
const open = tasks.filter(task => !isClosedTodo(task));
|
|
338
|
+
// Closed tasks are never active, so a settled phase selects over itself.
|
|
339
|
+
if (open.length === 0) return selectWithinCap(tasks, isMatched, cap);
|
|
340
|
+
// `done` accepts any named task, so closed tasks are not necessarily a prefix.
|
|
341
|
+
const lead = tasks.filter(isClosedTodo).slice(-COLLAPSED_CLOSED_CONTEXT);
|
|
342
|
+
const selected = selectWithinCap(open, isMatched, cap);
|
|
343
|
+
return { items: [...lead, ...selected.items], summary: selected.summary };
|
|
344
|
+
}
|
|
345
|
+
|
|
315
346
|
function resolveTaskOrError(
|
|
316
347
|
phases: TodoPhase[],
|
|
317
348
|
content: string | undefined,
|
|
@@ -1055,12 +1086,20 @@ function computeTouchedPhases(
|
|
|
1055
1086
|
return touched.size > 0 ? touched : null;
|
|
1056
1087
|
}
|
|
1057
1088
|
|
|
1089
|
+
/**
|
|
1090
|
+
* Dim `closed/total` suffix for a phase header. Counts closed tasks, not just
|
|
1091
|
+
* completed ones: the collapsed viewport hides both, so an abandoned task has to
|
|
1092
|
+
* move the counter or its phase reads as permanently stuck.
|
|
1093
|
+
*/
|
|
1094
|
+
function formatPhaseProgress(phase: TodoPhase, uiTheme: Theme): string {
|
|
1095
|
+
const done = phase.tasks.filter(isClosedTodo).length;
|
|
1096
|
+
return uiTheme.fg("dim", ` ${done}/${phase.tasks.length}`);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1058
1099
|
/** One-line summary for a collapsed (untouched) phase: dim header + progress. */
|
|
1059
1100
|
function formatPhaseSummary(phase: TodoPhase, oneBasedIndex: number, uiTheme: Theme): string {
|
|
1060
|
-
const total = phase.tasks.length;
|
|
1061
|
-
const done = phase.tasks.filter(task => task.status === "completed").length;
|
|
1062
1101
|
const name = uiTheme.fg("dim", chalk.bold(formatPhaseDisplayName(phase.name, oneBasedIndex)));
|
|
1063
|
-
return `${name}${
|
|
1102
|
+
return `${name}${formatPhaseProgress(phase, uiTheme)}`;
|
|
1064
1103
|
}
|
|
1065
1104
|
|
|
1066
1105
|
/**
|
|
@@ -1178,12 +1217,17 @@ export const todoToolRenderer = {
|
|
|
1178
1217
|
continue;
|
|
1179
1218
|
}
|
|
1180
1219
|
if (multiPhase) {
|
|
1181
|
-
|
|
1220
|
+
// Progress belongs on the expanded header too: the collapsed
|
|
1221
|
+
// viewport below hides closed rows, so without it the phase the
|
|
1222
|
+
// agent is actually working in is the one phase with no visible
|
|
1223
|
+
// completion signal at all.
|
|
1224
|
+
const name = uiTheme.fg("accent", chalk.bold(formatPhaseDisplayName(phase.name, p + 1)));
|
|
1225
|
+
bodyLines.push(`${name}${formatPhaseProgress(phase, uiTheme)}`);
|
|
1182
1226
|
}
|
|
1183
1227
|
const completionKeys = completionKeysByPhase.get(phase.name) ?? EMPTY_COMPLETION_KEYS;
|
|
1184
|
-
// Collapsed: walking viewport —
|
|
1185
|
-
// work (in-progress / subagent-matched)
|
|
1186
|
-
//
|
|
1228
|
+
// Collapsed: walking viewport — the last closed task leads, then
|
|
1229
|
+
// active work (in-progress / subagent-matched), then following
|
|
1230
|
+
// pending tasks (#5873). Expanded: every task in order.
|
|
1187
1231
|
const treeLines = expanded
|
|
1188
1232
|
? renderTreeList(
|
|
1189
1233
|
{
|
package/src/tools/tts.ts
CHANGED
|
@@ -7,9 +7,10 @@ import { type } from "@oh-my-pi/omptype";
|
|
|
7
7
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
8
8
|
import { type ApiKey, withAuth } from "@oh-my-pi/pi-ai";
|
|
9
9
|
import { ProviderHttpError } from "@oh-my-pi/pi-ai/error";
|
|
10
|
+
import { USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
10
11
|
import { settings } from "../config/settings";
|
|
11
12
|
import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
|
|
12
|
-
import {
|
|
13
|
+
import { resolveXAIHttpCredentials } from "../lib/xai-http";
|
|
13
14
|
import { DEFAULT_TTS_LOCAL_MODEL_KEY, DEFAULT_TTS_VOICE, isTtsLocalModelKey, KOKORO_VOICES } from "../tts/models";
|
|
14
15
|
import { ttsClient } from "../tts/tts-client";
|
|
15
16
|
import { encodeWav } from "../tts/wav";
|
|
@@ -150,7 +151,7 @@ async function synthesizeXai(
|
|
|
150
151
|
headers: {
|
|
151
152
|
Authorization: `Bearer ${key}`,
|
|
152
153
|
"Content-Type": "application/json",
|
|
153
|
-
"User-Agent":
|
|
154
|
+
"User-Agent": USER_AGENT,
|
|
154
155
|
},
|
|
155
156
|
body: JSON.stringify(payload),
|
|
156
157
|
signal: combinedSignal,
|
package/src/tools/write.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
AgentToolContext,
|
|
10
10
|
AgentToolResult,
|
|
11
11
|
AgentToolUpdateCallback,
|
|
12
|
-
|
|
12
|
+
ToolApprovalDecision,
|
|
13
13
|
} from "@oh-my-pi/pi-agent-core";
|
|
14
14
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
15
15
|
import { isEnoent, isRecord, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -500,7 +500,7 @@ function parseSqliteWriteTarget(subPath: string, queryString: string): { table:
|
|
|
500
500
|
*/
|
|
501
501
|
export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails> {
|
|
502
502
|
readonly name = "write";
|
|
503
|
-
readonly approval = (args: unknown):
|
|
503
|
+
readonly approval = (args: unknown): ToolApprovalDecision => {
|
|
504
504
|
const rawPath = (args as Partial<WriteParams>).path;
|
|
505
505
|
if (typeof rawPath !== "string") return "write";
|
|
506
506
|
// Unwrap a hashline `[path#TAG]` wrapper first (parity with execute) so a
|
|
@@ -532,7 +532,11 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
532
532
|
}
|
|
533
533
|
if (!isRecord(parsed)) return "exec";
|
|
534
534
|
try {
|
|
535
|
-
|
|
535
|
+
// The tier is the mounted tool's own (argument-dependent) approval; the
|
|
536
|
+
// policyKey makes the outer gate consult `tools.approval.<device>` for
|
|
537
|
+
// this dispatch before falling back to `tools.approval.write`, so users
|
|
538
|
+
// can scope allow/deny/prompt to a single device (issue #7923).
|
|
539
|
+
return { tier: resolveToolTier(inst, parsed), policyKey: xdevTarget.name! };
|
|
536
540
|
} catch {
|
|
537
541
|
return "exec";
|
|
538
542
|
}
|
package/src/utils/local-date.ts
CHANGED
|
@@ -5,3 +5,16 @@ export function formatLocalCalendarDate(date: Date = new Date()): string {
|
|
|
5
5
|
const day = String(date.getDate()).padStart(2, "0");
|
|
6
6
|
return `${year}-${month}-${day}`;
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
/** Format a local date and minute with a compact numeric UTC offset. */
|
|
10
|
+
export function formatLocalDateTimeWithOffset(date: Date): string {
|
|
11
|
+
const offsetMinutes = date.getTimezoneOffset();
|
|
12
|
+
const offsetSign = offsetMinutes <= 0 ? "+" : "-";
|
|
13
|
+
const absoluteOffset = Math.abs(offsetMinutes);
|
|
14
|
+
const offsetHours = Math.floor(absoluteOffset / 60);
|
|
15
|
+
const offsetRemainderMinutes = absoluteOffset % 60;
|
|
16
|
+
const pad2 = (value: number): string => String(value).padStart(2, "0");
|
|
17
|
+
return `${formatLocalCalendarDate(date)} ${pad2(date.getHours())}:${pad2(date.getMinutes())} ${offsetSign}${pad2(
|
|
18
|
+
offsetHours,
|
|
19
|
+
)}:${pad2(offsetRemainderMinutes)}`;
|
|
20
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { $which,
|
|
4
|
+
import { $which, getToolsDir, logger, ptree, TempDir, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { extractArchive } from "./zip";
|
|
6
6
|
|
|
7
7
|
const TOOLS_DIR = getToolsDir();
|
|
@@ -174,7 +174,7 @@ async function getLatestVersion(repo: string, signal?: AbortSignal): Promise<str
|
|
|
174
174
|
let response: Response;
|
|
175
175
|
try {
|
|
176
176
|
response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
|
|
177
|
-
headers: { "User-Agent":
|
|
177
|
+
headers: { "User-Agent": USER_AGENT },
|
|
178
178
|
signal: ptree.combineSignals(signal, TOOL_METADATA_TIMEOUT_MS),
|
|
179
179
|
});
|
|
180
180
|
} catch (err) {
|
package/src/vibe/runtime.ts
CHANGED
|
@@ -18,7 +18,7 @@ import * as os from "node:os";
|
|
|
18
18
|
import * as path from "node:path";
|
|
19
19
|
import { logger, prompt, Snowflake } from "@oh-my-pi/pi-utils";
|
|
20
20
|
import type { AsyncJob, AsyncJobManager } from "../async/job-manager";
|
|
21
|
-
import {
|
|
21
|
+
import { resolveAgentModelSelection } from "../config/model-resolver";
|
|
22
22
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
23
23
|
import { registerArtifactsDir } from "../internal-urls/registry-helpers";
|
|
24
24
|
import { MCPManager } from "../mcp/manager";
|
|
@@ -43,7 +43,7 @@ export type VibeCli = "fast" | "good";
|
|
|
43
43
|
* CLI flavor → bundled agent type. This IS the model-tier mapping: `sonic`
|
|
44
44
|
* carries `model: "@smol"` (the configured fast/low-latency role) and `task`
|
|
45
45
|
* carries `model: "@task"` (inherits the session's strong model).
|
|
46
|
-
* Resolution goes through {@link
|
|
46
|
+
* Resolution goes through {@link resolveAgentModelSelection} exactly like a
|
|
47
47
|
* `task` spawn, so `task.agentModelOverrides` and model-role settings apply.
|
|
48
48
|
*/
|
|
49
49
|
export const VIBE_CLI_AGENT: Record<VibeCli, string> = {
|
|
@@ -144,6 +144,8 @@ interface VibeRestoreCandidate {
|
|
|
144
144
|
interface ResolvedVibeWorker {
|
|
145
145
|
agent: AgentDefinition;
|
|
146
146
|
modelOverride?: string | string[];
|
|
147
|
+
/** Pre-expansion role alias behind {@link modelOverride}, when the worker agent named one. */
|
|
148
|
+
modelRole?: string;
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
interface VibeTurn {
|
|
@@ -165,6 +167,8 @@ interface VibeRecord {
|
|
|
165
167
|
childSessionFile?: string;
|
|
166
168
|
agent: AgentDefinition;
|
|
167
169
|
modelOverride?: string | string[];
|
|
170
|
+
/** Pre-expansion role alias behind {@link modelOverride}, when the worker agent named one. */
|
|
171
|
+
modelRole?: string;
|
|
168
172
|
state: VibeSessionState;
|
|
169
173
|
createdAt: number;
|
|
170
174
|
lastActivityAt: number;
|
|
@@ -490,16 +494,17 @@ export class VibeSessionRegistry {
|
|
|
490
494
|
throw new ToolError(`Bundled agent "${agentName}" for vibe cli "${cli}" is unavailable.`);
|
|
491
495
|
}
|
|
492
496
|
const agentModelOverrides = session.settings.get("task.agentModelOverrides");
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
};
|
|
497
|
+
// Same contract as the task spawn path: the expansion discards the role
|
|
498
|
+
// alias (`@task`, `@smol`), so patterns and role identity come from one
|
|
499
|
+
// call — the child's inherited retry-fallback chain is keyed off the role.
|
|
500
|
+
const { patterns, role } = resolveAgentModelSelection({
|
|
501
|
+
settingsOverride: agentModelOverrides[agentName],
|
|
502
|
+
agentModel: agent.model,
|
|
503
|
+
settings: session.settings,
|
|
504
|
+
activeModelPattern: session.getActiveModelString?.(),
|
|
505
|
+
fallbackModelPattern: session.getModelString?.(),
|
|
506
|
+
});
|
|
507
|
+
return { agent, modelOverride: patterns, modelRole: role };
|
|
503
508
|
}
|
|
504
509
|
|
|
505
510
|
async #appendLifecycleEvent(
|
|
@@ -890,7 +895,7 @@ export class VibeSessionRegistry {
|
|
|
890
895
|
existing.sessionFile === childSessionFile &&
|
|
891
896
|
(existing.status === "idle" || existing.status === "parked");
|
|
892
897
|
const blockedByCollision = Boolean(existing && !existingIsResumable);
|
|
893
|
-
const { agent, modelOverride } = this.#resolveWorker(session, spawn.cli);
|
|
898
|
+
const { agent, modelOverride, modelRole } = this.#resolveWorker(session, spawn.cli);
|
|
894
899
|
if (!existing) {
|
|
895
900
|
AgentRegistry.global().register({
|
|
896
901
|
id: spawn.id,
|
|
@@ -911,6 +916,7 @@ export class VibeSessionRegistry {
|
|
|
911
916
|
childSessionFile,
|
|
912
917
|
agent,
|
|
913
918
|
modelOverride,
|
|
919
|
+
modelRole,
|
|
914
920
|
state: "idle",
|
|
915
921
|
createdAt: spawn.createdAt,
|
|
916
922
|
lastActivityAt: candidate.lastActivityAt,
|
|
@@ -945,7 +951,7 @@ export class VibeSessionRegistry {
|
|
|
945
951
|
throw new ToolError("Vibe mode has exited; enter Vibe mode again before spawning a worker.");
|
|
946
952
|
}
|
|
947
953
|
const manager = this.#manager(session);
|
|
948
|
-
const { agent, modelOverride } = this.#resolveWorker(session, args.cli);
|
|
954
|
+
const { agent, modelOverride, modelRole } = this.#resolveWorker(session, args.cli);
|
|
949
955
|
if (!session.agentOutputManager) {
|
|
950
956
|
session.agentOutputManager = new AgentOutputManager(session.getArtifactsDir ?? (() => null));
|
|
951
957
|
}
|
|
@@ -969,6 +975,7 @@ export class VibeSessionRegistry {
|
|
|
969
975
|
childSessionFile,
|
|
970
976
|
agent,
|
|
971
977
|
modelOverride,
|
|
978
|
+
modelRole,
|
|
972
979
|
state: "starting",
|
|
973
980
|
createdAt,
|
|
974
981
|
lastActivityAt: createdAt,
|
|
@@ -1418,6 +1425,7 @@ export class VibeSessionRegistry {
|
|
|
1418
1425
|
taskDepth: session.taskDepth ?? 0,
|
|
1419
1426
|
detached: true,
|
|
1420
1427
|
modelOverride: record.modelOverride,
|
|
1428
|
+
modelRole: record.modelRole,
|
|
1421
1429
|
parentActiveModelPattern: session.getActiveModelString?.(),
|
|
1422
1430
|
thinkingLevel: record.agent.thinkingLevel,
|
|
1423
1431
|
sessionFile,
|
package/src/web/kagi.ts
CHANGED
|
@@ -98,7 +98,7 @@ export class KagiApiError extends Error {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
function extractKagiErrorMessage(payload: unknown): string | null {
|
|
101
|
-
if (!payload || typeof payload !== "object") return null;
|
|
101
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
|
102
102
|
const record = payload as Record<string, unknown>;
|
|
103
103
|
|
|
104
104
|
for (const value of [record.message, record.detail]) {
|
|
@@ -107,17 +107,20 @@ function extractKagiErrorMessage(payload: unknown): string | null {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for (const entry of
|
|
110
|
+
for (const errors of [record.error, record.errors]) {
|
|
111
|
+
if (typeof errors === "string" && errors.trim().length > 0) {
|
|
112
|
+
return errors.trim();
|
|
113
|
+
}
|
|
114
|
+
if (!Array.isArray(errors)) continue;
|
|
115
|
+
for (const entry of errors) {
|
|
116
116
|
if (!entry || typeof entry !== "object") continue;
|
|
117
117
|
const e = entry as Record<string, unknown>;
|
|
118
|
-
for (const value of [e.message, e.msg]) {
|
|
119
|
-
if (
|
|
120
|
-
|
|
118
|
+
for (const value of [e.message, e.msg, e.code]) {
|
|
119
|
+
if (
|
|
120
|
+
(typeof value === "string" && value.trim().length > 0) ||
|
|
121
|
+
(typeof value === "number" && Number.isFinite(value))
|
|
122
|
+
) {
|
|
123
|
+
return String(value).trim();
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
}
|
|
@@ -147,6 +150,34 @@ function parseKagiErrorResponse(statusCode: number, responseText: string): KagiA
|
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
function parseKagiSuccessResponse(statusCode: number, responseText: string): KagiSearchResponse {
|
|
154
|
+
let payload: unknown;
|
|
155
|
+
try {
|
|
156
|
+
payload = JSON.parse(responseText);
|
|
157
|
+
} catch {
|
|
158
|
+
throw new KagiApiError("Kagi API returned an invalid response: invalid JSON", statusCode);
|
|
159
|
+
}
|
|
160
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
161
|
+
throw new KagiApiError("Kagi API returned an invalid response: expected an object envelope", statusCode);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const record = payload as Record<string, unknown>;
|
|
165
|
+
const errorMessage = extractKagiErrorMessage(payload);
|
|
166
|
+
if (errorMessage && (record.error !== undefined || record.errors !== undefined)) {
|
|
167
|
+
const errors = Array.isArray(record.error) ? record.error : Array.isArray(record.errors) ? record.errors : [];
|
|
168
|
+
const first = errors[0];
|
|
169
|
+
const code =
|
|
170
|
+
first && typeof first === "object" && typeof (first as Record<string, unknown>).code === "number"
|
|
171
|
+
? ((first as Record<string, unknown>).code as number)
|
|
172
|
+
: statusCode;
|
|
173
|
+
throw createKagiApiError(code, errorMessage);
|
|
174
|
+
}
|
|
175
|
+
if (record.data !== undefined && (!record.data || typeof record.data !== "object" || Array.isArray(record.data))) {
|
|
176
|
+
throw new KagiApiError("Kagi API returned an invalid response: expected data to be an object", statusCode);
|
|
177
|
+
}
|
|
178
|
+
return payload as KagiSearchResponse;
|
|
179
|
+
}
|
|
180
|
+
|
|
150
181
|
// ---------------------------------------------------------------------------
|
|
151
182
|
// Public API
|
|
152
183
|
// ---------------------------------------------------------------------------
|
|
@@ -216,23 +247,40 @@ function buildRequestBody(query: string, options: KagiSearchOptions): KagiSearch
|
|
|
216
247
|
return req;
|
|
217
248
|
}
|
|
218
249
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
250
|
+
function firstNonEmptyString(...values: unknown[]): string | undefined {
|
|
251
|
+
for (const value of values) {
|
|
252
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Push every valid item in a result bucket as a source, with an optional title tag. */
|
|
258
|
+
function collectSources(sources: KagiSearchSource[], items: unknown, tag?: string): void {
|
|
259
|
+
if (!Array.isArray(items)) return;
|
|
260
|
+
for (const value of items) {
|
|
261
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
262
|
+
const item = value as Record<string, unknown>;
|
|
263
|
+
const url = firstNonEmptyString(item.url, item.href, item.link);
|
|
264
|
+
if (!url) continue;
|
|
265
|
+
const title = firstNonEmptyString(item.title, item.name) ?? url;
|
|
223
266
|
sources.push({
|
|
224
|
-
title: tag ? `${tag} ${
|
|
225
|
-
url
|
|
226
|
-
snippet: item.snippet,
|
|
227
|
-
publishedDate: item.time,
|
|
267
|
+
title: tag ? `${tag} ${title}` : title,
|
|
268
|
+
url,
|
|
269
|
+
snippet: firstNonEmptyString(item.snippet, item.description, item.summary),
|
|
270
|
+
publishedDate: firstNonEmptyString(item.time),
|
|
228
271
|
});
|
|
229
272
|
}
|
|
230
273
|
}
|
|
231
274
|
|
|
232
275
|
/** Pull a related/adjacent question from an item's props or fall back to title. */
|
|
233
|
-
function questionOf(
|
|
234
|
-
|
|
235
|
-
|
|
276
|
+
function questionOf(value: unknown): string | undefined {
|
|
277
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
278
|
+
const item = value as Record<string, unknown>;
|
|
279
|
+
const props =
|
|
280
|
+
item.props && typeof item.props === "object" && !Array.isArray(item.props)
|
|
281
|
+
? (item.props as Record<string, unknown>)
|
|
282
|
+
: undefined;
|
|
283
|
+
return firstNonEmptyString(props?.question, props?.query, item.title);
|
|
236
284
|
}
|
|
237
285
|
|
|
238
286
|
export async function searchWithKagi(
|
|
@@ -269,11 +317,7 @@ export async function searchWithKagi(
|
|
|
269
317
|
},
|
|
270
318
|
);
|
|
271
319
|
|
|
272
|
-
const payload = (await response.
|
|
273
|
-
if (payload.error && payload.error.length > 0) {
|
|
274
|
-
const first = payload.error[0];
|
|
275
|
-
throw createKagiApiError(first.code ?? response.status, extractKagiErrorMessage(payload) ?? first.message);
|
|
276
|
-
}
|
|
320
|
+
const payload = parseKagiSuccessResponse(response.status, await response.text());
|
|
277
321
|
|
|
278
322
|
const data = payload.data;
|
|
279
323
|
const sources: KagiSearchSource[] = [];
|
|
@@ -284,17 +328,30 @@ export async function searchWithKagi(
|
|
|
284
328
|
collectSources(sources, data?.news, "[News]");
|
|
285
329
|
collectSources(sources, data?.infobox, "[Info]");
|
|
286
330
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
331
|
+
const adjacentQuestions: unknown = data?.adjacent_question;
|
|
332
|
+
if (Array.isArray(adjacentQuestions)) {
|
|
333
|
+
for (const item of adjacentQuestions) {
|
|
334
|
+
const question = questionOf(item);
|
|
335
|
+
if (question) relatedQuestions.push(question);
|
|
336
|
+
}
|
|
290
337
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
338
|
+
const relatedSearches: unknown = data?.related_search;
|
|
339
|
+
if (Array.isArray(relatedSearches)) {
|
|
340
|
+
for (const item of relatedSearches) {
|
|
341
|
+
const question = questionOf(item);
|
|
342
|
+
if (question) relatedQuestions.push(question);
|
|
343
|
+
}
|
|
294
344
|
}
|
|
295
345
|
|
|
296
|
-
const
|
|
297
|
-
const
|
|
346
|
+
const directAnswers: unknown = data?.direct_answer;
|
|
347
|
+
const directAnswer = Array.isArray(directAnswers) ? directAnswers[0] : undefined;
|
|
348
|
+
const answer =
|
|
349
|
+
directAnswer && typeof directAnswer === "object" && !Array.isArray(directAnswer)
|
|
350
|
+
? firstNonEmptyString(
|
|
351
|
+
(directAnswer as Record<string, unknown>).snippet,
|
|
352
|
+
(directAnswer as Record<string, unknown>).title,
|
|
353
|
+
)
|
|
354
|
+
: undefined;
|
|
298
355
|
|
|
299
356
|
return {
|
|
300
357
|
requestId: payload.meta?.trace ?? payload.meta?.id ?? "",
|
package/src/web/parallel.ts
CHANGED
|
@@ -146,6 +146,15 @@ export function parseParallelErrorResponse(statusCode: number, responseText: str
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
export async function parseParallelJsonResponse(response: Response, operation: "search" | "extract"): Promise<unknown> {
|
|
150
|
+
try {
|
|
151
|
+
return await response.json();
|
|
152
|
+
} catch (err) {
|
|
153
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
154
|
+
throw new ParallelApiError(`Parallel ${operation} returned invalid JSON: ${detail}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
149
158
|
function getAuthHeaders(apiKey: string): {
|
|
150
159
|
Accept: string;
|
|
151
160
|
"Content-Type": string;
|
|
@@ -316,7 +325,7 @@ export async function searchWithParallel(
|
|
|
316
325
|
throw parseParallelErrorResponse(response.status, await response.text());
|
|
317
326
|
}
|
|
318
327
|
|
|
319
|
-
const payload
|
|
328
|
+
const payload = await parseParallelJsonResponse(response, "search");
|
|
320
329
|
return parseParallelSearchPayload(payload);
|
|
321
330
|
}
|
|
322
331
|
|
|
@@ -349,6 +358,6 @@ export async function extractWithParallel(
|
|
|
349
358
|
throw parseParallelErrorResponse(response.status, await response.text());
|
|
350
359
|
}
|
|
351
360
|
|
|
352
|
-
const payload
|
|
361
|
+
const payload = await parseParallelJsonResponse(response, "extract");
|
|
353
362
|
return parseExtractPayload(payload);
|
|
354
363
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { tryParseJson } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { tryParseJson, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
3
|
import { buildResult, formatNumber, loadPage, looksLikeHtml } from "./types";
|
|
4
4
|
|
|
@@ -26,7 +26,7 @@ export const handleCratesIo: SpecialHandler = async (
|
|
|
26
26
|
const result = await loadPage(apiUrl, {
|
|
27
27
|
timeout,
|
|
28
28
|
signal,
|
|
29
|
-
headers: { "User-Agent":
|
|
29
|
+
headers: { "User-Agent": USER_AGENT },
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
if (!result.ok) return null;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* API docs: https://www.discogs.com/developers
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { tryParseJson } from "@oh-my-pi/pi-utils";
|
|
8
|
+
import { tryParseJson, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
10
10
|
import { buildResult, loadPage } from "./types";
|
|
11
11
|
|
|
@@ -277,7 +277,7 @@ export const handleDiscogs: SpecialHandler = async (
|
|
|
277
277
|
signal,
|
|
278
278
|
headers: {
|
|
279
279
|
Accept: "application/json",
|
|
280
|
-
"User-Agent":
|
|
280
|
+
"User-Agent": USER_AGENT,
|
|
281
281
|
},
|
|
282
282
|
});
|
|
283
283
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { gunzipSync } from "node:zlib";
|
|
4
|
-
import { getDocsRsCacheDir, isEnoent, logger, ptree, tryParseJson } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { getDocsRsCacheDir, isEnoent, logger, ptree, tryParseJson, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { ToolAbortError } from "../../tools/tool-errors";
|
|
6
6
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
7
7
|
import { buildResult, MAX_BYTES } from "./types";
|
|
@@ -388,7 +388,7 @@ export const handleDocsRs: SpecialHandler = async (
|
|
|
388
388
|
const requestSignal = ptree.combineSignals(signal, timeout * 1000);
|
|
389
389
|
const response = await fetch(jsonUrl, {
|
|
390
390
|
signal: requestSignal,
|
|
391
|
-
headers: { "User-Agent":
|
|
391
|
+
headers: { "User-Agent": USER_AGENT, Accept: "application/gzip" },
|
|
392
392
|
redirect: "follow",
|
|
393
393
|
});
|
|
394
394
|
if (!response.ok) return null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $env, ptree } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { $env, ptree, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
3
|
import { buildResult, formatMediaDuration, loadPage } from "./types";
|
|
4
4
|
|
|
@@ -121,7 +121,7 @@ export async function fetchGitHubApi(
|
|
|
121
121
|
|
|
122
122
|
const headers: Record<string, string> = {
|
|
123
123
|
Accept: "application/vnd.github.v3+json",
|
|
124
|
-
"User-Agent":
|
|
124
|
+
"User-Agent": USER_AGENT,
|
|
125
125
|
};
|
|
126
126
|
|
|
127
127
|
// Use GITHUB_TOKEN if available
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* MusicBrainz URL handler for artists, releases, and recordings
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { tryParseJson } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { tryParseJson, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
7
7
|
import { buildResult, formatMediaDuration, loadPage } from "./types";
|
|
8
8
|
|
|
@@ -64,7 +64,6 @@ interface MusicBrainzRelease {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const MUSICBRAINZ_HOSTS = new Set(["musicbrainz.org", "www.musicbrainz.org"]);
|
|
67
|
-
const USER_AGENT = "omp-web-fetch/1.0 (https://github.com/anthropics)";
|
|
68
67
|
const MAX_TRACKS = 50;
|
|
69
68
|
|
|
70
69
|
function parseEntity(url: URL): { entity: MusicBrainzEntity; mbid: string } | null {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PubMed handler for web-fetch
|
|
3
3
|
*/
|
|
4
|
-
import { tryParseJson } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { tryParseJson, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { buildResult, loadPage, type RenderResult, type SpecialHandler } from "./types";
|
|
6
6
|
|
|
7
7
|
const NCBI_HEADERS = {
|
|
8
8
|
Accept: "application/json, text/plain;q=0.9, */*;q=0.8",
|
|
9
|
-
"User-Agent":
|
|
9
|
+
"User-Agent": USER_AGENT,
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
/**
|