@oh-my-pi/pi-coding-agent 16.2.8 → 16.2.11
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/dist/cli.js +3160 -3096
- package/dist/types/config/settings-schema.d.ts +41 -13
- package/dist/types/extensibility/skills.d.ts +29 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +8 -0
- package/dist/types/modes/components/todo-reminder.d.ts +3 -1
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +9 -0
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +0 -1
- package/dist/types/modes/skill-command.d.ts +1 -1
- package/dist/types/modes/types.d.ts +0 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/stt/asr-client.d.ts +7 -3
- package/dist/types/stt/index.d.ts +1 -0
- package/dist/types/stt/stt-controller.d.ts +2 -0
- package/dist/types/stt/submit-trigger.d.ts +30 -0
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/types.d.ts +6 -6
- package/dist/types/tiny/models.d.ts +22 -8
- package/package.json +14 -13
- package/scripts/bundle-dist.ts +23 -4
- package/scripts/generate-docs-index.ts +116 -24
- package/src/async/job-manager.ts +27 -3
- package/src/cli/grep-cli.ts +1 -1
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/commit/agentic/tools/analyze-file.ts +2 -2
- package/src/config/model-discovery.ts +118 -76
- package/src/config/settings-schema.ts +15 -1
- package/src/debug/profiler.ts +7 -1
- package/src/extensibility/skills.ts +77 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/lsp/config.ts +17 -3
- package/src/mcp/oauth-flow.ts +35 -8
- package/src/modes/acp/acp-agent.ts +6 -9
- package/src/modes/components/mcp-add-wizard.ts +43 -3
- package/src/modes/components/model-selector.ts +21 -9
- package/src/modes/components/todo-reminder.ts +5 -1
- package/src/modes/controllers/event-controller.ts +40 -15
- package/src/modes/controllers/mcp-command-controller.ts +84 -3
- package/src/modes/controllers/selector-controller.ts +57 -35
- package/src/modes/controllers/tool-args-reveal.ts +12 -0
- package/src/modes/interactive-mode.ts +5 -10
- package/src/modes/rpc/rpc-mode.ts +5 -8
- package/src/modes/skill-command.ts +8 -20
- package/src/modes/types.ts +0 -1
- package/src/prompts/agents/tester.md +107 -0
- package/src/prompts/system/orchestrate-notice.md +2 -2
- package/src/prompts/system/system-prompt.md +2 -5
- package/src/prompts/system/thinking-loop-redirect.md +10 -0
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/task.md +2 -9
- package/src/session/agent-session.ts +53 -18
- package/src/stt/asr-client.ts +87 -27
- package/src/stt/downloader.ts +8 -2
- package/src/stt/index.ts +1 -0
- package/src/stt/stt-controller.ts +31 -2
- package/src/stt/submit-trigger.ts +74 -0
- package/src/task/agents.ts +4 -4
- package/src/task/executor.ts +2 -4
- package/src/task/index.ts +32 -10
- package/src/task/types.ts +5 -5
- package/src/tiny/models.ts +10 -0
- package/src/tools/ast-grep.ts +34 -12
- package/src/tools/grep.ts +11 -8
- package/src/utils/git.ts +22 -1
- package/src/prompts/agents/oracle.md +0 -54
|
@@ -1,56 +1,148 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Populate
|
|
4
|
+
* Populate, check, or reset the embedded harness documentation index for `omp://`.
|
|
5
5
|
*
|
|
6
6
|
* `--generate` writes `src/internal-urls/docs-index.generated.txt` as two lines:
|
|
7
7
|
* a plain JSON array of the sorted `docs/**\/*.md` file names, then a base64
|
|
8
|
-
* gzip blob of the index-aligned doc bodies (`string[]`).
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* gzip blob of the index-aligned doc bodies (`string[]`). `--check` rebuilds
|
|
9
|
+
* that payload from the real docs corpus and compares it to the embed when
|
|
10
|
+
* present; the checked-in empty placeholder is accepted after verifying that a
|
|
11
|
+
* fresh generated payload round-trips. `--reset` restores the placeholder so the
|
|
12
12
|
* dev tree reads `docs/` from disk. Mirrors the stats / model-catalog embeds.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
-
import { gzipSync } from "node:zlib";
|
|
16
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
17
17
|
import { Glob } from "bun";
|
|
18
18
|
|
|
19
19
|
const docsDir = path.resolve(import.meta.dir, "../../../docs");
|
|
20
20
|
const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/docs-index.generated.txt");
|
|
21
21
|
const GENERATE_FLAG = "--generate";
|
|
22
22
|
const RESET_FLAG = "--reset";
|
|
23
|
+
const CHECK_FLAG = "--check";
|
|
24
|
+
|
|
25
|
+
export interface DocsIndexPayload {
|
|
26
|
+
/** Sorted `docs/**\/*.md` file names plus index-aligned bodies and embed text. */
|
|
27
|
+
readonly files: readonly string[];
|
|
28
|
+
readonly bodies: readonly string[];
|
|
29
|
+
readonly payload: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DecodedDocsIndexPayload {
|
|
33
|
+
/** Sorted `docs/**\/*.md` file names decoded from an embed payload. */
|
|
34
|
+
readonly files: readonly string[];
|
|
35
|
+
/** Index-aligned Markdown bodies decoded from an embed payload. */
|
|
36
|
+
readonly bodies: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isStringArray(value: unknown): value is string[] {
|
|
40
|
+
return Array.isArray(value) && value.every(item => typeof item === "string");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Build the exact two-line `omp://` docs embed from the source `docs/**\/*.md` corpus. */
|
|
44
|
+
export async function buildDocsIndexPayload(): Promise<DocsIndexPayload> {
|
|
45
|
+
const glob = new Glob("**/*.md");
|
|
46
|
+
const files: string[] = [];
|
|
47
|
+
for await (const relativePath of glob.scan(docsDir)) {
|
|
48
|
+
files.push(relativePath.split(path.sep).join("/"));
|
|
49
|
+
}
|
|
50
|
+
files.sort();
|
|
51
|
+
|
|
52
|
+
const bodies = await Promise.all(files.map(file => Bun.file(path.join(docsDir, file)).text()));
|
|
53
|
+
const bodiesB64 = Buffer.from(gzipSync(Buffer.from(JSON.stringify(bodies)), { level: 9 })).toString("base64");
|
|
54
|
+
return {
|
|
55
|
+
files,
|
|
56
|
+
bodies,
|
|
57
|
+
payload: `${JSON.stringify(files)}\n${bodiesB64}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Decode a populated docs embed payload into filenames and index-aligned Markdown bodies. */
|
|
62
|
+
export function decodeDocsIndexPayload(embed: string): DecodedDocsIndexPayload | null {
|
|
63
|
+
const newline = embed.indexOf("\n");
|
|
64
|
+
if (newline === -1) return null;
|
|
65
|
+
|
|
66
|
+
const filenames: unknown = JSON.parse(embed.slice(0, newline));
|
|
67
|
+
if (!isStringArray(filenames)) {
|
|
68
|
+
throw new Error("Embedded docs index filename line is not a JSON string array.");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const inflated = gunzipSync(Buffer.from(embed.slice(newline + 1), "base64"));
|
|
72
|
+
const bodies: unknown = JSON.parse(inflated.toString("utf8"));
|
|
73
|
+
if (!isStringArray(bodies)) {
|
|
74
|
+
throw new Error("Embedded docs index body blob is not a JSON string array.");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { files: filenames, bodies };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Assert that an embed payload is fresh against the current source docs payload.
|
|
82
|
+
* An empty placeholder is accepted by round-tripping the expected payload (the
|
|
83
|
+
* dev tree and post-build reset state both checked-in placeholders).
|
|
84
|
+
*/
|
|
85
|
+
export function assertDocsIndexFresh(embed: string, expected: DecodedDocsIndexPayload): void {
|
|
86
|
+
const source =
|
|
87
|
+
embed.length > 0
|
|
88
|
+
? embed
|
|
89
|
+
: `${JSON.stringify(expected.files)}\n${Buffer.from(gzipSync(Buffer.from(JSON.stringify(expected.bodies)), { level: 9 })).toString("base64")}`;
|
|
90
|
+
const decoded = decodeDocsIndexPayload(source);
|
|
91
|
+
if (decoded === null) {
|
|
92
|
+
throw new Error("Embedded docs index is malformed: missing newline separator.");
|
|
93
|
+
}
|
|
94
|
+
if (decoded.files.length !== expected.files.length) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Embedded docs index has ${decoded.files.length} docs; source corpus has ${expected.files.length}.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (decoded.bodies.length !== expected.bodies.length) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Embedded docs index has ${decoded.bodies.length} bodies; source corpus has ${expected.bodies.length}.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
for (let i = 0; i < expected.files.length; i++) {
|
|
105
|
+
if (decoded.files[i] !== expected.files[i]) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Embedded docs index filename mismatch at ${i}: ${decoded.files[i] ?? "<missing>"} !== ${expected.files[i]}.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (decoded.bodies[i] !== expected.bodies[i]) {
|
|
111
|
+
throw new Error(`Embedded docs index body mismatch for ${expected.files[i]}. Run \`bun run gen:docs\`.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
23
115
|
|
|
24
116
|
async function main(): Promise<void> {
|
|
25
117
|
const rel = path.relative(process.cwd(), outputPath);
|
|
26
118
|
|
|
27
119
|
if (process.argv.includes(RESET_FLAG)) {
|
|
28
120
|
await Bun.write(outputPath, "");
|
|
29
|
-
|
|
121
|
+
process.stdout.write(`Reset ${rel}\n`);
|
|
30
122
|
return;
|
|
31
123
|
}
|
|
32
124
|
|
|
33
|
-
if (
|
|
34
|
-
|
|
125
|
+
if (process.argv.includes(CHECK_FLAG)) {
|
|
126
|
+
const current = await buildDocsIndexPayload();
|
|
127
|
+
const embed = await Bun.file(outputPath).text();
|
|
128
|
+
assertDocsIndexFresh(embed, current);
|
|
129
|
+
process.stdout.write(`Docs index fresh for ${current.files.length} docs (${rel})\n`);
|
|
35
130
|
return;
|
|
36
131
|
}
|
|
37
132
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
133
|
+
if (!process.argv.includes(GENERATE_FLAG)) {
|
|
134
|
+
process.stdout.write(
|
|
135
|
+
`Skipping ${rel}; pass ${GENERATE_FLAG} to embed docs (the dev tree reads docs/ from disk)\n`,
|
|
136
|
+
);
|
|
137
|
+
return;
|
|
42
138
|
}
|
|
43
|
-
files.sort();
|
|
44
|
-
|
|
45
|
-
// Index-aligned bodies (Promise.all preserves order), kept separate from the
|
|
46
|
-
// filename list so the loader can list docs without inflating the blob.
|
|
47
|
-
const bodies = await Promise.all(files.map(file => Bun.file(path.join(docsDir, file)).text()));
|
|
48
139
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
console.log(`Generated ${rel} (${files.length} docs, ${payload.length} bytes)`);
|
|
140
|
+
const current = await buildDocsIndexPayload();
|
|
141
|
+
assertDocsIndexFresh(current.payload, current);
|
|
142
|
+
await Bun.write(outputPath, current.payload);
|
|
143
|
+
process.stdout.write(`Generated ${rel} (${current.files.length} docs, ${current.payload.length} bytes)\n`);
|
|
54
144
|
}
|
|
55
145
|
|
|
56
|
-
|
|
146
|
+
if (import.meta.main) {
|
|
147
|
+
await main();
|
|
148
|
+
}
|
package/src/async/job-manager.ts
CHANGED
|
@@ -397,6 +397,27 @@ export class AsyncJobManager {
|
|
|
397
397
|
await Promise.all(Array.from(this.#jobs.values()).map(job => job.promise));
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
async #waitForAllUntil(deadline: number): Promise<boolean> {
|
|
401
|
+
const promises = Array.from(this.#jobs.values()).map(job => job.promise);
|
|
402
|
+
if (promises.length === 0) return true;
|
|
403
|
+
if (deadline === Number.POSITIVE_INFINITY) {
|
|
404
|
+
await Promise.all(promises);
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
const remainingMs = deadline - Date.now();
|
|
408
|
+
if (remainingMs <= 0) return false;
|
|
409
|
+
|
|
410
|
+
const timeout = Promise.withResolvers<"timeout">();
|
|
411
|
+
const timer = setTimeout(() => timeout.resolve("timeout"), remainingMs);
|
|
412
|
+
timer.unref();
|
|
413
|
+
try {
|
|
414
|
+
const result = await Promise.race([Promise.all(promises).then(() => "settled" as const), timeout.promise]);
|
|
415
|
+
return result === "settled";
|
|
416
|
+
} finally {
|
|
417
|
+
clearTimeout(timer);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
400
421
|
async drainDeliveries(options?: { timeoutMs?: number; filter?: AsyncJobFilter }): Promise<boolean> {
|
|
401
422
|
const timeoutMs = options?.timeoutMs;
|
|
402
423
|
const filter = options?.filter;
|
|
@@ -445,8 +466,10 @@ export class AsyncJobManager {
|
|
|
445
466
|
this.#disposed = true;
|
|
446
467
|
this.#clearEvictionTimers();
|
|
447
468
|
this.cancelAll();
|
|
448
|
-
|
|
449
|
-
const
|
|
469
|
+
const timeoutMs = Math.max(options?.timeoutMs ?? 3_000, 0);
|
|
470
|
+
const deadline = Date.now() + timeoutMs;
|
|
471
|
+
const jobsSettled = await this.#waitForAllUntil(deadline);
|
|
472
|
+
const drained = await this.drainDeliveries({ timeoutMs: Math.max(deadline - Date.now(), 0) });
|
|
450
473
|
this.#clearEvictionTimers();
|
|
451
474
|
this.#jobs.clear();
|
|
452
475
|
this.#deliveries.length = 0;
|
|
@@ -454,7 +477,7 @@ export class AsyncJobManager {
|
|
|
454
477
|
this.#suppressedDeliveries.clear();
|
|
455
478
|
this.#watchedJobs.clear();
|
|
456
479
|
this.#pollEscalation.clear();
|
|
457
|
-
return drained;
|
|
480
|
+
return jobsSettled && drained;
|
|
458
481
|
}
|
|
459
482
|
|
|
460
483
|
#resolveJobId(preferredId?: string): string {
|
|
@@ -483,6 +506,7 @@ export class AsyncJobManager {
|
|
|
483
506
|
}
|
|
484
507
|
|
|
485
508
|
#scheduleEviction(jobId: string): void {
|
|
509
|
+
if (this.#disposed) return;
|
|
486
510
|
if (this.#retentionMs <= 0) {
|
|
487
511
|
this.#jobs.delete(jobId);
|
|
488
512
|
this.#suppressedDeliveries.delete(jobId);
|
package/src/cli/grep-cli.ts
CHANGED
|
@@ -150,7 +150,7 @@ ${chalk.bold("Options:")}
|
|
|
150
150
|
--no-gitignore Include files excluded by .gitignore
|
|
151
151
|
|
|
152
152
|
${chalk.bold("Environment:")}
|
|
153
|
-
|
|
153
|
+
PI_WALK_WORKERS=N Set filesystem walker workers (default 4, 0 = auto)
|
|
154
154
|
|
|
155
155
|
${chalk.bold("Examples:")}
|
|
156
156
|
${APP_NAME} grep "import" src/
|
|
@@ -42,7 +42,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
|
|
|
42
42
|
types_description: typesDescription,
|
|
43
43
|
});
|
|
44
44
|
const state: CommitAgentState = { diffText: input.diffText };
|
|
45
|
-
const spawns = "
|
|
45
|
+
const spawns = "sonic";
|
|
46
46
|
const tools = createCommitTools({
|
|
47
47
|
cwd: input.cwd,
|
|
48
48
|
authStorage: input.authStorage,
|
|
@@ -27,7 +27,7 @@ Tool guidance:
|
|
|
27
27
|
- git_file_diff: diff for specific files
|
|
28
28
|
- git_hunk: specific hunks for large diffs
|
|
29
29
|
- recent_commits: recent commit subjects + style stats
|
|
30
|
-
- analyze_files: spawn
|
|
30
|
+
- analyze_files: spawn sonic subagents in parallel for analysis
|
|
31
31
|
- propose_changelog: provide changelog entries for each changelog target
|
|
32
32
|
- propose_commit: submit final commit proposal and run validation
|
|
33
33
|
- split_commit: propose multiple commit groups (no overlapping files; all staged files covered)
|
|
@@ -63,7 +63,7 @@ export function createAnalyzeFileTool(options: {
|
|
|
63
63
|
return {
|
|
64
64
|
name: "analyze_files",
|
|
65
65
|
label: "Analyze Files",
|
|
66
|
-
description: "Spawn
|
|
66
|
+
description: "Spawn sonic agents to analyze files.",
|
|
67
67
|
parameters: analyzeFileSchema,
|
|
68
68
|
async execute(toolCallId, params, _onUpdate, ctx, signal) {
|
|
69
69
|
const toolSession = buildToolSession(ctx, options);
|
|
@@ -83,7 +83,7 @@ export function createAnalyzeFileTool(options: {
|
|
|
83
83
|
related_files: relatedFiles,
|
|
84
84
|
});
|
|
85
85
|
const taskParams: TaskParams = {
|
|
86
|
-
agent: "
|
|
86
|
+
agent: "sonic",
|
|
87
87
|
id: `AnalyzeFile${index + 1}`,
|
|
88
88
|
description: `Analyze ${file}`,
|
|
89
89
|
assignment,
|
|
@@ -35,6 +35,31 @@ import type { ProviderDiscovery } from "./models-config-schema";
|
|
|
35
35
|
export const DISCOVERY_DEFAULT_CONTEXT_WINDOW = OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW;
|
|
36
36
|
export const DISCOVERY_DEFAULT_MAX_TOKENS = OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS;
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Run `fn` with an abort signal that fires after `timeoutMs`, clearing the
|
|
40
|
+
* backing timer the instant the operation settles.
|
|
41
|
+
*
|
|
42
|
+
* Unlike the built-in abort-signal timeout API, the timer never outlives the
|
|
43
|
+
* request: on the success path it is cancelled before `fn` resolves, so the
|
|
44
|
+
* signal is never aborted and no pending callback lingers on the heap. A leaked
|
|
45
|
+
* abort-signal timeout (e.g. discovery against a mocked fetch that resolves
|
|
46
|
+
* instantly) fires seconds later and sets its abort `reason` — which crashed
|
|
47
|
+
* Bun's concurrent GC while it marked the signal's wrapped reason during an
|
|
48
|
+
* unrelated allocation (`JSAbortSignal::visitAdditionalChildren`).
|
|
49
|
+
*/
|
|
50
|
+
async function withTimeoutSignal<T>(timeoutMs: number, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
const timer = setTimeout(
|
|
53
|
+
() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")),
|
|
54
|
+
timeoutMs,
|
|
55
|
+
);
|
|
56
|
+
try {
|
|
57
|
+
return await fn(controller.signal);
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
|
|
39
64
|
const OLLAMA_HOST_DEFAULT_PORT = "11434";
|
|
40
65
|
|
|
@@ -288,16 +313,18 @@ async function discoverOllamaModelMetadata(
|
|
|
288
313
|
): Promise<OllamaDiscoveredModelMetadata | null> {
|
|
289
314
|
const showUrl = `${endpoint}/api/show`;
|
|
290
315
|
try {
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
316
|
+
const payload = await withTimeoutSignal(150, async signal => {
|
|
317
|
+
const response = await ctx.fetch(showUrl, {
|
|
318
|
+
method: "POST",
|
|
319
|
+
headers: { ...(headers ?? {}), "Content-Type": "application/json" },
|
|
320
|
+
body: JSON.stringify({ model: modelId }),
|
|
321
|
+
signal,
|
|
322
|
+
});
|
|
323
|
+
if (!response.ok) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
return (await response.json()) as unknown;
|
|
296
327
|
});
|
|
297
|
-
if (!response.ok) {
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
const payload = (await response.json()) as unknown;
|
|
301
328
|
if (!isRecord(payload)) {
|
|
302
329
|
return null;
|
|
303
330
|
}
|
|
@@ -339,14 +366,16 @@ export async function discoverOllamaModels(
|
|
|
339
366
|
const endpoint = normalizeOllamaBaseUrl(providerConfig.baseUrl);
|
|
340
367
|
const tagsUrl = `${endpoint}/api/tags`;
|
|
341
368
|
const headers = { ...(providerConfig.headers ?? {}) };
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
369
|
+
const payload = await withTimeoutSignal(250, async signal => {
|
|
370
|
+
const response = await ctx.fetch(tagsUrl, {
|
|
371
|
+
headers,
|
|
372
|
+
signal,
|
|
373
|
+
});
|
|
374
|
+
if (!response.ok) {
|
|
375
|
+
throw new Error(`HTTP ${response.status} from ${tagsUrl}`);
|
|
376
|
+
}
|
|
377
|
+
return (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
|
|
345
378
|
});
|
|
346
|
-
if (!response.ok) {
|
|
347
|
-
throw new Error(`HTTP ${response.status} from ${tagsUrl}`);
|
|
348
|
-
}
|
|
349
|
-
const payload = (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
|
|
350
379
|
const entries = (payload.models ?? []).flatMap(item => {
|
|
351
380
|
const id = item.model || item.name;
|
|
352
381
|
return id ? [{ id, name: item.name || id }] : [];
|
|
@@ -384,14 +413,16 @@ async function discoverLlamaCppServerMetadata(
|
|
|
384
413
|
): Promise<LlamaCppDiscoveredServerMetadata | null> {
|
|
385
414
|
const propsUrl = `${toLlamaCppNativeBaseUrl(baseUrl)}/props`;
|
|
386
415
|
try {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
416
|
+
const payload = await withTimeoutSignal(150, async signal => {
|
|
417
|
+
const response = await ctx.fetch(propsUrl, {
|
|
418
|
+
headers,
|
|
419
|
+
signal,
|
|
420
|
+
});
|
|
421
|
+
if (!response.ok) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
return (await response.json()) as unknown;
|
|
390
425
|
});
|
|
391
|
-
if (!response.ok) {
|
|
392
|
-
return null;
|
|
393
|
-
}
|
|
394
|
-
const payload = (await response.json()) as unknown;
|
|
395
426
|
if (!isRecord(payload)) {
|
|
396
427
|
return null;
|
|
397
428
|
}
|
|
@@ -415,24 +446,26 @@ export async function discoverLlamaCppModels(
|
|
|
415
446
|
const baseHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
416
447
|
let headers = baseHeaders;
|
|
417
448
|
const attempt = async (h: Record<string, string>) => {
|
|
418
|
-
const [
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
449
|
+
const [payload, metadata] = await Promise.all([
|
|
450
|
+
withTimeoutSignal(250, async signal => {
|
|
451
|
+
const response = await ctx.fetch(modelsUrl, {
|
|
452
|
+
headers: h,
|
|
453
|
+
signal,
|
|
454
|
+
});
|
|
455
|
+
if (!response.ok) {
|
|
456
|
+
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
457
|
+
}
|
|
458
|
+
headers = h;
|
|
459
|
+
return (await response.json()) as unknown;
|
|
422
460
|
}),
|
|
423
461
|
discoverLlamaCppServerMetadata(ctx, baseUrl, h),
|
|
424
462
|
]);
|
|
425
|
-
|
|
426
|
-
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
427
|
-
}
|
|
428
|
-
headers = h;
|
|
429
|
-
return [response, metadata] as const;
|
|
463
|
+
return [payload, metadata] as const;
|
|
430
464
|
};
|
|
431
465
|
const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
|
|
432
|
-
const [
|
|
466
|
+
const [payload, serverMetadata] = apiKey
|
|
433
467
|
? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
|
|
434
468
|
: await attempt(baseHeaders);
|
|
435
|
-
const payload = (await response.json()) as unknown;
|
|
436
469
|
const models = parseLlamaCppModelList(payload);
|
|
437
470
|
const discovered: Model<Api>[] = [];
|
|
438
471
|
for (const item of models) {
|
|
@@ -476,17 +509,22 @@ export async function discoverLlamaCppModelRuntimeMetadata(
|
|
|
476
509
|
const modelsUrl = `${baseUrl}/models`;
|
|
477
510
|
const baseHeaders: Record<string, string> = { ...(model.headers ?? {}) };
|
|
478
511
|
const attempt = async (headers: Record<string, string>) => {
|
|
479
|
-
const [
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
512
|
+
const [entries, serverMetadata] = await Promise.all([
|
|
513
|
+
withTimeoutSignal(250, async signal => {
|
|
514
|
+
const response = await ctx.fetch(modelsUrl, {
|
|
515
|
+
headers,
|
|
516
|
+
signal,
|
|
517
|
+
});
|
|
518
|
+
if (!response.ok) {
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
return parseLlamaCppModelList(await response.json());
|
|
483
522
|
}),
|
|
484
523
|
discoverLlamaCppServerMetadata(ctx, baseUrl, headers),
|
|
485
524
|
]);
|
|
486
|
-
if (!
|
|
525
|
+
if (!entries) {
|
|
487
526
|
return undefined;
|
|
488
527
|
}
|
|
489
|
-
const entries = parseLlamaCppModelList(await response.json());
|
|
490
528
|
const entry = entries.find(entry => entry.id === model.id);
|
|
491
529
|
if (!entry) {
|
|
492
530
|
return undefined;
|
|
@@ -524,26 +562,28 @@ export async function discoverOpenAIModelsList(
|
|
|
524
562
|
providerConfig.discovery.type === "lm-studio"
|
|
525
563
|
? fetchLmStudioNativeModelMetadata(baseUrl, ctx.fetch, { headers: h })
|
|
526
564
|
: Promise.resolve(null);
|
|
527
|
-
const [
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
565
|
+
const [payload, nativeMetadata] = await Promise.all([
|
|
566
|
+
withTimeoutSignal(10_000, async signal => {
|
|
567
|
+
const res = await ctx.fetch(modelsUrl, {
|
|
568
|
+
headers: h,
|
|
569
|
+
signal,
|
|
570
|
+
});
|
|
571
|
+
if (!res.ok) {
|
|
572
|
+
throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
|
|
573
|
+
}
|
|
574
|
+
headers = h;
|
|
575
|
+
return (await res.json()) as {
|
|
576
|
+
data?: Array<{ id?: string; max_model_len?: unknown; context_length?: unknown }>;
|
|
577
|
+
};
|
|
531
578
|
}),
|
|
532
579
|
nativeMetadataPromise,
|
|
533
580
|
]);
|
|
534
|
-
|
|
535
|
-
throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
|
|
536
|
-
}
|
|
537
|
-
headers = h;
|
|
538
|
-
return [res, nativeMetadata] as const;
|
|
581
|
+
return [payload, nativeMetadata] as const;
|
|
539
582
|
};
|
|
540
583
|
const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
|
|
541
|
-
const [
|
|
584
|
+
const [payload, nativeMetadata] = apiKey
|
|
542
585
|
? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
|
|
543
586
|
: await attempt(baseHeaders);
|
|
544
|
-
const payload = (await response.json()) as {
|
|
545
|
-
data?: Array<{ id?: string; max_model_len?: unknown; context_length?: unknown }>;
|
|
546
|
-
};
|
|
547
587
|
const models = payload.data ?? [];
|
|
548
588
|
const discovered: Model<Api>[] = [];
|
|
549
589
|
for (const item of models) {
|
|
@@ -600,15 +640,17 @@ export async function discoverLiteLLMModels(
|
|
|
600
640
|
}
|
|
601
641
|
return response;
|
|
602
642
|
};
|
|
603
|
-
const models = await
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
643
|
+
const models = await withTimeoutSignal(10_000, signal =>
|
|
644
|
+
fetchLiteLLMRichModels({
|
|
645
|
+
api: providerConfig.api,
|
|
646
|
+
provider: providerConfig.provider,
|
|
647
|
+
baseUrl,
|
|
648
|
+
headers: h,
|
|
649
|
+
fetch: authAwareFetch,
|
|
650
|
+
referenceResolver: resolveReference,
|
|
651
|
+
signal,
|
|
652
|
+
}),
|
|
653
|
+
);
|
|
612
654
|
if (authError && models === null) {
|
|
613
655
|
throw authError;
|
|
614
656
|
}
|
|
@@ -657,24 +699,24 @@ export async function discoverProxyModels(
|
|
|
657
699
|
|
|
658
700
|
const baseHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
659
701
|
let headers = baseHeaders;
|
|
660
|
-
const attempt = async (h: Record<string, string>) =>
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
702
|
+
const attempt = async (h: Record<string, string>) =>
|
|
703
|
+
withTimeoutSignal(10_000, async signal => {
|
|
704
|
+
const res = await ctx.fetch(modelsUrl, {
|
|
705
|
+
headers: h,
|
|
706
|
+
signal,
|
|
707
|
+
});
|
|
708
|
+
if (!res.ok) {
|
|
709
|
+
throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
|
|
710
|
+
}
|
|
711
|
+
headers = h;
|
|
712
|
+
return (await res.json()) as {
|
|
713
|
+
data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[]; context_length?: number }>;
|
|
714
|
+
};
|
|
664
715
|
});
|
|
665
|
-
if (!res.ok) {
|
|
666
|
-
throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
|
|
667
|
-
}
|
|
668
|
-
headers = h;
|
|
669
|
-
return res;
|
|
670
|
-
};
|
|
671
716
|
const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
|
|
672
|
-
const
|
|
717
|
+
const payload = apiKey
|
|
673
718
|
? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
|
|
674
719
|
: await attempt(baseHeaders);
|
|
675
|
-
const payload = (await response.json()) as {
|
|
676
|
-
data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[]; context_length?: number }>;
|
|
677
|
-
};
|
|
678
720
|
const items = payload.data ?? [];
|
|
679
721
|
const discovered: Model<Api>[] = [];
|
|
680
722
|
for (const item of items) {
|
|
@@ -3,6 +3,7 @@ import { DEFAULT_SHARE_URL } from "@oh-my-pi/pi-wire";
|
|
|
3
3
|
import { SHAPE_VARIANT_NAMES } from "@oh-my-pi/snapcompact";
|
|
4
4
|
import { DEFAULT_RELAY_URL } from "../collab/protocol";
|
|
5
5
|
import { DEFAULT_STT_MODEL_KEY, STT_MODEL_OPTIONS, STT_MODEL_VALUES } from "../stt/models";
|
|
6
|
+
import { STT_SUBMIT_TRIGGER_OPTIONS, STT_SUBMIT_TRIGGER_VALUES } from "../stt/submit-trigger";
|
|
6
7
|
import { AUTO_THINKING, getConfiguredThinkingLevelMetadata, getThinkingLevelMetadata } from "../thinking";
|
|
7
8
|
import {
|
|
8
9
|
TINY_MODEL_DEVICE_DEFAULT,
|
|
@@ -1794,6 +1795,19 @@ export const SETTINGS_SCHEMA = {
|
|
|
1794
1795
|
options: STT_MODEL_OPTIONS,
|
|
1795
1796
|
},
|
|
1796
1797
|
},
|
|
1798
|
+
"stt.submitTrigger": {
|
|
1799
|
+
type: "enum",
|
|
1800
|
+
values: STT_SUBMIT_TRIGGER_VALUES,
|
|
1801
|
+
default: "never",
|
|
1802
|
+
ui: {
|
|
1803
|
+
tab: "interaction",
|
|
1804
|
+
group: "Speech",
|
|
1805
|
+
label: "Speech-to-Text Submit Trigger",
|
|
1806
|
+
description:
|
|
1807
|
+
"Choose when speech dictation automatically submits: Never, Release (2+ words), Release with complete sentence, or When I Say Submit.",
|
|
1808
|
+
options: STT_SUBMIT_TRIGGER_OPTIONS,
|
|
1809
|
+
},
|
|
1810
|
+
},
|
|
1797
1811
|
|
|
1798
1812
|
// ────────────────────────────────────────────────────────────────────────
|
|
1799
1813
|
// Context
|
|
@@ -4081,7 +4095,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
4081
4095
|
group: "Subagents",
|
|
4082
4096
|
label: "Soft Subagent Request Budget",
|
|
4083
4097
|
description:
|
|
4084
|
-
"Soft per-subagent request budget (assistant requests per run). Crossing it injects one steering notice asking the subagent to wrap up; at 1.5x the budget the run is aborted gracefully, salvaging partial output. 0 disables the guard. Bundled explore/
|
|
4098
|
+
"Soft per-subagent request budget (assistant requests per run). Crossing it injects one steering notice asking the subagent to wrap up; at 1.5x the budget the run is aborted gracefully, salvaging partial output. 0 disables the guard. Bundled explore/sonic agents use a lower built-in budget.",
|
|
4085
4099
|
options: [
|
|
4086
4100
|
{ value: "0", label: "Disabled" },
|
|
4087
4101
|
{ value: "40", label: "40 requests" },
|
package/src/debug/profiler.ts
CHANGED
|
@@ -114,7 +114,13 @@ function formatProfileAsMarkdown(profileJson: string): string {
|
|
|
114
114
|
*/
|
|
115
115
|
export async function startCpuProfile(): Promise<ProfilerSession> {
|
|
116
116
|
const v8 = await import("node:v8");
|
|
117
|
-
|
|
117
|
+
try {
|
|
118
|
+
// Enables `%GetOptimizationStatus` and friends when V8 natives are needed
|
|
119
|
+
// for ad-hoc profiling. Best-effort: Bun does not implement
|
|
120
|
+
// `setFlagsFromString` (oven-sh/bun#1702) but the CPU profiler itself
|
|
121
|
+
// works without it, so swallow the error and continue.
|
|
122
|
+
v8.setFlagsFromString("--allow-natives-syntax");
|
|
123
|
+
} catch {}
|
|
118
124
|
|
|
119
125
|
const { Session } = await import("node:inspector/promises");
|
|
120
126
|
const session = new Session();
|