@oh-my-pi/pi-coding-agent 15.7.3 → 15.7.5
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 +39 -0
- package/dist/types/config/settings-schema.d.ts +3 -22
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/local-protocol.d.ts +19 -9
- package/dist/types/internal-urls/types.d.ts +14 -0
- package/dist/types/lsp/client.d.ts +3 -0
- package/dist/types/mcp/manager.d.ts +14 -5
- package/dist/types/modes/controllers/command-controller.d.ts +2 -3
- package/dist/types/session/agent-session.d.ts +2 -6
- package/dist/types/session/shake-types.d.ts +3 -3
- package/dist/types/task/repair-args.d.ts +52 -0
- package/dist/types/tiny/models.d.ts +0 -14
- package/dist/types/tiny/title-client.d.ts +28 -2
- package/dist/types/tiny/title-protocol.d.ts +8 -9
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tui/output-block.d.ts +7 -7
- package/package.json +9 -9
- package/scripts/build-binary.ts +0 -1
- package/src/cli.ts +59 -0
- package/src/config/settings-schema.ts +3 -24
- package/src/config/settings.ts +10 -0
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/internal-urls/local-protocol.ts +23 -11
- package/src/internal-urls/types.ts +15 -0
- package/src/lsp/client.ts +28 -5
- package/src/mcp/manager.ts +87 -4
- package/src/modes/controllers/command-controller.ts +7 -39
- package/src/modes/controllers/event-controller.ts +33 -26
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/prompts/system/project-prompt.md +3 -2
- package/src/prompts/system/subagent-system-prompt.md +12 -8
- package/src/prompts/system/system-prompt.md +8 -6
- package/src/session/agent-session.ts +11 -93
- package/src/session/shake-types.ts +4 -5
- package/src/slash-commands/builtin-registry.ts +2 -4
- package/src/task/executor.ts +14 -4
- package/src/task/index.ts +3 -2
- package/src/task/repair-args.ts +117 -0
- package/src/tiny/models.ts +0 -28
- package/src/tiny/title-client.ts +133 -43
- package/src/tiny/title-protocol.ts +11 -16
- package/src/tiny/worker.ts +6 -61
- package/src/tools/ast-edit.ts +3 -0
- package/src/tools/ast-grep.ts +3 -0
- package/src/tools/find.ts +20 -6
- package/src/tools/gh.ts +1 -0
- package/src/tools/path-utils.ts +13 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/search.ts +12 -1
- package/src/tui/output-block.ts +37 -75
- package/src/utils/git.ts +9 -3
package/src/tiny/title-client.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
1
2
|
import { $env, isCompiledBinary, logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import type { Subprocess } from "bun";
|
|
2
4
|
import { settings } from "../config/settings";
|
|
3
5
|
import { tinyModelDeviceSettingToEnv } from "./device";
|
|
4
6
|
import { tinyModelDtypeSettingToEnv } from "./dtype";
|
|
@@ -12,6 +14,14 @@ import {
|
|
|
12
14
|
} from "./models";
|
|
13
15
|
import type { TinyTitleProgressEvent, TinyTitleWorkerInbound, TinyTitleWorkerOutbound } from "./title-protocol";
|
|
14
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Abstraction over the tiny-model subprocess. Modelled as a worker interface
|
|
19
|
+
* so existing callers (titles, memory completions, downloads) compose the
|
|
20
|
+
* same way; the runtime implementation is a Bun child process so
|
|
21
|
+
* `onnxruntime-node`'s NAPI finalizer never runs inside the main agent
|
|
22
|
+
* address space — that destructor segfaults Bun on Windows during shutdown
|
|
23
|
+
* (issue #1606).
|
|
24
|
+
*/
|
|
15
25
|
interface WorkerHandle {
|
|
16
26
|
send(message: TinyTitleWorkerInbound): void;
|
|
17
27
|
onMessage(handler: (message: TinyTitleWorkerOutbound) => void): () => void;
|
|
@@ -31,6 +41,12 @@ export interface TinyTitleDownloadOptions {
|
|
|
31
41
|
|
|
32
42
|
const SMOKE_TEST_TIMEOUT_MS = 5_000;
|
|
33
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Hidden subcommand on the main CLI that boots the tiny-model worker in the
|
|
46
|
+
* spawned subprocess. Kept in sync with the dispatch in `cli.ts`.
|
|
47
|
+
*/
|
|
48
|
+
export const TINY_WORKER_ARG = "--tiny-worker";
|
|
49
|
+
|
|
34
50
|
function readTinyModelSetting(path: "providers.tinyModelDevice" | "providers.tinyModelDtype"): string | undefined {
|
|
35
51
|
try {
|
|
36
52
|
const value = settings.get(path);
|
|
@@ -66,49 +82,128 @@ export function tinyWorkerEnvOverlay(
|
|
|
66
82
|
}
|
|
67
83
|
|
|
68
84
|
/**
|
|
69
|
-
* Env handed to the tiny-model
|
|
70
|
-
* vars win; otherwise the persisted `providers.tinyModelDevice` /
|
|
71
|
-
* `providers.tinyModelDtype` settings are mapped onto those vars so the
|
|
72
|
-
* env-based resolution picks them up. Resolved once at spawn
|
|
85
|
+
* Env handed to the tiny-model subprocess. The `PI_TINY_DEVICE` / `PI_TINY_DTYPE`
|
|
86
|
+
* env vars win; otherwise the persisted `providers.tinyModelDevice` /
|
|
87
|
+
* `providers.tinyModelDtype` settings are mapped onto those vars so the
|
|
88
|
+
* subprocess's env-based resolution picks them up. Resolved once at spawn
|
|
89
|
+
* (pipelines are cached for the lifetime of the subprocess).
|
|
73
90
|
*/
|
|
74
|
-
function tinyWorkerEnv(): Record<string, string>
|
|
91
|
+
function tinyWorkerEnv(): Record<string, string> {
|
|
75
92
|
const overlay = tinyWorkerEnvOverlay(
|
|
76
93
|
$env,
|
|
77
94
|
readTinyModelSetting("providers.tinyModelDevice"),
|
|
78
95
|
readTinyModelSetting("providers.tinyModelDtype"),
|
|
79
96
|
);
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
const base = $env as Record<string, string | undefined>;
|
|
98
|
+
const merged: Record<string, string> = {};
|
|
99
|
+
for (const key in base) {
|
|
100
|
+
const value = base[key];
|
|
101
|
+
if (typeof value === "string") merged[key] = value;
|
|
102
|
+
}
|
|
103
|
+
for (const key in overlay) merged[key] = overlay[key];
|
|
104
|
+
return merged;
|
|
82
105
|
}
|
|
83
106
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Resolve the argv used to relaunch the agent CLI into tiny-worker mode. In a
|
|
109
|
+
* compiled binary the entry point is the binary itself; in dev/source the
|
|
110
|
+
* spawned `bun` needs the absolute path to `cli.ts` so it can resolve module
|
|
111
|
+
* imports against the on-disk source tree.
|
|
112
|
+
*/
|
|
113
|
+
function tinyWorkerSpawnCmd(): string[] {
|
|
114
|
+
if (isCompiledBinary()) return [process.execPath, TINY_WORKER_ARG];
|
|
115
|
+
const cliPath = path.resolve(import.meta.dir, "..", "cli.ts");
|
|
116
|
+
return [process.execPath, cliPath, TINY_WORKER_ARG];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface SpawnedSubprocess {
|
|
120
|
+
proc: Subprocess<"ignore", "inherit", "inherit">;
|
|
121
|
+
inbound: Set<(message: TinyTitleWorkerOutbound) => void>;
|
|
122
|
+
errors: Set<(error: Error) => void>;
|
|
123
|
+
/**
|
|
124
|
+
* Flipped to `true` by {@link wrapSubprocess}'s `terminate()` right
|
|
125
|
+
* before it SIGKILLs the child so `onExit` can distinguish the
|
|
126
|
+
* expected hard-kill from a crash/OOM/external signal. Only the
|
|
127
|
+
* latter is surfaced as a worker error.
|
|
128
|
+
*/
|
|
129
|
+
intentionalExit: { value: boolean };
|
|
90
130
|
}
|
|
91
131
|
|
|
92
|
-
|
|
93
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Spawn the tiny-model worker as a subprocess. Exported for tests and the
|
|
134
|
+
* smoke probe; production callers go through {@link spawnTinyTitleWorker}
|
|
135
|
+
* which wraps the result in a {@link WorkerHandle}.
|
|
136
|
+
*/
|
|
137
|
+
export function createTinyTitleSubprocess(): SpawnedSubprocess {
|
|
138
|
+
const inbound = new Set<(message: TinyTitleWorkerOutbound) => void>();
|
|
139
|
+
const errors = new Set<(error: Error) => void>();
|
|
140
|
+
const intentionalExit = { value: false };
|
|
141
|
+
const proc = Bun.spawn({
|
|
142
|
+
cmd: tinyWorkerSpawnCmd(),
|
|
143
|
+
env: tinyWorkerEnv(),
|
|
144
|
+
stdin: "ignore",
|
|
145
|
+
stdout: "inherit",
|
|
146
|
+
stderr: "inherit",
|
|
147
|
+
serialization: "advanced",
|
|
148
|
+
windowsHide: true,
|
|
149
|
+
ipc(message) {
|
|
150
|
+
for (const handler of inbound) handler(message as TinyTitleWorkerOutbound);
|
|
151
|
+
},
|
|
152
|
+
onExit(_proc, exitCode, signalCode) {
|
|
153
|
+
// Clean exit. The child only exits via SIGKILL in practice, but
|
|
154
|
+
// treat code 0 as a no-op for symmetry.
|
|
155
|
+
if (exitCode === 0) return;
|
|
156
|
+
// `exitCode === null` + non-null `signalCode` covers both the
|
|
157
|
+
// expected SIGKILL from `terminate()` AND external kills
|
|
158
|
+
// (SIGSEGV from a native crash, SIGKILL from the OOM killer, an
|
|
159
|
+
// operator `kill -9`, etc.). Swallow only the expected one;
|
|
160
|
+
// every other signal exit is a real worker death that must
|
|
161
|
+
// fault every in-flight request so callers don't await forever.
|
|
162
|
+
if (exitCode === null && intentionalExit.value) return;
|
|
163
|
+
const reason = exitCode !== null ? `code ${exitCode}` : `signal ${signalCode ?? "unknown"}`;
|
|
164
|
+
const err = new Error(`tiny model subprocess exited with ${reason}`);
|
|
165
|
+
for (const handler of errors) handler(err);
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
// Don't keep the parent event loop alive on account of an idle worker; the
|
|
169
|
+
// agent dispose path calls `terminate()` explicitly when shutting down.
|
|
170
|
+
proc.unref();
|
|
171
|
+
return { proc, inbound, errors, intentionalExit };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function wrapSubprocess({ proc, inbound, errors, intentionalExit }: SpawnedSubprocess): WorkerHandle {
|
|
94
175
|
return {
|
|
95
176
|
send(message) {
|
|
96
|
-
|
|
177
|
+
try {
|
|
178
|
+
proc.send(message);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
logger.debug("tiny-title: send to subprocess failed", {
|
|
181
|
+
error: error instanceof Error ? error.message : String(error),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
97
184
|
},
|
|
98
185
|
onMessage(handler) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return () => worker.removeEventListener("message", wrap);
|
|
186
|
+
inbound.add(handler);
|
|
187
|
+
return () => inbound.delete(handler);
|
|
102
188
|
},
|
|
103
189
|
onError(handler) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
};
|
|
107
|
-
worker.addEventListener("error", wrap);
|
|
108
|
-
return () => worker.removeEventListener("error", wrap);
|
|
190
|
+
errors.add(handler);
|
|
191
|
+
return () => errors.delete(handler);
|
|
109
192
|
},
|
|
110
193
|
async terminate() {
|
|
111
|
-
|
|
194
|
+
// SIGKILL: the whole point of the subprocess isolation is that the
|
|
195
|
+
// parent never runs `onnxruntime-node`'s NAPI finalizer. A polite
|
|
196
|
+
// SIGTERM lets the subprocess try to clean up, which is exactly the
|
|
197
|
+
// codepath that crashes Bun on Windows. Hard-kill instead — the
|
|
198
|
+
// model lives in process memory and the OS reclaims everything.
|
|
199
|
+
// Flip the intentional-exit flag *before* killing so `onExit` can
|
|
200
|
+
// tell this apart from a crash or external SIGKILL.
|
|
201
|
+
intentionalExit.value = true;
|
|
202
|
+
try {
|
|
203
|
+
proc.kill("SIGKILL");
|
|
204
|
+
} catch {
|
|
205
|
+
// Already gone.
|
|
206
|
+
}
|
|
112
207
|
},
|
|
113
208
|
};
|
|
114
209
|
}
|
|
@@ -126,10 +221,6 @@ function spawnInlineUnavailableWorker(error: unknown): WorkerHandle {
|
|
|
126
221
|
emit({ type: "pong", id: message.id });
|
|
127
222
|
return;
|
|
128
223
|
}
|
|
129
|
-
if (message.type === "close") {
|
|
130
|
-
emit({ type: "closed" });
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
224
|
emit({ type: "error", id: message.id, error: errorMessage });
|
|
134
225
|
});
|
|
135
226
|
},
|
|
@@ -148,9 +239,9 @@ function spawnInlineUnavailableWorker(error: unknown): WorkerHandle {
|
|
|
148
239
|
|
|
149
240
|
function spawnTinyTitleWorker(): WorkerHandle {
|
|
150
241
|
try {
|
|
151
|
-
return
|
|
242
|
+
return wrapSubprocess(createTinyTitleSubprocess());
|
|
152
243
|
} catch (error) {
|
|
153
|
-
logger.warn("Tiny title
|
|
244
|
+
logger.warn("Tiny title worker spawn failed; local titles disabled", {
|
|
154
245
|
error: error instanceof Error ? error.message : String(error),
|
|
155
246
|
});
|
|
156
247
|
return spawnInlineUnavailableWorker(error);
|
|
@@ -293,9 +384,9 @@ export class TinyTitleClient {
|
|
|
293
384
|
}
|
|
294
385
|
this.#pending.clear();
|
|
295
386
|
try {
|
|
296
|
-
worker?.
|
|
387
|
+
await worker?.terminate();
|
|
297
388
|
} catch {
|
|
298
|
-
//
|
|
389
|
+
// Already gone.
|
|
299
390
|
}
|
|
300
391
|
}
|
|
301
392
|
|
|
@@ -317,7 +408,6 @@ export class TinyTitleClient {
|
|
|
317
408
|
this.#emitProgress(message.event);
|
|
318
409
|
return;
|
|
319
410
|
}
|
|
320
|
-
if (message.type === "closed") return;
|
|
321
411
|
if (message.type === "pong") return;
|
|
322
412
|
|
|
323
413
|
const pending = this.#pending.get(message.id);
|
|
@@ -371,25 +461,25 @@ export async function smokeTestTinyTitleWorker({
|
|
|
371
461
|
}: {
|
|
372
462
|
timeoutMs?: number;
|
|
373
463
|
} = {}): Promise<void> {
|
|
374
|
-
const
|
|
464
|
+
const handle = wrapSubprocess(createTinyTitleSubprocess());
|
|
375
465
|
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
376
466
|
const timer = setTimeout(() => reject(new Error(`tiny title worker did not pong within ${timeoutMs}ms`)), timeoutMs);
|
|
377
|
-
|
|
378
|
-
const message = event.data;
|
|
467
|
+
const unsubscribeMessage = handle.onMessage(message => {
|
|
379
468
|
if (message.type === "pong") {
|
|
380
469
|
resolve();
|
|
381
470
|
return;
|
|
382
471
|
}
|
|
472
|
+
if (message.type === "log") return;
|
|
383
473
|
reject(new Error(`tiny title worker: expected pong, got ${JSON.stringify(message)}`));
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
reject(event.error instanceof Error ? event.error : new Error(event.message || "tiny title worker error"));
|
|
387
|
-
};
|
|
474
|
+
});
|
|
475
|
+
const unsubscribeError = handle.onError(reject);
|
|
388
476
|
try {
|
|
389
|
-
|
|
477
|
+
handle.send({ type: "ping", id: "smoke" } satisfies TinyTitleWorkerInbound);
|
|
390
478
|
await promise;
|
|
391
479
|
} finally {
|
|
392
480
|
clearTimeout(timer);
|
|
393
|
-
|
|
481
|
+
unsubscribeMessage();
|
|
482
|
+
unsubscribeError();
|
|
483
|
+
await handle.terminate();
|
|
394
484
|
}
|
|
395
485
|
}
|
|
@@ -30,19 +30,8 @@ export interface TinyTitleProgressEvent {
|
|
|
30
30
|
export type TinyTitleWorkerInbound =
|
|
31
31
|
| { type: "ping"; id: string }
|
|
32
32
|
| { type: "generate"; id: string; modelKey: TinyTitleLocalModelKey; message: string }
|
|
33
|
-
| {
|
|
34
|
-
|
|
35
|
-
id: string;
|
|
36
|
-
modelKey: TinyLocalModelKey;
|
|
37
|
-
prompt: string;
|
|
38
|
-
maxTokens?: number;
|
|
39
|
-
/** Optional assistant-turn prefix appended after the generation prompt to pin output format. */
|
|
40
|
-
prefill?: string;
|
|
41
|
-
/** Optional literal stop string; generation halts once it appears in the decoded tail. */
|
|
42
|
-
stop?: string;
|
|
43
|
-
}
|
|
44
|
-
| { type: "download"; id: string; modelKey: TinyLocalModelKey }
|
|
45
|
-
| { type: "close" };
|
|
33
|
+
| { type: "complete"; id: string; modelKey: TinyLocalModelKey; prompt: string; maxTokens?: number }
|
|
34
|
+
| { type: "download"; id: string; modelKey: TinyLocalModelKey };
|
|
46
35
|
|
|
47
36
|
export type TinyTitleWorkerOutbound =
|
|
48
37
|
| { type: "pong"; id: string }
|
|
@@ -51,11 +40,17 @@ export type TinyTitleWorkerOutbound =
|
|
|
51
40
|
| { type: "downloaded"; id: string }
|
|
52
41
|
| { type: "error"; id: string; error: string }
|
|
53
42
|
| { type: "progress"; id: string; event: TinyTitleProgressEvent }
|
|
54
|
-
| { type: "log"; level: "debug" | "warn" | "error"; msg: string; meta?: Record<string, unknown> }
|
|
55
|
-
| { type: "closed" };
|
|
43
|
+
| { type: "log"; level: "debug" | "warn" | "error"; msg: string; meta?: Record<string, unknown> };
|
|
56
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Wire transport between the parent (`TinyTitleClient`) and the tiny-model
|
|
47
|
+
* subprocess. The parent owns the subprocess lifecycle (graceful work, hard
|
|
48
|
+
* kill on shutdown); the protocol therefore carries no explicit close
|
|
49
|
+
* handshake — once the parent decides to terminate, it signals the OS to
|
|
50
|
+
* reap the child so `onnxruntime-node`'s NAPI finalizer never runs in any
|
|
51
|
+
* shared address space. See `title-client.ts` for the spawn/kill glue.
|
|
52
|
+
*/
|
|
57
53
|
export interface TinyTitleTransport {
|
|
58
54
|
send(message: TinyTitleWorkerOutbound): void;
|
|
59
55
|
onMessage(handler: (message: TinyTitleWorkerInbound) => void): () => void;
|
|
60
|
-
close(): void;
|
|
61
56
|
}
|
package/src/tiny/worker.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { parentPort } from "node:worker_threads";
|
|
5
4
|
import type {
|
|
6
5
|
ProgressInfo,
|
|
7
6
|
TextGenerationPipeline,
|
|
@@ -20,12 +19,7 @@ import {
|
|
|
20
19
|
type TinyTitleLocalModelSpec,
|
|
21
20
|
} from "./models";
|
|
22
21
|
import { formatTitleUserMessage, normalizeGeneratedTitle } from "./text";
|
|
23
|
-
import type {
|
|
24
|
-
TinyTitleProgressEvent,
|
|
25
|
-
TinyTitleTransport,
|
|
26
|
-
TinyTitleWorkerInbound,
|
|
27
|
-
TinyTitleWorkerOutbound,
|
|
28
|
-
} from "./title-protocol";
|
|
22
|
+
import type { TinyTitleProgressEvent, TinyTitleTransport, TinyTitleWorkerInbound } from "./title-protocol";
|
|
29
23
|
|
|
30
24
|
const TITLE_PREFILL = "<title>";
|
|
31
25
|
const TITLE_CLOSE = "</title>";
|
|
@@ -461,15 +455,14 @@ async function generateTitle(
|
|
|
461
455
|
return extractTinyTitle(output[0]?.generated_text ?? "");
|
|
462
456
|
}
|
|
463
457
|
|
|
464
|
-
function buildCompletionPrompt(generator: TextGenerationPipeline, promptText: string
|
|
458
|
+
function buildCompletionPrompt(generator: TextGenerationPipeline, promptText: string): string {
|
|
465
459
|
const chat = [{ role: "user", content: promptText }];
|
|
466
460
|
const chatTemplateOptions = {
|
|
467
461
|
add_generation_prompt: true,
|
|
468
462
|
tokenize: false,
|
|
469
463
|
enable_thinking: false,
|
|
470
464
|
};
|
|
471
|
-
|
|
472
|
-
return prefill ? `${base}${prefill}` : base;
|
|
465
|
+
return `${generator.tokenizer.apply_chat_template(chat, chatTemplateOptions)}`;
|
|
473
466
|
}
|
|
474
467
|
|
|
475
468
|
/**
|
|
@@ -484,37 +477,18 @@ async function generateCompletion(
|
|
|
484
477
|
modelKey: TinyLocalModelKey,
|
|
485
478
|
promptText: string,
|
|
486
479
|
maxTokens: number | undefined,
|
|
487
|
-
prefill?: string,
|
|
488
|
-
stop?: string,
|
|
489
480
|
): Promise<string | null> {
|
|
490
481
|
const generator = await loadPipeline(modelKey, transport, requestId);
|
|
491
|
-
const text = buildCompletionPrompt(generator, promptText
|
|
482
|
+
const text = buildCompletionPrompt(generator, promptText);
|
|
492
483
|
const requested = maxTokens ?? MEMORY_COMPLETION_MAX_NEW_TOKENS;
|
|
493
484
|
const maxNewTokens = Math.min(Math.max(1, requested), MEMORY_COMPLETION_MAX_NEW_TOKENS);
|
|
494
|
-
const transformers = stop ? await loadTransformers(transport, requestId, modelKey) : undefined;
|
|
495
485
|
const output = (await generator(text, {
|
|
496
486
|
max_new_tokens: maxNewTokens,
|
|
497
487
|
do_sample: false,
|
|
498
488
|
return_full_text: false,
|
|
499
|
-
...(transformers && stop
|
|
500
|
-
? { stopping_criteria: createStopOnTextCriteria(transformers, generator.tokenizer, stop) }
|
|
501
|
-
: {}),
|
|
502
489
|
})) as TextGenerationStringOutput;
|
|
503
|
-
const generated = output[0]?.generated_text ?? "";
|
|
504
|
-
|
|
505
|
-
// including the opening tag it pinned via `prefill`.
|
|
506
|
-
const full = `${prefill ?? ""}${generated}`.trim();
|
|
507
|
-
return full === "" ? null : full;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function releasePipelines(): void {
|
|
511
|
-
// Intentionally NOT calling `pipeline.dispose()`. transformers.js disposes the
|
|
512
|
-
// underlying onnxruntime InferenceSession, freeing native memory that Bun's
|
|
513
|
-
// worker/NAPI teardown then frees a second time — a double-free that aborts the
|
|
514
|
-
// process on quit ("malloc: pointer being freed was not allocated" /
|
|
515
|
-
// "NAPI FATAL ERROR"). The worker is torn down immediately after `close`, so the
|
|
516
|
-
// OS reclaims the model memory regardless; skipping dispose avoids the crash.
|
|
517
|
-
pipelines.clear();
|
|
490
|
+
const generated = (output[0]?.generated_text ?? "").trim();
|
|
491
|
+
return generated === "" ? null : generated;
|
|
518
492
|
}
|
|
519
493
|
|
|
520
494
|
function enqueueRequest(
|
|
@@ -548,8 +522,6 @@ async function handleQueuedRequest(
|
|
|
548
522
|
request.modelKey,
|
|
549
523
|
request.prompt,
|
|
550
524
|
request.maxTokens,
|
|
551
|
-
request.prefill,
|
|
552
|
-
request.stop,
|
|
553
525
|
);
|
|
554
526
|
transport.send({ type: "completion", id: request.id, text });
|
|
555
527
|
return;
|
|
@@ -567,33 +539,6 @@ export function startTinyTitleWorker(transport: TinyTitleTransport): void {
|
|
|
567
539
|
transport.send({ type: "pong", id: message.id });
|
|
568
540
|
return;
|
|
569
541
|
}
|
|
570
|
-
if (message.type === "close") {
|
|
571
|
-
releasePipelines();
|
|
572
|
-
transport.send({ type: "closed" });
|
|
573
|
-
transport.close();
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
542
|
enqueueRequest(transport, message);
|
|
577
543
|
});
|
|
578
544
|
}
|
|
579
|
-
|
|
580
|
-
if (!parentPort) throw new Error("tiny-title-worker: missing parentPort");
|
|
581
|
-
|
|
582
|
-
const port = parentPort;
|
|
583
|
-
const transport: TinyTitleTransport = {
|
|
584
|
-
send: (message: TinyTitleWorkerOutbound) => port.postMessage(message),
|
|
585
|
-
onMessage: handler => {
|
|
586
|
-
const wrap = (data: unknown): void => handler(data as TinyTitleWorkerInbound);
|
|
587
|
-
port.on("message", wrap);
|
|
588
|
-
return () => port.off("message", wrap);
|
|
589
|
-
},
|
|
590
|
-
close: () => {
|
|
591
|
-
try {
|
|
592
|
-
port.close();
|
|
593
|
-
} catch {
|
|
594
|
-
// Already closed.
|
|
595
|
-
}
|
|
596
|
-
},
|
|
597
|
-
};
|
|
598
|
-
|
|
599
|
-
startTinyTitleWorker(transport);
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -230,6 +230,9 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
230
230
|
rawPaths: params.paths,
|
|
231
231
|
cwd: this.session.cwd,
|
|
232
232
|
internalUrlAction: "rewrite",
|
|
233
|
+
settings: this.session.settings,
|
|
234
|
+
signal,
|
|
235
|
+
localProtocolOptions: this.session.localProtocolOptions,
|
|
233
236
|
});
|
|
234
237
|
const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = scope;
|
|
235
238
|
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -155,6 +155,9 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
155
155
|
rawPaths: params.paths,
|
|
156
156
|
cwd: this.session.cwd,
|
|
157
157
|
internalUrlAction: "search",
|
|
158
|
+
settings: this.session.settings,
|
|
159
|
+
signal,
|
|
160
|
+
localProtocolOptions: this.session.localProtocolOptions,
|
|
158
161
|
});
|
|
159
162
|
const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = scope;
|
|
160
163
|
|
package/src/tools/find.ts
CHANGED
|
@@ -192,7 +192,12 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
192
192
|
if (hasGlobPathChars(rawPattern)) {
|
|
193
193
|
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPattern}`);
|
|
194
194
|
}
|
|
195
|
-
const resource = await internalRouter.resolve(rawPattern
|
|
195
|
+
const resource = await internalRouter.resolve(rawPattern, {
|
|
196
|
+
cwd: this.session.cwd,
|
|
197
|
+
settings: this.session.settings,
|
|
198
|
+
signal,
|
|
199
|
+
localProtocolOptions: this.session.localProtocolOptions,
|
|
200
|
+
});
|
|
196
201
|
if (!resource.sourcePath) {
|
|
197
202
|
throw new ToolError(`Cannot find internal URL without a backing file: ${rawPattern}`);
|
|
198
203
|
}
|
|
@@ -443,10 +448,14 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
443
448
|
// =============================================================================
|
|
444
449
|
|
|
445
450
|
interface FindRenderArgs {
|
|
446
|
-
paths?: string[];
|
|
451
|
+
paths?: string | string[];
|
|
447
452
|
limit?: number;
|
|
448
453
|
}
|
|
449
454
|
|
|
455
|
+
function formatFindRenderPaths(paths: FindRenderArgs["paths"]): string | undefined {
|
|
456
|
+
return Array.isArray(paths) ? paths.join(", ") : paths;
|
|
457
|
+
}
|
|
458
|
+
|
|
450
459
|
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
451
460
|
|
|
452
461
|
export const findToolRenderer = {
|
|
@@ -456,7 +465,7 @@ export const findToolRenderer = {
|
|
|
456
465
|
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
457
466
|
|
|
458
467
|
const text = renderStatusLine(
|
|
459
|
-
{ icon: "pending", title: "Find", description: args.paths
|
|
468
|
+
{ icon: "pending", title: "Find", description: formatFindRenderPaths(args.paths) || "*", meta },
|
|
460
469
|
uiTheme,
|
|
461
470
|
);
|
|
462
471
|
return new Text(text, 0, 0);
|
|
@@ -493,7 +502,7 @@ export const findToolRenderer = {
|
|
|
493
502
|
{
|
|
494
503
|
icon: "success",
|
|
495
504
|
title: "Find",
|
|
496
|
-
description: args?.paths
|
|
505
|
+
description: formatFindRenderPaths(args?.paths),
|
|
497
506
|
meta: [formatCount("file", lines.length)],
|
|
498
507
|
},
|
|
499
508
|
uiTheme,
|
|
@@ -528,7 +537,7 @@ export const findToolRenderer = {
|
|
|
528
537
|
|
|
529
538
|
if (fileCount === 0) {
|
|
530
539
|
const header = renderStatusLine(
|
|
531
|
-
{ icon: "warning", title: "Find", description: args?.paths
|
|
540
|
+
{ icon: "warning", title: "Find", description: formatFindRenderPaths(args?.paths), meta: ["0 files"] },
|
|
532
541
|
uiTheme,
|
|
533
542
|
);
|
|
534
543
|
const lines = [header, formatEmptyMessage("No files found", uiTheme)];
|
|
@@ -539,7 +548,12 @@ export const findToolRenderer = {
|
|
|
539
548
|
if (details?.scopePath) meta.push(`in ${details.scopePath}`);
|
|
540
549
|
if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
|
|
541
550
|
const header = renderStatusLine(
|
|
542
|
-
{
|
|
551
|
+
{
|
|
552
|
+
icon: truncated ? "warning" : "success",
|
|
553
|
+
title: "Find",
|
|
554
|
+
description: formatFindRenderPaths(args?.paths),
|
|
555
|
+
meta,
|
|
556
|
+
},
|
|
543
557
|
uiTheme,
|
|
544
558
|
);
|
|
545
559
|
|
package/src/tools/gh.ts
CHANGED
package/src/tools/path-utils.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as url from "node:url";
|
|
5
5
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
6
|
-
import { InternalUrlRouter } from "../internal-urls";
|
|
6
|
+
import { InternalUrlRouter, type LocalProtocolOptions } from "../internal-urls";
|
|
7
7
|
import { ToolError } from "./tool-errors";
|
|
8
8
|
|
|
9
9
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
@@ -740,6 +740,12 @@ export interface ToolScopeOptions {
|
|
|
740
740
|
surfaceExactFilePaths?: boolean;
|
|
741
741
|
/** Extra hint appended to "Path not found" when stat fails and the user supplied multiple paths. */
|
|
742
742
|
multipathStatHint?: string;
|
|
743
|
+
/** Calling session's settings — forwarded to the internal-URL router so caller-aware handlers (issue://, pr://) honor it. */
|
|
744
|
+
settings?: unknown;
|
|
745
|
+
/** Caller's abort signal — forwarded to the internal-URL router. */
|
|
746
|
+
signal?: AbortSignal;
|
|
747
|
+
/** Calling session's `local://` root mapping — pins resolutions to the calling session. */
|
|
748
|
+
localProtocolOptions?: LocalProtocolOptions;
|
|
743
749
|
}
|
|
744
750
|
|
|
745
751
|
export interface ToolScopeResolution {
|
|
@@ -778,7 +784,12 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
778
784
|
if (hasGlobPathChars(rawPath)) {
|
|
779
785
|
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
780
786
|
}
|
|
781
|
-
const resource = await internalRouter.resolve(rawPath
|
|
787
|
+
const resource = await internalRouter.resolve(rawPath, {
|
|
788
|
+
cwd,
|
|
789
|
+
settings: opts.settings,
|
|
790
|
+
signal: opts.signal,
|
|
791
|
+
localProtocolOptions: opts.localProtocolOptions,
|
|
792
|
+
});
|
|
782
793
|
if (!resource.sourcePath) {
|
|
783
794
|
throw new ToolError(`Cannot ${internalUrlAction} internal URL without a backing file: ${rawPath}`);
|
|
784
795
|
}
|
package/src/tools/read.ts
CHANGED
|
@@ -2141,6 +2141,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2141
2141
|
cwd: this.session.cwd,
|
|
2142
2142
|
settings: this.session.settings,
|
|
2143
2143
|
signal,
|
|
2144
|
+
localProtocolOptions: this.session.localProtocolOptions,
|
|
2144
2145
|
});
|
|
2145
2146
|
const details: ReadToolDetails = { resolvedPath: resource.sourcePath, contentType: resource.contentType };
|
|
2146
2147
|
|
package/src/tools/search.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
|
10
10
|
import * as z from "zod/v4";
|
|
11
11
|
import { recordFileSnapshot } from "../edit/file-snapshot-store";
|
|
12
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
|
+
import type { LocalProtocolOptions } from "../internal-urls/local-protocol";
|
|
13
14
|
import { InternalUrlRouter } from "../internal-urls/router";
|
|
14
15
|
import type { InternalResource, ResolveContext } from "../internal-urls/types";
|
|
15
16
|
import type { Theme } from "../modes/theme/theme";
|
|
@@ -543,6 +544,7 @@ async function resolveInternalSearchInputs(opts: {
|
|
|
543
544
|
settings: unknown;
|
|
544
545
|
signal?: AbortSignal;
|
|
545
546
|
archiveDisplayMap: ReadonlyMap<string, string>;
|
|
547
|
+
localProtocolOptions?: LocalProtocolOptions;
|
|
546
548
|
}): Promise<InternalSearchInputResolution> {
|
|
547
549
|
const internalRouter = InternalUrlRouter.instance();
|
|
548
550
|
const paths = opts.resolvedPaths.slice();
|
|
@@ -551,7 +553,12 @@ async function resolveInternalSearchInputs(opts: {
|
|
|
551
553
|
const virtualInputIndexes = new Set<number>();
|
|
552
554
|
const immutableSourcePaths = new Set<string>();
|
|
553
555
|
let virtualScopePath: string | undefined;
|
|
554
|
-
const context: ResolveContext = {
|
|
556
|
+
const context: ResolveContext = {
|
|
557
|
+
cwd: opts.cwd,
|
|
558
|
+
settings: opts.settings,
|
|
559
|
+
signal: opts.signal,
|
|
560
|
+
localProtocolOptions: opts.localProtocolOptions,
|
|
561
|
+
};
|
|
555
562
|
|
|
556
563
|
for (let idx = 0; idx < paths.length; idx++) {
|
|
557
564
|
const rawPath = paths[idx];
|
|
@@ -674,6 +681,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
674
681
|
settings: this.session.settings,
|
|
675
682
|
signal,
|
|
676
683
|
archiveDisplayMap,
|
|
684
|
+
localProtocolOptions: this.session.localProtocolOptions,
|
|
677
685
|
});
|
|
678
686
|
const searchablePaths = internalResolution.paths;
|
|
679
687
|
const { virtualResources, virtualPathSet, virtualInputIndexes } = internalResolution;
|
|
@@ -738,6 +746,9 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
738
746
|
trackImmutableSources: true,
|
|
739
747
|
surfaceExactFilePaths: true,
|
|
740
748
|
multipathStatHint: " (`paths` entries must each exist relative to cwd)",
|
|
749
|
+
settings: this.session.settings,
|
|
750
|
+
signal,
|
|
751
|
+
localProtocolOptions: this.session.localProtocolOptions,
|
|
741
752
|
});
|
|
742
753
|
searchPath = scope.searchPath;
|
|
743
754
|
isDirectory = scope.isDirectory;
|