@oh-my-pi/pi-coding-agent 3.15.0 → 3.20.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 +61 -1
- package/docs/extensions.md +1055 -0
- package/docs/rpc.md +69 -13
- package/docs/session-tree-plan.md +1 -1
- package/examples/extensions/README.md +141 -0
- package/examples/extensions/api-demo.ts +87 -0
- package/examples/extensions/chalk-logger.ts +26 -0
- package/examples/extensions/hello.ts +33 -0
- package/examples/extensions/pirate.ts +44 -0
- package/examples/extensions/plan-mode.ts +551 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/todo.ts +299 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +16 -0
- package/examples/sdk/02-custom-model.ts +3 -3
- package/examples/sdk/05-tools.ts +7 -3
- package/examples/sdk/06-extensions.ts +81 -0
- package/examples/sdk/06-hooks.ts +14 -13
- package/examples/sdk/08-prompt-templates.ts +42 -0
- package/examples/sdk/08-slash-commands.ts +17 -12
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/12-full-control.ts +6 -6
- package/package.json +11 -7
- package/src/capability/extension-module.ts +34 -0
- package/src/cli/args.ts +22 -7
- package/src/cli/file-processor.ts +38 -67
- package/src/cli/list-models.ts +1 -1
- package/src/config.ts +25 -14
- package/src/core/agent-session.ts +505 -242
- package/src/core/auth-storage.ts +33 -21
- package/src/core/compaction/branch-summarization.ts +4 -4
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/custom-commands/bundled/wt/index.ts +430 -0
- package/src/core/custom-commands/loader.ts +9 -0
- package/src/core/custom-tools/wrapper.ts +5 -0
- package/src/core/event-bus.ts +59 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/extensions/index.ts +100 -0
- package/src/core/extensions/loader.ts +501 -0
- package/src/core/extensions/runner.ts +477 -0
- package/src/core/extensions/types.ts +712 -0
- package/src/core/extensions/wrapper.ts +147 -0
- package/src/core/hooks/types.ts +2 -2
- package/src/core/index.ts +10 -21
- package/src/core/keybindings.ts +199 -0
- package/src/core/messages.ts +26 -7
- package/src/core/model-registry.ts +123 -46
- package/src/core/model-resolver.ts +7 -5
- package/src/core/prompt-templates.ts +242 -0
- package/src/core/sdk.ts +378 -295
- package/src/core/session-manager.ts +72 -58
- package/src/core/settings-manager.ts +118 -22
- package/src/core/system-prompt.ts +24 -1
- package/src/core/terminal-notify.ts +37 -0
- package/src/core/tools/context.ts +4 -4
- package/src/core/tools/exa/mcp-client.ts +5 -4
- package/src/core/tools/exa/render.ts +176 -131
- package/src/core/tools/gemini-image.ts +361 -0
- package/src/core/tools/git.ts +216 -0
- package/src/core/tools/index.ts +28 -15
- package/src/core/tools/lsp/config.ts +5 -4
- package/src/core/tools/lsp/index.ts +17 -12
- package/src/core/tools/lsp/render.ts +39 -47
- package/src/core/tools/read.ts +66 -29
- package/src/core/tools/render-utils.ts +268 -0
- package/src/core/tools/renderers.ts +243 -225
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +66 -58
- package/src/core/tools/task/index.ts +29 -10
- package/src/core/tools/task/model-resolver.ts +8 -13
- package/src/core/tools/task/omp-command.ts +24 -0
- package/src/core/tools/task/render.ts +35 -60
- package/src/core/tools/task/types.ts +3 -0
- package/src/core/tools/web-fetch.ts +29 -28
- package/src/core/tools/web-search/index.ts +6 -5
- package/src/core/tools/web-search/providers/exa.ts +6 -5
- package/src/core/tools/web-search/render.ts +66 -111
- package/src/core/voice-controller.ts +135 -0
- package/src/core/voice-supervisor.ts +1003 -0
- package/src/core/voice.ts +308 -0
- package/src/discovery/builtin.ts +75 -1
- package/src/discovery/claude.ts +47 -1
- package/src/discovery/codex.ts +54 -2
- package/src/discovery/gemini.ts +55 -2
- package/src/discovery/helpers.ts +100 -1
- package/src/discovery/index.ts +2 -0
- package/src/index.ts +14 -9
- package/src/lib/worktree/collapse.ts +179 -0
- package/src/lib/worktree/constants.ts +14 -0
- package/src/lib/worktree/errors.ts +23 -0
- package/src/lib/worktree/git.ts +110 -0
- package/src/lib/worktree/index.ts +23 -0
- package/src/lib/worktree/operations.ts +216 -0
- package/src/lib/worktree/session.ts +114 -0
- package/src/lib/worktree/stats.ts +67 -0
- package/src/main.ts +61 -37
- package/src/migrations.ts +37 -7
- package/src/modes/interactive/components/bash-execution.ts +6 -4
- package/src/modes/interactive/components/custom-editor.ts +55 -0
- package/src/modes/interactive/components/custom-message.ts +95 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/extensions/types.ts +1 -0
- package/src/modes/interactive/components/footer.ts +324 -0
- package/src/modes/interactive/components/hook-editor.ts +1 -0
- package/src/modes/interactive/components/hook-selector.ts +3 -3
- package/src/modes/interactive/components/model-selector.ts +7 -6
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/settings-defs.ts +55 -6
- package/src/modes/interactive/components/status-line/separators.ts +4 -4
- package/src/modes/interactive/components/status-line.ts +45 -35
- package/src/modes/interactive/components/tool-execution.ts +95 -23
- package/src/modes/interactive/interactive-mode.ts +644 -113
- package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
- package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
- package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
- package/src/modes/interactive/theme/defaults/basalt.json +90 -0
- package/src/modes/interactive/theme/defaults/birch.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
- package/src/modes/interactive/theme/defaults/graphite.json +99 -0
- package/src/modes/interactive/theme/defaults/index.ts +128 -0
- package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
- package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
- package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
- package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
- package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
- package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
- package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
- package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
- package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
- package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
- package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
- package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
- package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
- package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
- package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
- package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
- package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
- package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
- package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
- package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
- package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
- package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
- package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
- package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
- package/src/modes/interactive/theme/defaults/limestone.json +100 -0
- package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
- package/src/modes/interactive/theme/defaults/marble.json +99 -0
- package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
- package/src/modes/interactive/theme/defaults/onyx.json +90 -0
- package/src/modes/interactive/theme/defaults/pearl.json +99 -0
- package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
- package/src/modes/interactive/theme/defaults/quartz.json +102 -0
- package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
- package/src/modes/interactive/theme/defaults/titanium.json +89 -0
- package/src/modes/print-mode.ts +14 -72
- package/src/modes/rpc/rpc-client.ts +23 -9
- package/src/modes/rpc/rpc-mode.ts +137 -125
- package/src/modes/rpc/rpc-types.ts +46 -24
- package/src/prompts/task.md +1 -0
- package/src/prompts/tools/gemini-image.md +4 -0
- package/src/prompts/tools/git.md +9 -0
- package/src/prompts/voice-summary.md +12 -0
- package/src/utils/image-convert.ts +26 -0
- package/src/utils/image-resize.ts +215 -0
- package/src/utils/shell-snapshot.ts +22 -20
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as os from "node:os";
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
4
2
|
import * as path from "node:path";
|
|
5
3
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
6
4
|
import { Type } from "@sinclair/typebox";
|
|
@@ -197,18 +195,15 @@ function exec(
|
|
|
197
195
|
args: string[],
|
|
198
196
|
options?: { timeout?: number; input?: string | Buffer },
|
|
199
197
|
): { stdout: string; stderr: string; ok: boolean } {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
maxBuffer: MAX_BYTES,
|
|
205
|
-
input: options?.input,
|
|
206
|
-
shell: true,
|
|
198
|
+
const result = Bun.spawnSync([cmd, ...args], {
|
|
199
|
+
stdin: options?.input ? (options.input as any) : "ignore",
|
|
200
|
+
stdout: "pipe",
|
|
201
|
+
stderr: "pipe",
|
|
207
202
|
});
|
|
208
203
|
return {
|
|
209
204
|
stdout: result.stdout?.toString() ?? "",
|
|
210
205
|
stderr: result.stderr?.toString() ?? "",
|
|
211
|
-
ok: result.
|
|
206
|
+
ok: result.exitCode === 0,
|
|
212
207
|
};
|
|
213
208
|
}
|
|
214
209
|
|
|
@@ -217,8 +212,12 @@ function exec(
|
|
|
217
212
|
*/
|
|
218
213
|
function hasCommand(cmd: string): boolean {
|
|
219
214
|
const checkCmd = isWindows ? "where" : "which";
|
|
220
|
-
const result = spawnSync(checkCmd,
|
|
221
|
-
|
|
215
|
+
const result = Bun.spawnSync([checkCmd, cmd], {
|
|
216
|
+
stdin: "ignore",
|
|
217
|
+
stdout: "pipe",
|
|
218
|
+
stderr: "pipe",
|
|
219
|
+
});
|
|
220
|
+
return result.exitCode === 0;
|
|
222
221
|
}
|
|
223
222
|
|
|
224
223
|
/**
|
|
@@ -299,26 +298,27 @@ function looksLikeHtml(content: string): boolean {
|
|
|
299
298
|
/**
|
|
300
299
|
* Convert binary file to markdown using markitdown
|
|
301
300
|
*/
|
|
302
|
-
function convertWithMarkitdown(
|
|
301
|
+
async function convertWithMarkitdown(
|
|
303
302
|
content: Buffer,
|
|
304
303
|
extensionHint: string,
|
|
305
304
|
timeout: number,
|
|
306
|
-
): { content: string; ok: boolean } {
|
|
305
|
+
): Promise<{ content: string; ok: boolean }> {
|
|
307
306
|
if (!hasCommand("markitdown")) {
|
|
308
307
|
return { content: "", ok: false };
|
|
309
308
|
}
|
|
310
309
|
|
|
311
310
|
// Write to temp file with extension hint
|
|
312
311
|
const ext = extensionHint || ".bin";
|
|
313
|
-
const
|
|
312
|
+
const tmpDir = tmpdir();
|
|
313
|
+
const tmpFile = path.join(tmpDir, `omp-convert-${Date.now()}${ext}`);
|
|
314
314
|
|
|
315
315
|
try {
|
|
316
|
-
|
|
316
|
+
await Bun.write(tmpFile, content);
|
|
317
317
|
const result = exec("markitdown", [tmpFile], { timeout });
|
|
318
318
|
return { content: result.stdout, ok: result.ok };
|
|
319
319
|
} finally {
|
|
320
320
|
try {
|
|
321
|
-
|
|
321
|
+
await Bun.$`rm ${tmpFile}`.quiet();
|
|
322
322
|
} catch {}
|
|
323
323
|
}
|
|
324
324
|
}
|
|
@@ -531,10 +531,11 @@ function parseFeedToMarkdown(content: string, maxItems = 10): string {
|
|
|
531
531
|
/**
|
|
532
532
|
* Render HTML to text using lynx
|
|
533
533
|
*/
|
|
534
|
-
function renderWithLynx(html: string, timeout: number): { content: string; ok: boolean } {
|
|
535
|
-
const
|
|
534
|
+
async function renderWithLynx(html: string, timeout: number): Promise<{ content: string; ok: boolean }> {
|
|
535
|
+
const tmpDir = tmpdir();
|
|
536
|
+
const tmpFile = path.join(tmpDir, `omp-render-${Date.now()}.html`);
|
|
536
537
|
try {
|
|
537
|
-
|
|
538
|
+
await Bun.write(tmpFile, html);
|
|
538
539
|
// Convert path to file URL (handles Windows paths correctly)
|
|
539
540
|
const normalizedPath = tmpFile.replace(/\\/g, "/");
|
|
540
541
|
const fileUrl = normalizedPath.startsWith("/") ? `file://${normalizedPath}` : `file:///${normalizedPath}`;
|
|
@@ -542,7 +543,7 @@ function renderWithLynx(html: string, timeout: number): { content: string; ok: b
|
|
|
542
543
|
return { content: result.stdout, ok: result.ok };
|
|
543
544
|
} finally {
|
|
544
545
|
try {
|
|
545
|
-
|
|
546
|
+
await Bun.$`rm ${tmpFile}`.quiet();
|
|
546
547
|
} catch {}
|
|
547
548
|
}
|
|
548
549
|
}
|
|
@@ -1752,7 +1753,7 @@ async function handleArxiv(url: string, timeout: number): Promise<RenderResult |
|
|
|
1752
1753
|
notes.push("Fetching PDF for full content...");
|
|
1753
1754
|
const pdfResult = await fetchBinary(pdfLink, timeout);
|
|
1754
1755
|
if (pdfResult.ok) {
|
|
1755
|
-
const converted = convertWithMarkitdown(pdfResult.buffer, ".pdf", timeout);
|
|
1756
|
+
const converted = await convertWithMarkitdown(pdfResult.buffer, ".pdf", timeout);
|
|
1756
1757
|
if (converted.ok && converted.content.length > 500) {
|
|
1757
1758
|
md += `---\n\n## Full Paper\n\n${converted.content}\n`;
|
|
1758
1759
|
notes.push("PDF converted via markitdown");
|
|
@@ -1835,7 +1836,7 @@ async function handleIacr(url: string, timeout: number): Promise<RenderResult |
|
|
|
1835
1836
|
notes.push("Fetching PDF for full content...");
|
|
1836
1837
|
const pdfResult = await fetchBinary(pdfUrl, timeout);
|
|
1837
1838
|
if (pdfResult.ok) {
|
|
1838
|
-
const converted = convertWithMarkitdown(pdfResult.buffer, ".pdf", timeout);
|
|
1839
|
+
const converted = await convertWithMarkitdown(pdfResult.buffer, ".pdf", timeout);
|
|
1839
1840
|
if (converted.ok && converted.content.length > 500) {
|
|
1840
1841
|
md += `---\n\n## Full Paper\n\n${converted.content}\n`;
|
|
1841
1842
|
notes.push("PDF converted via markitdown");
|
|
@@ -1992,7 +1993,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean = false): Pr
|
|
|
1992
1993
|
const binary = await fetchBinary(finalUrl, timeout);
|
|
1993
1994
|
if (binary.ok) {
|
|
1994
1995
|
const ext = getExtensionHint(finalUrl, binary.contentDisposition) || extHint;
|
|
1995
|
-
const converted = convertWithMarkitdown(binary.buffer, ext, timeout);
|
|
1996
|
+
const converted = await convertWithMarkitdown(binary.buffer, ext, timeout);
|
|
1996
1997
|
if (converted.ok && converted.content.trim().length > 50) {
|
|
1997
1998
|
notes.push(`Converted with markitdown`);
|
|
1998
1999
|
const output = finalizeOutput(converted.content);
|
|
@@ -2174,7 +2175,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean = false): Pr
|
|
|
2174
2175
|
};
|
|
2175
2176
|
}
|
|
2176
2177
|
|
|
2177
|
-
const lynxResult = renderWithLynx(rawContent, timeout);
|
|
2178
|
+
const lynxResult = await renderWithLynx(rawContent, timeout);
|
|
2178
2179
|
if (!lynxResult.ok) {
|
|
2179
2180
|
notes.push("lynx failed");
|
|
2180
2181
|
const output = finalizeOutput(rawContent);
|
|
@@ -2198,7 +2199,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean = false): Pr
|
|
|
2198
2199
|
const binary = await fetchBinary(docUrl, timeout);
|
|
2199
2200
|
if (binary.ok) {
|
|
2200
2201
|
const ext = getExtensionHint(docUrl, binary.contentDisposition);
|
|
2201
|
-
const converted = convertWithMarkitdown(binary.buffer, ext, timeout);
|
|
2202
|
+
const converted = await convertWithMarkitdown(binary.buffer, ext, timeout);
|
|
2202
2203
|
if (converted.ok && converted.content.trim().length > lynxResult.content.length) {
|
|
2203
2204
|
notes.push(`Extracted and converted document: ${docUrl}`);
|
|
2204
2205
|
const output = finalizeOutput(converted.content);
|
|
@@ -2384,7 +2385,7 @@ export function renderWebFetchResult(
|
|
|
2384
2385
|
? uiTheme.styledSymbol("status.warning", "warning")
|
|
2385
2386
|
: uiTheme.styledSymbol("status.success", "success");
|
|
2386
2387
|
const expandHint = expanded ? "" : uiTheme.fg("dim", " (Ctrl+O to expand)");
|
|
2387
|
-
let text = `${statusIcon} ${uiTheme.fg("
|
|
2388
|
+
let text = `${statusIcon} ${uiTheme.fg("accent", `(${domain})`)}${uiTheme.sep.dot}${uiTheme.fg("dim", details.method)}${expandHint}`;
|
|
2388
2389
|
|
|
2389
2390
|
// Get content text
|
|
2390
2391
|
const contentText = result.content[0]?.text ?? "";
|
|
@@ -20,10 +20,11 @@ import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../c
|
|
|
20
20
|
import { callExaTool, findApiKey as findExaKey, formatSearchResults, isSearchResponse } from "../exa/mcp-client";
|
|
21
21
|
import { renderExaCall, renderExaResult } from "../exa/render";
|
|
22
22
|
import type { ExaRenderDetails } from "../exa/types";
|
|
23
|
+
import { formatAge } from "../render-utils";
|
|
23
24
|
import { searchAnthropic } from "./providers/anthropic";
|
|
24
25
|
import { searchExa } from "./providers/exa";
|
|
25
26
|
import { findApiKey as findPerplexityKey, searchPerplexity } from "./providers/perplexity";
|
|
26
|
-
import {
|
|
27
|
+
import { renderWebSearchCall, renderWebSearchResult, type WebSearchRenderDetails } from "./render";
|
|
27
28
|
import type { WebSearchProvider, WebSearchResponse } from "./types";
|
|
28
29
|
|
|
29
30
|
/** Web search parameters schema */
|
|
@@ -31,8 +32,8 @@ export const webSearchSchema = Type.Object({
|
|
|
31
32
|
// Common
|
|
32
33
|
query: Type.String({ description: "Search query" }),
|
|
33
34
|
provider: Type.Optional(
|
|
34
|
-
Type.Union([Type.Literal("exa"), Type.Literal("anthropic"), Type.Literal("perplexity")], {
|
|
35
|
-
description: "Search provider (auto-detected if omitted
|
|
35
|
+
Type.Union([Type.Literal("auto"), Type.Literal("exa"), Type.Literal("anthropic"), Type.Literal("perplexity")], {
|
|
36
|
+
description: "Search provider (auto-detected if omitted or set to auto)",
|
|
36
37
|
}),
|
|
37
38
|
),
|
|
38
39
|
num_results: Type.Optional(Type.Number({ description: "Maximum number of results to return" })),
|
|
@@ -81,7 +82,7 @@ export const webSearchSchema = Type.Object({
|
|
|
81
82
|
|
|
82
83
|
export type WebSearchParams = {
|
|
83
84
|
query: string;
|
|
84
|
-
provider?: "exa" | "anthropic" | "perplexity";
|
|
85
|
+
provider?: "auto" | "exa" | "anthropic" | "perplexity";
|
|
85
86
|
num_results?: number;
|
|
86
87
|
// Anthropic
|
|
87
88
|
system_prompt?: string;
|
|
@@ -198,7 +199,7 @@ async function executeWebSearch(
|
|
|
198
199
|
params: WebSearchParams,
|
|
199
200
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: WebSearchRenderDetails }> {
|
|
200
201
|
try {
|
|
201
|
-
const provider = params.provider
|
|
202
|
+
const provider = params.provider && params.provider !== "auto" ? params.provider : await detectProvider();
|
|
202
203
|
|
|
203
204
|
let response: WebSearchResponse;
|
|
204
205
|
if (provider === "exa") {
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Returns structured search results with optional content extraction.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
8
10
|
import type { WebSearchResponse, WebSearchSource } from "../types";
|
|
9
11
|
|
|
10
12
|
const EXA_API_URL = "https://api.exa.ai/search";
|
|
@@ -27,10 +29,9 @@ export interface ExaSearchParams {
|
|
|
27
29
|
async function parseEnvFile(filePath: string): Promise<Record<string, string>> {
|
|
28
30
|
const result: Record<string, string> = {};
|
|
29
31
|
try {
|
|
30
|
-
|
|
31
|
-
if (!(await file.exists())) return result;
|
|
32
|
+
if (!existsSync(filePath)) return result;
|
|
32
33
|
|
|
33
|
-
const content =
|
|
34
|
+
const content = readFileSync(filePath, "utf-8");
|
|
34
35
|
for (const line of content.split("\n")) {
|
|
35
36
|
let trimmed = line.trim();
|
|
36
37
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -57,8 +58,8 @@ async function parseEnvFile(filePath: string): Promise<Record<string, string>> {
|
|
|
57
58
|
return result;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
function getHomeDir(): string
|
|
61
|
-
return
|
|
61
|
+
function getHomeDir(): string {
|
|
62
|
+
return homedir();
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
/** Find EXA_API_KEY from environment or .env files */
|
|
@@ -8,72 +8,39 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
8
8
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import type { Theme } from "../../../modes/interactive/theme/theme";
|
|
10
10
|
import type { RenderResultOptions } from "../../custom-tools/types";
|
|
11
|
+
import {
|
|
12
|
+
formatAge,
|
|
13
|
+
formatCount,
|
|
14
|
+
formatExpandHint,
|
|
15
|
+
formatMoreItems,
|
|
16
|
+
getDomain,
|
|
17
|
+
getPreviewLines,
|
|
18
|
+
getStyledStatusIcon,
|
|
19
|
+
PREVIEW_LIMITS,
|
|
20
|
+
TRUNCATE_LENGTHS,
|
|
21
|
+
truncate,
|
|
22
|
+
} from "../render-utils";
|
|
11
23
|
import type { WebSearchResponse } from "./types";
|
|
12
24
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const sliceLen = Math.max(0, maxLen - ellipsis.length);
|
|
17
|
-
return `${text.slice(0, sliceLen)}${ellipsis}`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Extract domain from URL */
|
|
21
|
-
export function getDomain(url: string): string {
|
|
22
|
-
try {
|
|
23
|
-
const u = new URL(url);
|
|
24
|
-
return u.hostname.replace(/^www\./, "");
|
|
25
|
-
} catch {
|
|
26
|
-
return url;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Format age string from seconds */
|
|
31
|
-
export function formatAge(ageSeconds: number | null | undefined): string {
|
|
32
|
-
if (!ageSeconds) return "";
|
|
33
|
-
const mins = Math.floor(ageSeconds / 60);
|
|
34
|
-
const hours = Math.floor(mins / 60);
|
|
35
|
-
const days = Math.floor(hours / 24);
|
|
36
|
-
const weeks = Math.floor(days / 7);
|
|
37
|
-
const months = Math.floor(days / 30);
|
|
38
|
-
|
|
39
|
-
if (months > 0) return `${months}mo ago`;
|
|
40
|
-
if (weeks > 0) return `${weeks}w ago`;
|
|
41
|
-
if (days > 0) return `${days}d ago`;
|
|
42
|
-
if (hours > 0) return `${hours}h ago`;
|
|
43
|
-
if (mins > 0) return `${mins}m ago`;
|
|
44
|
-
return "just now";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Get first N lines of text as preview */
|
|
48
|
-
export function getPreviewLines(text: string, maxLines: number, maxLineLen: number, ellipsis: string): string[] {
|
|
49
|
-
const lines = text.split("\n").filter((l) => l.trim());
|
|
50
|
-
return lines.slice(0, maxLines).map((l) => truncate(l.trim(), maxLineLen, ellipsis));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const MAX_COLLAPSED_ANSWER_LINES = 3;
|
|
54
|
-
const MAX_EXPANDED_ANSWER_LINES = 12;
|
|
55
|
-
const MAX_ANSWER_LINE_LEN = 110;
|
|
25
|
+
const MAX_COLLAPSED_ANSWER_LINES = PREVIEW_LIMITS.COLLAPSED_LINES;
|
|
26
|
+
const MAX_EXPANDED_ANSWER_LINES = PREVIEW_LIMITS.EXPANDED_LINES;
|
|
27
|
+
const MAX_ANSWER_LINE_LEN = TRUNCATE_LENGTHS.LINE;
|
|
56
28
|
const MAX_SNIPPET_LINES = 2;
|
|
57
|
-
const MAX_SNIPPET_LINE_LEN =
|
|
29
|
+
const MAX_SNIPPET_LINE_LEN = TRUNCATE_LENGTHS.LINE;
|
|
58
30
|
const MAX_RELATED_QUESTIONS = 6;
|
|
59
31
|
const MAX_QUERY_PREVIEW = 2;
|
|
60
32
|
const MAX_QUERY_LEN = 90;
|
|
61
33
|
const MAX_REQUEST_ID_LEN = 36;
|
|
62
34
|
|
|
63
|
-
function formatCount(label: string, count: number): string {
|
|
64
|
-
const safeCount = Number.isFinite(count) ? count : 0;
|
|
65
|
-
return `${safeCount} ${label}${safeCount === 1 ? "" : "s"}`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
35
|
function renderFallbackText(contentText: string, expanded: boolean, theme: Theme): Component {
|
|
69
36
|
const lines = contentText.split("\n").filter((line) => line.trim());
|
|
70
37
|
const maxLines = expanded ? lines.length : 6;
|
|
71
38
|
const displayLines = lines.slice(0, maxLines).map((line) => truncate(line.trim(), 110, theme.format.ellipsis));
|
|
72
39
|
const remaining = lines.length - displayLines.length;
|
|
73
40
|
|
|
74
|
-
const headerIcon =
|
|
75
|
-
const expandHint = expanded
|
|
76
|
-
let text = `${headerIcon} ${theme.fg("
|
|
41
|
+
const headerIcon = getStyledStatusIcon("warning", theme);
|
|
42
|
+
const expandHint = formatExpandHint(expanded, remaining > 0, theme);
|
|
43
|
+
let text = `${headerIcon} ${theme.fg("dim", "Response")}${expandHint}`;
|
|
77
44
|
|
|
78
45
|
if (displayLines.length === 0) {
|
|
79
46
|
text += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", "No response data")}`;
|
|
@@ -87,10 +54,7 @@ function renderFallbackText(contentText: string, expanded: boolean, theme: Theme
|
|
|
87
54
|
}
|
|
88
55
|
|
|
89
56
|
if (!expanded && remaining > 0) {
|
|
90
|
-
text += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
91
|
-
"muted",
|
|
92
|
-
`${theme.format.ellipsis} ${remaining} more line${remaining === 1 ? "" : "s"}`,
|
|
93
|
-
)}`;
|
|
57
|
+
text += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", formatMoreItems(remaining, "line", theme))}`;
|
|
94
58
|
}
|
|
95
59
|
|
|
96
60
|
return new Text(text, 0, 0);
|
|
@@ -134,6 +98,15 @@ export function renderWebSearchResult(
|
|
|
134
98
|
: [];
|
|
135
99
|
const provider = response.provider;
|
|
136
100
|
|
|
101
|
+
// Get answer text
|
|
102
|
+
const answerText = typeof response.answer === "string" ? response.answer.trim() : "";
|
|
103
|
+
const contentText = answerText || rawText;
|
|
104
|
+
const totalAnswerLines = contentText ? contentText.split("\n").filter((l) => l.trim()).length : 0;
|
|
105
|
+
const answerLimit = expanded ? MAX_EXPANDED_ANSWER_LINES : MAX_COLLAPSED_ANSWER_LINES;
|
|
106
|
+
const answerPreview = contentText
|
|
107
|
+
? getPreviewLines(contentText, answerLimit, MAX_ANSWER_LINE_LEN, theme.format.ellipsis)
|
|
108
|
+
: [];
|
|
109
|
+
|
|
137
110
|
// Build header: status icon Web Search (provider) · counts
|
|
138
111
|
const providerLabel =
|
|
139
112
|
provider === "anthropic"
|
|
@@ -143,53 +116,48 @@ export function renderWebSearchResult(
|
|
|
143
116
|
: provider === "exa"
|
|
144
117
|
? "Exa"
|
|
145
118
|
: "Unknown";
|
|
146
|
-
const headerIcon =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
119
|
+
const headerIcon = getStyledStatusIcon(sourceCount > 0 ? "success" : "warning", theme);
|
|
120
|
+
const hasMore =
|
|
121
|
+
totalAnswerLines > answerPreview.length ||
|
|
122
|
+
sourceCount > 0 ||
|
|
123
|
+
citationCount > 0 ||
|
|
124
|
+
relatedCount > 0 ||
|
|
125
|
+
searchQueries.length > 0;
|
|
126
|
+
const expandHint = formatExpandHint(expanded, hasMore, theme);
|
|
127
|
+
let text = `${headerIcon} ${theme.fg("dim", `(${providerLabel})`)}${theme.sep.dot}${theme.fg(
|
|
150
128
|
"dim",
|
|
151
129
|
formatCount("source", sourceCount),
|
|
152
130
|
)}${expandHint}`;
|
|
153
131
|
|
|
154
|
-
// Get answer text
|
|
155
|
-
const answerText = typeof response.answer === "string" ? response.answer.trim() : "";
|
|
156
|
-
const contentText = answerText || rawText;
|
|
157
|
-
const totalAnswerLines = contentText ? contentText.split("\n").filter((l) => l.trim()).length : 0;
|
|
158
|
-
const answerLimit = expanded ? MAX_EXPANDED_ANSWER_LINES : MAX_COLLAPSED_ANSWER_LINES;
|
|
159
|
-
const answerPreview = contentText
|
|
160
|
-
? getPreviewLines(contentText, answerLimit, MAX_ANSWER_LINE_LEN, theme.format.ellipsis)
|
|
161
|
-
: [];
|
|
162
|
-
|
|
163
132
|
if (!expanded) {
|
|
164
133
|
const answerTitle = `${theme.fg("accent", theme.status.info)} ${theme.fg("accent", "Answer")}`;
|
|
165
|
-
text += `\n ${theme.fg("dim", theme.tree.
|
|
134
|
+
text += `\n ${theme.fg("dim", theme.tree.branch)} ${answerTitle}`;
|
|
135
|
+
|
|
136
|
+
const remaining = totalAnswerLines - answerPreview.length;
|
|
137
|
+
const allLines: Array<{ text: string; style: "dim" | "muted" }> = [];
|
|
166
138
|
|
|
167
139
|
if (answerPreview.length === 0) {
|
|
168
|
-
text
|
|
169
|
-
"muted",
|
|
170
|
-
"No answer text returned",
|
|
171
|
-
)}`;
|
|
140
|
+
allLines.push({ text: "No answer text returned", style: "muted" });
|
|
172
141
|
} else {
|
|
173
142
|
for (const line of answerPreview) {
|
|
174
|
-
text
|
|
175
|
-
"dim",
|
|
176
|
-
line,
|
|
177
|
-
)}`;
|
|
143
|
+
allLines.push({ text: line, style: "dim" });
|
|
178
144
|
}
|
|
179
145
|
}
|
|
180
|
-
|
|
181
|
-
const remaining = totalAnswerLines - answerPreview.length;
|
|
182
146
|
if (remaining > 0) {
|
|
183
|
-
text
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
147
|
+
allLines.push({ text: formatMoreItems(remaining, "line", theme), style: "muted" });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < allLines.length; i++) {
|
|
151
|
+
const { text: lineText, style } = allLines[i];
|
|
152
|
+
const isLastLine = i === allLines.length - 1;
|
|
153
|
+
const lineBranch = isLastLine ? theme.tree.last : theme.tree.branch;
|
|
154
|
+
text += `\n ${theme.fg("dim", theme.tree.vertical)} ${theme.fg("dim", lineBranch)} ${theme.fg(style, lineText)}`;
|
|
187
155
|
}
|
|
188
156
|
|
|
189
157
|
const summary = [
|
|
190
158
|
formatCount("source", sourceCount),
|
|
191
159
|
formatCount("citation", citationCount),
|
|
192
|
-
formatCount("related", relatedCount),
|
|
160
|
+
formatCount("related question", relatedCount),
|
|
193
161
|
].join(theme.sep.dot);
|
|
194
162
|
text += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", summary)}`;
|
|
195
163
|
return new Text(text, 0, 0);
|
|
@@ -201,9 +169,7 @@ export function renderWebSearchResult(
|
|
|
201
169
|
);
|
|
202
170
|
const remainingAnswer = totalAnswerLines - answerPreview.length;
|
|
203
171
|
if (remainingAnswer > 0) {
|
|
204
|
-
answerSectionLines.push(
|
|
205
|
-
theme.fg("muted", `${theme.format.ellipsis} ${remainingAnswer} more line${remainingAnswer === 1 ? "" : "s"}`),
|
|
206
|
-
);
|
|
172
|
+
answerSectionLines.push(theme.fg("muted", formatMoreItems(remainingAnswer, "line", theme)));
|
|
207
173
|
}
|
|
208
174
|
|
|
209
175
|
const sourceLines: string[] = [];
|
|
@@ -263,14 +229,7 @@ export function renderWebSearchResult(
|
|
|
263
229
|
relatedLines.push(theme.fg("muted", `${theme.format.dash} ${related[i]}`));
|
|
264
230
|
}
|
|
265
231
|
if (relatedCount > maxRelated) {
|
|
266
|
-
relatedLines.push(
|
|
267
|
-
theme.fg(
|
|
268
|
-
"muted",
|
|
269
|
-
`${theme.format.ellipsis} ${relatedCount - maxRelated} more question${
|
|
270
|
-
relatedCount - maxRelated === 1 ? "" : "s"
|
|
271
|
-
}`,
|
|
272
|
-
),
|
|
273
|
-
);
|
|
232
|
+
relatedLines.push(theme.fg("muted", formatMoreItems(relatedCount - maxRelated, "question", theme)));
|
|
274
233
|
}
|
|
275
234
|
}
|
|
276
235
|
|
|
@@ -311,36 +270,29 @@ export function renderWebSearchResult(
|
|
|
311
270
|
metaLines.push(theme.fg("muted", `${theme.format.dash} ${truncate(q, MAX_QUERY_LEN, theme.format.ellipsis)}`));
|
|
312
271
|
}
|
|
313
272
|
if (searchQueries.length > MAX_QUERY_PREVIEW) {
|
|
314
|
-
metaLines.push(
|
|
315
|
-
theme.fg(
|
|
316
|
-
"muted",
|
|
317
|
-
`${theme.format.ellipsis} ${searchQueries.length - MAX_QUERY_PREVIEW} more query${
|
|
318
|
-
searchQueries.length - MAX_QUERY_PREVIEW === 1 ? "" : "s"
|
|
319
|
-
}`,
|
|
320
|
-
),
|
|
321
|
-
);
|
|
273
|
+
metaLines.push(theme.fg("muted", formatMoreItems(searchQueries.length - MAX_QUERY_PREVIEW, "query", theme)));
|
|
322
274
|
}
|
|
323
275
|
}
|
|
324
276
|
|
|
325
277
|
const sections: Array<{ title: string; icon: string; lines: string[] }> = [
|
|
326
278
|
{
|
|
327
279
|
title: "Answer",
|
|
328
|
-
icon:
|
|
280
|
+
icon: getStyledStatusIcon("info", theme),
|
|
329
281
|
lines: answerSectionLines,
|
|
330
282
|
},
|
|
331
283
|
{
|
|
332
284
|
title: "Sources",
|
|
333
|
-
icon: sourceCount > 0 ?
|
|
285
|
+
icon: getStyledStatusIcon(sourceCount > 0 ? "success" : "warning", theme),
|
|
334
286
|
lines: sourceLines,
|
|
335
287
|
},
|
|
336
288
|
{
|
|
337
289
|
title: "Related",
|
|
338
|
-
icon: relatedCount > 0 ?
|
|
290
|
+
icon: getStyledStatusIcon(relatedCount > 0 ? "info" : "warning", theme),
|
|
339
291
|
lines: relatedLines,
|
|
340
292
|
},
|
|
341
293
|
{
|
|
342
294
|
title: "Meta",
|
|
343
|
-
icon:
|
|
295
|
+
icon: getStyledStatusIcon("info", theme),
|
|
344
296
|
lines: metaLines,
|
|
345
297
|
},
|
|
346
298
|
];
|
|
@@ -349,11 +301,14 @@ export function renderWebSearchResult(
|
|
|
349
301
|
const section = sections[i];
|
|
350
302
|
const isLast = i === sections.length - 1;
|
|
351
303
|
const branch = isLast ? theme.tree.last : theme.tree.branch;
|
|
352
|
-
const indent = isLast ? "
|
|
304
|
+
const indent = isLast ? " " : `${theme.tree.vertical} `;
|
|
353
305
|
|
|
354
306
|
text += `\n ${theme.fg("dim", branch)} ${section.icon} ${theme.fg("accent", section.title)}`;
|
|
355
|
-
for (
|
|
356
|
-
|
|
307
|
+
for (let j = 0; j < section.lines.length; j++) {
|
|
308
|
+
const line = section.lines[j];
|
|
309
|
+
const isLastLine = j === section.lines.length - 1;
|
|
310
|
+
const lineBranch = isLastLine ? theme.tree.last : theme.tree.branch;
|
|
311
|
+
text += `\n ${theme.fg("dim", indent)}${theme.fg("dim", lineBranch)} ${line}`;
|
|
357
312
|
}
|
|
358
313
|
}
|
|
359
314
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Agent, run, setDefaultOpenAIKey } from "@openai/agents";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { logger } from "./logger";
|
|
4
|
+
import type { ModelRegistry } from "./model-registry";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CONTROLLER_MODEL = process.env.OMP_VOICE_CONTROLLER_MODEL ?? "gpt-4o-mini";
|
|
7
|
+
const DEFAULT_SUMMARY_MODEL = process.env.OMP_VOICE_SUMMARY_MODEL ?? DEFAULT_CONTROLLER_MODEL;
|
|
8
|
+
const MAX_INPUT_CHARS = 8000;
|
|
9
|
+
|
|
10
|
+
export type VoiceSteeringDecision = { action: "pass" | "ask"; text: string };
|
|
11
|
+
export type VoicePresentationDecision = { action: "skip" | "speak"; text?: string };
|
|
12
|
+
type VoiceSummaryOutput = { text: string };
|
|
13
|
+
|
|
14
|
+
const steeringSchema: z.ZodType<VoiceSteeringDecision> = z.object({
|
|
15
|
+
action: z.enum(["pass", "ask"]),
|
|
16
|
+
text: z.string().min(1),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const presentationSchema: z.ZodType<VoicePresentationDecision> = z.object({
|
|
20
|
+
action: z.enum(["skip", "speak"]),
|
|
21
|
+
text: z.string().min(1).optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const summarySchema: z.ZodType<VoiceSummaryOutput> = z.object({
|
|
25
|
+
text: z.string().min(1),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function normalizeText(text: string): string {
|
|
29
|
+
return text.replace(/\s+/g, " ").trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function truncateText(text: string, maxChars: number): string {
|
|
33
|
+
if (text.length <= maxChars) return text;
|
|
34
|
+
return `${text.slice(0, maxChars)}...`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class VoiceController {
|
|
38
|
+
private lastApiKey: string | undefined;
|
|
39
|
+
|
|
40
|
+
constructor(private registry: ModelRegistry) {}
|
|
41
|
+
|
|
42
|
+
private async ensureApiKey(): Promise<string | null> {
|
|
43
|
+
const apiKey = await this.registry.getApiKeyForProvider("openai");
|
|
44
|
+
if (!apiKey) {
|
|
45
|
+
logger.debug("voice-controller: no OpenAI API key available");
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (apiKey !== this.lastApiKey) {
|
|
49
|
+
setDefaultOpenAIKey(apiKey);
|
|
50
|
+
this.lastApiKey = apiKey;
|
|
51
|
+
}
|
|
52
|
+
return apiKey;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async steerUserInput(text: string): Promise<VoiceSteeringDecision | null> {
|
|
56
|
+
if (!(await this.ensureApiKey())) return null;
|
|
57
|
+
|
|
58
|
+
const normalized = truncateText(normalizeText(text), MAX_INPUT_CHARS);
|
|
59
|
+
const agent = new Agent({
|
|
60
|
+
name: "Voice Input Steering",
|
|
61
|
+
instructions:
|
|
62
|
+
"You are a voice-input controller for a coding agent. " +
|
|
63
|
+
"Given a user's speech transcript, decide if it is clear enough to send to the agent. " +
|
|
64
|
+
"If unclear or missing key details, ask exactly one short question. " +
|
|
65
|
+
"If clear, rewrite it as a concise instruction for the agent. " +
|
|
66
|
+
"Keep it short and preserve intent.",
|
|
67
|
+
model: DEFAULT_CONTROLLER_MODEL,
|
|
68
|
+
outputType: steeringSchema,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = await run(agent, normalized);
|
|
73
|
+
return result.finalOutput ?? null;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
logger.debug("voice-controller: steering error", {
|
|
76
|
+
error: error instanceof Error ? error.message : String(error),
|
|
77
|
+
});
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async decidePresentation(text: string): Promise<VoicePresentationDecision | null> {
|
|
83
|
+
if (!(await this.ensureApiKey())) return null;
|
|
84
|
+
|
|
85
|
+
const normalized = truncateText(normalizeText(text), MAX_INPUT_CHARS);
|
|
86
|
+
const agent = new Agent({
|
|
87
|
+
name: "Voice Presentation Gate",
|
|
88
|
+
instructions:
|
|
89
|
+
"You are a voice presentation gate for a coding agent. " +
|
|
90
|
+
"Decide whether to speak the assistant response to the user. " +
|
|
91
|
+
"Speak when there is a decision, summary, or a question for the user. " +
|
|
92
|
+
"Skip if it is mostly tool output, verbose logs, or not useful to speak. " +
|
|
93
|
+
"When speaking, respond in 1-3 short sentences (<=45 words) in a casual, concise tone. " +
|
|
94
|
+
"If user input is needed, ask exactly one short question.",
|
|
95
|
+
model: DEFAULT_CONTROLLER_MODEL,
|
|
96
|
+
outputType: presentationSchema,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const result = await run(agent, normalized);
|
|
101
|
+
return result.finalOutput ?? null;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.debug("voice-controller: presentation error", {
|
|
104
|
+
error: error instanceof Error ? error.message : String(error),
|
|
105
|
+
});
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async summarizeForVoice(text: string): Promise<string | null> {
|
|
111
|
+
if (!(await this.ensureApiKey())) return null;
|
|
112
|
+
|
|
113
|
+
const normalized = truncateText(normalizeText(text), MAX_INPUT_CHARS);
|
|
114
|
+
const agent = new Agent({
|
|
115
|
+
name: "Voice Summary",
|
|
116
|
+
instructions:
|
|
117
|
+
"Summarize the assistant response for voice playback. " +
|
|
118
|
+
"Use 1-2 short sentences. " +
|
|
119
|
+
"If a question is required from the user, ask one short question.",
|
|
120
|
+
model: DEFAULT_SUMMARY_MODEL,
|
|
121
|
+
outputType: summarySchema,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const result = await run(agent, normalized);
|
|
126
|
+
const output = result.finalOutput?.text ?? "";
|
|
127
|
+
return output.trim() || null;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
logger.debug("voice-controller: summary error", {
|
|
130
|
+
error: error instanceof Error ? error.message : String(error),
|
|
131
|
+
});
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|