@oh-my-pi/pi-coding-agent 17.2.1 → 17.2.3
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 -0
- package/dist/{CHANGELOG-dj46zzrm.md → CHANGELOG-c5hpqt9r.md} +61 -0
- package/dist/cli.js +3856 -3850
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +16 -5
- package/dist/types/edit/hashline/block-resolver.d.ts +5 -5
- package/dist/types/edit/hashline/diff.d.ts +4 -4
- package/dist/types/eval/executor-base.d.ts +27 -0
- package/dist/types/eval/js/context-manager.d.ts +7 -0
- package/dist/types/eval/js/executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +64 -5
- package/dist/types/extensibility/extensions/types.d.ts +15 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +1 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
- package/dist/types/live/visualizer.d.ts +3 -1
- package/dist/types/modes/acp/acp-agent.d.ts +3 -3
- package/dist/types/modes/components/ask-dialog.d.ts +0 -1
- package/dist/types/modes/components/keybinding-hints.d.ts +1 -1
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +13 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/secrets/index.d.ts +22 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/codex-auto-reset.d.ts +182 -64
- package/dist/types/session/session-storage.d.ts +5 -2
- package/dist/types/session/session-tools.d.ts +2 -1
- package/dist/types/session/turn-recovery.d.ts +7 -1
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +27 -0
- package/dist/types/task/persisted-revive.d.ts +7 -0
- package/dist/types/tools/bash-interceptor.d.ts +1 -1
- package/dist/types/tools/browser/attach.d.ts +9 -1
- package/dist/types/tools/browser/launch.d.ts +50 -1
- package/dist/types/tools/browser/registry.d.ts +7 -0
- package/dist/types/tools/browser/shared-daemon.d.ts +26 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +8 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/shell-tokenize.d.ts +11 -0
- package/dist/types/utils/image-loading.d.ts +2 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/src/commit/agentic/index.ts +2 -1
- package/src/commit/pipeline.ts +3 -2
- package/src/config/keybindings.ts +6 -1
- package/src/config/settings-schema.ts +16 -3
- package/src/edit/hashline/block-resolver.ts +5 -5
- package/src/edit/hashline/diff.ts +22 -15
- package/src/edit/hashline/execute.ts +14 -13
- package/src/edit/renderer.ts +3 -3
- package/src/eval/executor-base.ts +39 -0
- package/src/eval/jl/executor.ts +13 -2
- package/src/eval/js/context-manager.ts +97 -14
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/py/executor.ts +55 -15
- package/src/eval/rb/executor.ts +8 -1
- package/src/extensibility/extensions/runner.ts +121 -4
- package/src/extensibility/extensions/types.ts +15 -0
- package/src/extensibility/extensions/wrapper.ts +19 -2
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +5 -5
- package/src/launch/broker.ts +59 -41
- package/src/live/visualizer.ts +8 -1
- package/src/lsp/index.ts +51 -11
- package/src/main.ts +3 -0
- package/src/modes/acp/acp-agent.ts +16 -5
- package/src/modes/components/ask-dialog.ts +65 -23
- package/src/modes/components/assistant-message.ts +5 -10
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/keybinding-hints.ts +4 -13
- package/src/modes/components/status-line/segments.ts +4 -5
- package/src/modes/components/tool-execution.ts +4 -5
- package/src/modes/controllers/input-controller.ts +18 -40
- package/src/modes/controllers/live-command-controller.ts +1 -0
- package/src/modes/controllers/mcp-command-controller.ts +28 -11
- package/src/modes/interactive-mode.ts +59 -2
- package/src/modes/theme/theme.ts +13 -6
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/sdk.ts +47 -16
- package/src/secrets/index.ts +83 -3
- package/src/secrets/obfuscator.ts +70 -15
- package/src/session/agent-session-types.ts +7 -1
- package/src/session/agent-session.ts +394 -113
- package/src/session/codex-auto-reset.ts +594 -123
- package/src/session/eval-runner.ts +2 -0
- package/src/session/messages.ts +1 -1
- package/src/session/session-maintenance.ts +50 -3
- package/src/session/session-manager.ts +4 -3
- package/src/session/session-stats.ts +14 -3
- package/src/session/session-storage.ts +47 -15
- package/src/session/session-tools.ts +30 -2
- package/src/session/turn-recovery.ts +15 -2
- package/src/slash-commands/builtin-registry.ts +21 -15
- package/src/slash-commands/helpers/reset-usage.ts +2 -0
- package/src/system-prompt.ts +13 -1
- package/src/task/executor.ts +168 -15
- package/src/task/persisted-revive.ts +26 -1
- package/src/telemetry-export.ts +8 -4
- package/src/tools/bash-interceptor.ts +85 -8
- package/src/tools/bash.ts +12 -6
- package/src/tools/browser/attach.ts +35 -10
- package/src/tools/browser/launch.ts +110 -16
- package/src/tools/browser/registry.ts +140 -31
- package/src/tools/browser/shared-daemon.ts +190 -0
- package/src/tools/browser/tab-protocol.ts +8 -0
- package/src/tools/browser/tab-supervisor.ts +31 -4
- package/src/tools/browser/tab-worker.ts +32 -2
- package/src/tools/browser.ts +1 -1
- package/src/tools/output-meta.ts +16 -1
- package/src/tools/shell-tokenize.ts +101 -0
- package/src/utils/image-loading.ts +8 -3
- package/src/utils/shell-snapshot.ts +31 -21
- package/src/web/search/providers/codex.ts +31 -6
- package/src/web/search/providers/duckduckgo.ts +183 -15
- package/src/web/search/types.ts +6 -1
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import type { ToolSession } from "../../tools";
|
|
9
9
|
import { ToolAbortError, ToolError } from "../../tools/tool-errors";
|
|
10
10
|
import { safeSend as safeSendIpc } from "../../utils/ipc";
|
|
11
|
+
import { attachSessionOwner, resolveOwnerScopedSessionKey, type SessionOwners } from "../executor-base";
|
|
11
12
|
import { shouldDetachKernel } from "../py/spawn-options";
|
|
12
13
|
import { callSessionTool, type JsStatusEvent } from "./tool-bridge";
|
|
13
14
|
import { WorkerCore } from "./worker-core";
|
|
@@ -57,10 +58,16 @@ interface JsSession {
|
|
|
57
58
|
worker: WorkerHandle;
|
|
58
59
|
state: "alive" | "dead";
|
|
59
60
|
pending: Map<string, PendingRun>;
|
|
61
|
+
ownerIds: Set<string>;
|
|
62
|
+
hasFallbackOwner: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface StartingJsSession extends SessionOwners {
|
|
66
|
+
promise: Promise<JsSession>;
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
const sessions = new Map<string, JsSession>();
|
|
63
|
-
const startingSessions = new Map<string,
|
|
70
|
+
const startingSessions = new Map<string, StartingJsSession>();
|
|
64
71
|
const resettingSessions = new Map<string, Promise<void>>();
|
|
65
72
|
// Worker startup (module-graph import + WorkerCore construction) is infrastructure
|
|
66
73
|
// cost, not user compute. Floor it independently of Bun's 5s default per-test timeout
|
|
@@ -99,6 +106,8 @@ export function setJsEvalWorkerThreadForTests(enabled: boolean): boolean {
|
|
|
99
106
|
export async function executeInVmContext(options: {
|
|
100
107
|
sessionKey: string;
|
|
101
108
|
sessionId: string;
|
|
109
|
+
/** Logical owner identifier; scopes `reset` on shared contexts and retained-worker cleanup. */
|
|
110
|
+
ownerId?: string;
|
|
102
111
|
cwd: string;
|
|
103
112
|
session: ToolSession;
|
|
104
113
|
localRoots?: Record<string, string>;
|
|
@@ -108,47 +117,55 @@ export async function executeInVmContext(options: {
|
|
|
108
117
|
timeoutMs?: number;
|
|
109
118
|
runState: VmRunState;
|
|
110
119
|
}): Promise<{ value: unknown }> {
|
|
120
|
+
const sessionKey = resolveOwnerScopedSessionKey({
|
|
121
|
+
baseKey: options.sessionKey,
|
|
122
|
+
ownerId: options.ownerId,
|
|
123
|
+
reset: options.reset === true,
|
|
124
|
+
hasSession: key => sessions.has(key) || startingSessions.has(key),
|
|
125
|
+
getOwners: key => sessions.get(key) ?? startingSessions.get(key),
|
|
126
|
+
});
|
|
111
127
|
if (options.reset) {
|
|
112
128
|
// Coalesce concurrent resets: an existing in-flight reset already
|
|
113
129
|
// produces a fresh context, so a follow-up `reset: true` cell should
|
|
114
130
|
// just wait for it rather than failing the user-visible call.
|
|
115
|
-
const inFlight = resettingSessions.get(
|
|
131
|
+
const inFlight = resettingSessions.get(sessionKey);
|
|
116
132
|
if (inFlight) await inFlight.catch(() => undefined);
|
|
117
133
|
else {
|
|
118
|
-
const resetPromise = resetVmContext(
|
|
134
|
+
const resetPromise = resetVmContext(sessionKey);
|
|
119
135
|
resettingSessions.set(
|
|
120
|
-
|
|
136
|
+
sessionKey,
|
|
121
137
|
resetPromise.then(() => undefined),
|
|
122
138
|
);
|
|
123
139
|
try {
|
|
124
140
|
await resetPromise;
|
|
125
141
|
} finally {
|
|
126
|
-
resettingSessions.delete(
|
|
142
|
+
resettingSessions.delete(sessionKey);
|
|
127
143
|
}
|
|
128
144
|
}
|
|
129
145
|
} else {
|
|
130
146
|
// Internal coordination: wait for any in-flight reset to settle and
|
|
131
147
|
// then run on the freshly-rebuilt context.
|
|
132
|
-
const inFlight = resettingSessions.get(
|
|
148
|
+
const inFlight = resettingSessions.get(sessionKey);
|
|
133
149
|
if (inFlight) await inFlight.catch(() => undefined);
|
|
134
150
|
}
|
|
135
151
|
const session = await acquireSession(
|
|
136
|
-
|
|
152
|
+
sessionKey,
|
|
137
153
|
{ cwd: options.cwd, sessionId: options.sessionId, localRoots: options.localRoots },
|
|
138
154
|
options.timeoutMs,
|
|
155
|
+
options.ownerId,
|
|
139
156
|
);
|
|
140
157
|
return await runOnce(session, options);
|
|
141
158
|
}
|
|
142
159
|
|
|
143
160
|
export async function resetVmContext(sessionKey: string): Promise<void> {
|
|
144
|
-
const session = sessions.get(sessionKey) ?? (await startingSessions.get(sessionKey)?.catch(() => undefined));
|
|
161
|
+
const session = sessions.get(sessionKey) ?? (await startingSessions.get(sessionKey)?.promise.catch(() => undefined));
|
|
145
162
|
if (!session) return;
|
|
146
163
|
sessions.delete(sessionKey);
|
|
147
164
|
await killSession(session, new ToolError("JS context reset"), { force: false });
|
|
148
165
|
}
|
|
149
166
|
|
|
150
167
|
export async function disposeAllVmContexts(): Promise<void> {
|
|
151
|
-
const pending = [...startingSessions.values()];
|
|
168
|
+
const pending = [...startingSessions.values()].map(starting => starting.promise);
|
|
152
169
|
startingSessions.clear();
|
|
153
170
|
const started = await Promise.allSettled(pending);
|
|
154
171
|
const all = [...sessions.values()];
|
|
@@ -160,6 +177,45 @@ export async function disposeAllVmContexts(): Promise<void> {
|
|
|
160
177
|
await Promise.all(all.map(session => killSession(session, new ToolError("JS context disposed"), { force: false })));
|
|
161
178
|
}
|
|
162
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Shut down retained JS contexts owned solely by `ownerId` (e.g. a subagent's
|
|
182
|
+
* private fork); shared contexts just drop the owner registration.
|
|
183
|
+
*/
|
|
184
|
+
export async function disposeVmContextsByOwner(ownerId: string): Promise<void> {
|
|
185
|
+
const toKill: JsSession[] = [];
|
|
186
|
+
for (const session of [...sessions.values()]) {
|
|
187
|
+
if (!session.ownerIds.has(ownerId)) continue;
|
|
188
|
+
if (session.ownerIds.size === 1) {
|
|
189
|
+
toKill.push(session);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
session.ownerIds.delete(ownerId);
|
|
193
|
+
}
|
|
194
|
+
const startingToKill: StartingJsSession[] = [];
|
|
195
|
+
for (const [sessionKey, starting] of [...startingSessions.entries()]) {
|
|
196
|
+
if (sessions.has(sessionKey) || !starting.ownerIds.has(ownerId)) continue;
|
|
197
|
+
if (starting.ownerIds.size === 1) {
|
|
198
|
+
startingSessions.delete(sessionKey);
|
|
199
|
+
startingToKill.push(starting);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
starting.ownerIds.delete(ownerId);
|
|
203
|
+
}
|
|
204
|
+
for (const session of toKill) {
|
|
205
|
+
if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
|
|
206
|
+
}
|
|
207
|
+
const started = await Promise.allSettled(startingToKill.map(starting => starting.promise));
|
|
208
|
+
for (const result of started) {
|
|
209
|
+
if (result.status !== "fulfilled") continue;
|
|
210
|
+
const session = result.value;
|
|
211
|
+
if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
|
|
212
|
+
toKill.push(session);
|
|
213
|
+
}
|
|
214
|
+
await Promise.all(
|
|
215
|
+
toKill.map(session => killSession(session, new ToolError("JS context disposed"), { force: false })),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
163
219
|
/**
|
|
164
220
|
* Smoke probe: spawn the JS evaluator through the worker-host entry and prove
|
|
165
221
|
* it answers the `init` handshake in a real isolated subprocess (not the inline
|
|
@@ -177,6 +233,8 @@ export async function smokeTestJsEvalWorker(): Promise<void> {
|
|
|
177
233
|
worker,
|
|
178
234
|
state: "alive",
|
|
179
235
|
pending: new Map(),
|
|
236
|
+
ownerIds: new Set(),
|
|
237
|
+
hasFallbackOwner: false,
|
|
180
238
|
};
|
|
181
239
|
try {
|
|
182
240
|
await initWorker(session, { cwd: process.cwd(), sessionId: "smoke" }, WORKER_INIT_TIMEOUT_MS);
|
|
@@ -243,15 +301,25 @@ async function runOnce(
|
|
|
243
301
|
}
|
|
244
302
|
}
|
|
245
303
|
|
|
246
|
-
async function acquireSession(
|
|
304
|
+
async function acquireSession(
|
|
305
|
+
sessionKey: string,
|
|
306
|
+
snapshot: SessionSnapshot,
|
|
307
|
+
timeoutMs?: number,
|
|
308
|
+
ownerId?: string,
|
|
309
|
+
): Promise<JsSession> {
|
|
247
310
|
const existing = sessions.get(sessionKey);
|
|
248
311
|
if (existing && existing.state === "alive") {
|
|
249
312
|
existing.sessionId = snapshot.sessionId;
|
|
250
313
|
existing.cwd = snapshot.cwd;
|
|
314
|
+
attachSessionOwner(existing, snapshot.sessionId, ownerId);
|
|
251
315
|
return existing;
|
|
252
316
|
}
|
|
253
317
|
const starting = startingSessions.get(sessionKey);
|
|
254
|
-
if (starting)
|
|
318
|
+
if (starting) {
|
|
319
|
+
attachSessionOwner(starting, snapshot.sessionId, ownerId);
|
|
320
|
+
return await starting.promise;
|
|
321
|
+
}
|
|
322
|
+
let startingSession!: StartingJsSession;
|
|
255
323
|
|
|
256
324
|
const startup = (async (): Promise<JsSession> => {
|
|
257
325
|
// Attach the message listener before sending init. Both Bun Worker messages
|
|
@@ -264,6 +332,8 @@ async function acquireSession(sessionKey: string, snapshot: SessionSnapshot, tim
|
|
|
264
332
|
worker,
|
|
265
333
|
state: "alive",
|
|
266
334
|
pending: new Map(),
|
|
335
|
+
ownerIds: new Set(),
|
|
336
|
+
hasFallbackOwner: false,
|
|
267
337
|
};
|
|
268
338
|
// Init headroom is the fixed infrastructure floor; the caller's per-cell timeout
|
|
269
339
|
// dominates when larger so users can grant more by raising `timeout` on a cell.
|
|
@@ -293,14 +363,27 @@ async function acquireSession(sessionKey: string, snapshot: SessionSnapshot, tim
|
|
|
293
363
|
session.state = "alive";
|
|
294
364
|
}
|
|
295
365
|
}
|
|
296
|
-
|
|
366
|
+
session.ownerIds = new Set(startingSession.ownerIds);
|
|
367
|
+
session.hasFallbackOwner = startingSession.hasFallbackOwner;
|
|
368
|
+
// Publish only while this startup still owns the key: owner disposal or
|
|
369
|
+
// a concurrent dispose-all may have already reaped the starting record,
|
|
370
|
+
// and publishing here would resurrect a context that was just torn down.
|
|
371
|
+
if (startingSessions.get(sessionKey) === startingSession) {
|
|
372
|
+
sessions.set(sessionKey, session);
|
|
373
|
+
}
|
|
297
374
|
return session;
|
|
298
375
|
})();
|
|
299
|
-
|
|
376
|
+
startingSession = {
|
|
377
|
+
ownerIds: new Set(),
|
|
378
|
+
hasFallbackOwner: false,
|
|
379
|
+
promise: startup,
|
|
380
|
+
};
|
|
381
|
+
attachSessionOwner(startingSession, snapshot.sessionId, ownerId);
|
|
382
|
+
startingSessions.set(sessionKey, startingSession);
|
|
300
383
|
try {
|
|
301
384
|
return await startup;
|
|
302
385
|
} finally {
|
|
303
|
-
if (startingSessions.get(sessionKey) ===
|
|
386
|
+
if (startingSessions.get(sessionKey) === startingSession) startingSessions.delete(sessionKey);
|
|
304
387
|
}
|
|
305
388
|
}
|
|
306
389
|
|
package/src/eval/js/executor.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface JsExecutorOptions {
|
|
|
19
19
|
onStatus?: (event: JsStatusEvent) => void;
|
|
20
20
|
signal?: AbortSignal;
|
|
21
21
|
sessionId: string;
|
|
22
|
+
/** Logical owner identifier; scopes `reset` on shared contexts and retained-worker cleanup. */
|
|
23
|
+
kernelOwnerId?: string;
|
|
22
24
|
reset?: boolean;
|
|
23
25
|
sessionFile?: string;
|
|
24
26
|
artifactPath?: string;
|
|
@@ -100,6 +102,7 @@ export async function executeJs(code: string, options: JsExecutorOptions): Promi
|
|
|
100
102
|
await executeInVmContext({
|
|
101
103
|
sessionKey: options.sessionId,
|
|
102
104
|
sessionId: options.sessionId,
|
|
105
|
+
ownerId: options.kernelOwnerId,
|
|
103
106
|
cwd: options.cwd ?? options.session.cwd,
|
|
104
107
|
session: options.session,
|
|
105
108
|
localRoots: options.localRoots,
|
package/src/eval/js/index.ts
CHANGED
package/src/eval/py/executor.ts
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
getRemainingTimeoutMs,
|
|
14
14
|
isCancellationError,
|
|
15
15
|
isTimedOutCancellation,
|
|
16
|
+
resolveOwnerScopedSessionKey,
|
|
17
|
+
type SessionOwners,
|
|
16
18
|
waitForPromiseWithCancellation,
|
|
17
19
|
} from "../executor-base";
|
|
18
20
|
import type { JsStatusEvent } from "../js/shared/types";
|
|
@@ -154,8 +156,12 @@ interface PythonSession {
|
|
|
154
156
|
hasFallbackOwner: boolean;
|
|
155
157
|
}
|
|
156
158
|
|
|
159
|
+
interface StartingPythonSession extends SessionOwners {
|
|
160
|
+
promise: Promise<PythonSession>;
|
|
161
|
+
}
|
|
162
|
+
|
|
157
163
|
const sessions = new Map<string, PythonSession>();
|
|
158
|
-
const startingSessions = new Map<string,
|
|
164
|
+
const startingSessions = new Map<string, StartingPythonSession>();
|
|
159
165
|
const resettingSessions = new Map<string, Promise<void>>();
|
|
160
166
|
|
|
161
167
|
function normalizeSessionCwd(cwd: string): string {
|
|
@@ -252,10 +258,10 @@ async function acquireSession(
|
|
|
252
258
|
}
|
|
253
259
|
const starting = startingSessions.get(sessionKey);
|
|
254
260
|
if (starting) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return session;
|
|
261
|
+
attachSessionOwner(starting, sessionId, options.kernelOwnerId);
|
|
262
|
+
return await starting.promise;
|
|
258
263
|
}
|
|
264
|
+
let startingSession!: StartingPythonSession;
|
|
259
265
|
const startup = (async () => {
|
|
260
266
|
const kernel = await startKernel(cwd, options);
|
|
261
267
|
const session: PythonSession = {
|
|
@@ -264,19 +270,28 @@ async function acquireSession(
|
|
|
264
270
|
cwd,
|
|
265
271
|
kernel,
|
|
266
272
|
generation: 0,
|
|
267
|
-
ownerIds: new Set(),
|
|
268
|
-
hasFallbackOwner:
|
|
273
|
+
ownerIds: new Set(startingSession.ownerIds),
|
|
274
|
+
hasFallbackOwner: startingSession.hasFallbackOwner,
|
|
269
275
|
};
|
|
270
|
-
|
|
276
|
+
// Publish only while this startup still owns the key: owner disposal or
|
|
277
|
+
// a concurrent dispose-all may have already reaped the starting record,
|
|
278
|
+
// and publishing here would resurrect a kernel that was just torn down.
|
|
279
|
+
if (startingSessions.get(sessionKey) === startingSession) {
|
|
280
|
+
sessions.set(sessionKey, session);
|
|
281
|
+
}
|
|
271
282
|
return session;
|
|
272
283
|
})();
|
|
273
|
-
|
|
284
|
+
startingSession = {
|
|
285
|
+
ownerIds: new Set(),
|
|
286
|
+
hasFallbackOwner: false,
|
|
287
|
+
promise: startup,
|
|
288
|
+
};
|
|
289
|
+
attachSessionOwner(startingSession, sessionId, options.kernelOwnerId);
|
|
290
|
+
startingSessions.set(sessionKey, startingSession);
|
|
274
291
|
try {
|
|
275
|
-
|
|
276
|
-
attachSessionOwner(session, sessionId, options.kernelOwnerId);
|
|
277
|
-
return session;
|
|
292
|
+
return await startup;
|
|
278
293
|
} finally {
|
|
279
|
-
if (startingSessions.get(sessionKey) ===
|
|
294
|
+
if (startingSessions.get(sessionKey) === startingSession) startingSessions.delete(sessionKey);
|
|
280
295
|
}
|
|
281
296
|
}
|
|
282
297
|
|
|
@@ -370,7 +385,8 @@ async function acquireLiveSessionKernel(
|
|
|
370
385
|
}
|
|
371
386
|
|
|
372
387
|
async function resetSession(sessionKey: string): Promise<void> {
|
|
373
|
-
const existing =
|
|
388
|
+
const existing =
|
|
389
|
+
sessions.get(sessionKey) ?? (await startingSessions.get(sessionKey)?.promise.catch(() => undefined));
|
|
374
390
|
if (!existing) return;
|
|
375
391
|
existing.generation += 1;
|
|
376
392
|
sessions.delete(sessionKey);
|
|
@@ -382,7 +398,7 @@ async function resetSession(sessionKey: string): Promise<void> {
|
|
|
382
398
|
// ---------------------------------------------------------------------------
|
|
383
399
|
|
|
384
400
|
export async function disposeAllKernelSessions(): Promise<void> {
|
|
385
|
-
const pending = [...startingSessions.values()];
|
|
401
|
+
const pending = [...startingSessions.values()].map(starting => starting.promise);
|
|
386
402
|
startingSessions.clear();
|
|
387
403
|
const started = await Promise.allSettled(pending);
|
|
388
404
|
const all = [...sessions.entries()];
|
|
@@ -422,10 +438,28 @@ export async function disposeKernelSessionsByOwner(ownerId: string): Promise<voi
|
|
|
422
438
|
}
|
|
423
439
|
session.ownerIds.delete(ownerId);
|
|
424
440
|
}
|
|
441
|
+
const startingToShutdown: StartingPythonSession[] = [];
|
|
442
|
+
for (const [sessionKey, starting] of [...startingSessions.entries()]) {
|
|
443
|
+
if (sessions.has(sessionKey) || !starting.ownerIds.has(ownerId)) continue;
|
|
444
|
+
if (starting.ownerIds.size === 1) {
|
|
445
|
+
startingSessions.delete(sessionKey);
|
|
446
|
+
startingToShutdown.push(starting);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
starting.ownerIds.delete(ownerId);
|
|
450
|
+
}
|
|
425
451
|
for (const session of toShutdown) {
|
|
426
452
|
session.generation += 1;
|
|
427
453
|
if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
|
|
428
454
|
}
|
|
455
|
+
const started = await Promise.allSettled(startingToShutdown.map(starting => starting.promise));
|
|
456
|
+
for (const result of started) {
|
|
457
|
+
if (result.status !== "fulfilled") continue;
|
|
458
|
+
const session = result.value;
|
|
459
|
+
session.generation += 1;
|
|
460
|
+
if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
|
|
461
|
+
toShutdown.push(session);
|
|
462
|
+
}
|
|
429
463
|
const results = await Promise.allSettled(toShutdown.map(session => shutdownInvalidatedSession(session)));
|
|
430
464
|
for (let i = 0; i < toShutdown.length; i += 1) {
|
|
431
465
|
const session = toShutdown[i];
|
|
@@ -503,7 +537,13 @@ async function executePerCall(code: string, cwd: string, options: PythonExecutor
|
|
|
503
537
|
|
|
504
538
|
async function executeOnSession(code: string, cwd: string, options: PythonExecutorOptions): Promise<PythonResult> {
|
|
505
539
|
const sessionId = options.sessionId ?? `session:${cwd}`;
|
|
506
|
-
const sessionKey =
|
|
540
|
+
const sessionKey = resolveOwnerScopedSessionKey({
|
|
541
|
+
baseKey: buildSessionKey(sessionId, cwd, options.interpreter),
|
|
542
|
+
ownerId: options.kernelOwnerId,
|
|
543
|
+
reset: options.reset === true,
|
|
544
|
+
hasSession: key => sessions.has(key) || startingSessions.has(key),
|
|
545
|
+
getOwners: key => sessions.get(key) ?? startingSessions.get(key),
|
|
546
|
+
});
|
|
507
547
|
if (options.bridge && !options.bridgeSessionId) {
|
|
508
548
|
options.bridgeSessionId = sessionId;
|
|
509
549
|
}
|
package/src/eval/rb/executor.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getRemainingTimeoutMs,
|
|
14
14
|
isCancellationError,
|
|
15
15
|
isTimedOutCancellation,
|
|
16
|
+
resolveOwnerScopedSessionKey,
|
|
16
17
|
waitForPromiseWithCancellation,
|
|
17
18
|
} from "../executor-base";
|
|
18
19
|
import type { JsStatusEvent } from "../js/shared/types";
|
|
@@ -407,7 +408,13 @@ async function ensureToolBridge(options: RubyExecutorOptions): Promise<void> {
|
|
|
407
408
|
|
|
408
409
|
async function executeOnSession(code: string, cwd: string, options: RubyExecutorOptions): Promise<RubyResult> {
|
|
409
410
|
const sessionId = options.sessionId ?? `session:${cwd}`;
|
|
410
|
-
const sessionKey =
|
|
411
|
+
const sessionKey = resolveOwnerScopedSessionKey({
|
|
412
|
+
baseKey: buildSessionKey(sessionId, cwd, options.interpreter),
|
|
413
|
+
ownerId: options.kernelOwnerId,
|
|
414
|
+
reset: options.reset === true,
|
|
415
|
+
hasSession: key => sessions.has(key) || startingSessions.has(key),
|
|
416
|
+
getOwners: key => sessions.get(key) ?? startingSessions.get(key),
|
|
417
|
+
});
|
|
411
418
|
if (options.bridge && !options.bridgeSessionId) {
|
|
412
419
|
options.bridgeSessionId = sessionId;
|
|
413
420
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extension runner - executes extensions and manages their lifecycle.
|
|
3
3
|
*/
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AgentMessage,
|
|
6
|
+
AgentTool,
|
|
7
|
+
AgentToolContext,
|
|
8
|
+
AgentToolResult,
|
|
9
|
+
AgentToolUpdateCallback,
|
|
10
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
5
11
|
import type { CredentialDisabledEvent, ImageContent, Model, ProviderResponseMetadata } from "@oh-my-pi/pi-ai";
|
|
6
12
|
import type { KeyId } from "@oh-my-pi/pi-tui";
|
|
7
13
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
@@ -407,10 +413,72 @@ export class ExtensionRunner {
|
|
|
407
413
|
return this.#emittedToolCalls.delete(`${toolCallId}:${toolName}`);
|
|
408
414
|
}
|
|
409
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Resolves a tool NAME to its native built-in implementation (the pre-extension-override,
|
|
418
|
+
* unwrapped tool) plus a factory for the `AgentToolContext` that native tool expects, or
|
|
419
|
+
* undefined when no native built-in of that name exists. Set by the SDK; backs same-tool
|
|
420
|
+
* `invokeTool`. The context factory is the same one the agent loop uses for tool execution, so a
|
|
421
|
+
* delegated native call sees the ordinary session tool context (ui, cwd, snapshot state, etc.).
|
|
422
|
+
*/
|
|
423
|
+
#nativeToolResolver?: (name: string) => { tool: AgentTool; makeContext: () => AgentToolContext } | undefined;
|
|
424
|
+
|
|
425
|
+
/** Wires the native-tool resolver used by {@link invokeNativeTool}. */
|
|
426
|
+
setNativeToolResolver(
|
|
427
|
+
resolve: (name: string) => { tool: AgentTool; makeContext: () => AgentToolContext } | undefined,
|
|
428
|
+
): void {
|
|
429
|
+
this.#nativeToolResolver = resolve;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Whether a native built-in of `name` is available to delegate to. */
|
|
433
|
+
hasNativeTool(name: string): boolean {
|
|
434
|
+
return this.#nativeToolResolver?.(name) !== undefined;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Run the native built-in of `name` with `params` and return its result — the delegation target
|
|
439
|
+
* of a same-tool `ctx.invokeTool`. Calls the unwrapped native `execute` directly with the loop's
|
|
440
|
+
* ordinary tool context, so it inherits the caller's already-granted approval (the caller is the
|
|
441
|
+
* same tool) rather than re-running the gate. `depth` guards a wrapper that recurses into itself;
|
|
442
|
+
* it is per call chain (threaded from the caller), not session-global, so concurrent independent
|
|
443
|
+
* delegations do not interfere.
|
|
444
|
+
*/
|
|
445
|
+
async invokeNativeTool<TDetails = unknown>(
|
|
446
|
+
name: string,
|
|
447
|
+
params: Record<string, unknown>,
|
|
448
|
+
options?: {
|
|
449
|
+
signal?: AbortSignal;
|
|
450
|
+
onUpdate?: AgentToolUpdateCallback<TDetails>;
|
|
451
|
+
depth?: number;
|
|
452
|
+
/**
|
|
453
|
+
* The caller tool's own context. Reused for the native call so metadata the native tool
|
|
454
|
+
* reads — `toolCall` (write/edit LSP batch flushing) and provider metadata /
|
|
455
|
+
* `providerSafetyApproved` (computer) — is preserved. Falls back to a fresh session tool
|
|
456
|
+
* context only when the caller had none.
|
|
457
|
+
*/
|
|
458
|
+
callerContext?: AgentToolContext;
|
|
459
|
+
},
|
|
460
|
+
): Promise<AgentToolResult<TDetails>> {
|
|
461
|
+
const resolved = this.#nativeToolResolver?.(name);
|
|
462
|
+
if (!resolved) throw new Error(`invokeTool: no native built-in named "${name}" to delegate to`);
|
|
463
|
+
const depth = options?.depth ?? 0;
|
|
464
|
+
if (depth >= 8) {
|
|
465
|
+
throw new Error(`invokeTool: delegation depth exceeded 8 (recursive invokeTool for "${name}"?)`);
|
|
466
|
+
}
|
|
467
|
+
const toolCallId = `invoke-${name}-${Date.now().toString(36)}-${depth}`;
|
|
468
|
+
return (await resolved.tool.execute(
|
|
469
|
+
toolCallId,
|
|
470
|
+
params as never,
|
|
471
|
+
options?.signal,
|
|
472
|
+
options?.onUpdate as never,
|
|
473
|
+
options?.callerContext ?? resolved.makeContext(),
|
|
474
|
+
)) as AgentToolResult<TDetails>;
|
|
475
|
+
}
|
|
476
|
+
|
|
410
477
|
constructor(
|
|
411
478
|
private readonly extensions: Extension[],
|
|
412
479
|
private readonly runtime: ExtensionRuntime,
|
|
413
|
-
|
|
480
|
+
/** Ignored: `cwd` is always read live via the `cwd` getter below, not cached here. */
|
|
481
|
+
_initialCwd: string,
|
|
414
482
|
private readonly sessionManager: SessionManager,
|
|
415
483
|
private readonly modelRegistry: ModelRegistry,
|
|
416
484
|
getMemory?: () => MemoryRuntimeContext | undefined,
|
|
@@ -423,6 +491,24 @@ export class ExtensionRunner {
|
|
|
423
491
|
this.#getAsyncJobSnapshotFn = getAsyncJobSnapshot ?? (() => null);
|
|
424
492
|
}
|
|
425
493
|
|
|
494
|
+
/**
|
|
495
|
+
* Live session directory, not a session-start snapshot: `/move`
|
|
496
|
+
* (`SessionManager.moveTo()`) relocates the owning session by updating
|
|
497
|
+
* `sessionManager`'s own `#cwd`, not a process-global. Reading it here
|
|
498
|
+
* via the getter — instead of caching the constructor-time value in a
|
|
499
|
+
* field — keeps every `ExtensionContext` built below in sync with this
|
|
500
|
+
* session's actual, current directory. Deliberately `sessionManager.getCwd()`
|
|
501
|
+
* rather than `getProjectDir()`: the latter is a single process-wide value
|
|
502
|
+
* that only the interactive TUI's `/move` handler happens to also update
|
|
503
|
+
* (`InteractiveModeContext#applyCwdChange`) — an SDK/ACP host running
|
|
504
|
+
* several concurrent sessions each with their own `cwd` (see
|
|
505
|
+
* `CreateAgentSessionOptions.cwd`) must never have one session's move
|
|
506
|
+
* leak into another's `ctx.cwd` by reading a shared global.
|
|
507
|
+
*/
|
|
508
|
+
get cwd(): string {
|
|
509
|
+
return this.sessionManager.getCwd();
|
|
510
|
+
}
|
|
511
|
+
|
|
426
512
|
initialize(
|
|
427
513
|
actions: ExtensionActions,
|
|
428
514
|
contextActions: ExtensionContextActions,
|
|
@@ -728,8 +814,27 @@ export class ExtensionRunner {
|
|
|
728
814
|
return undefined;
|
|
729
815
|
}
|
|
730
816
|
|
|
731
|
-
/**
|
|
732
|
-
|
|
817
|
+
/**
|
|
818
|
+
* Creates an extension context, optionally scoped to a provider request model.
|
|
819
|
+
*
|
|
820
|
+
* `delegation` wires the same-tool `ctx.invokeTool` for a re-registered built-in: when `toolName`
|
|
821
|
+
* names an existing native built-in, the context carries an `invokeTool` that runs it (see
|
|
822
|
+
* {@link invokeNativeTool}). The rest inherits the wrapper's own call so a bare
|
|
823
|
+
* `ctx.invokeTool(params)` behaves like the outer call — `context` preserves `toolCall`/provider
|
|
824
|
+
* metadata, `signal`/`onUpdate` default to the wrapper's own channels so aborting the outer tool
|
|
825
|
+
* call stops the native one and native progress still streams, and `depth` bounds recursion per
|
|
826
|
+
* call chain. Explicit options passed to `invokeTool` override the inherited `signal`/`onUpdate`.
|
|
827
|
+
*/
|
|
828
|
+
createContext(
|
|
829
|
+
model?: Model,
|
|
830
|
+
delegation?: {
|
|
831
|
+
toolName: string;
|
|
832
|
+
depth?: number;
|
|
833
|
+
context?: AgentToolContext;
|
|
834
|
+
signal?: AbortSignal;
|
|
835
|
+
onUpdate?: AgentToolUpdateCallback;
|
|
836
|
+
},
|
|
837
|
+
): ExtensionContext {
|
|
733
838
|
const getModel = model ? () => model : this.#getModel;
|
|
734
839
|
return {
|
|
735
840
|
ui: this.#uiContext,
|
|
@@ -754,6 +859,18 @@ export class ExtensionRunner {
|
|
|
754
859
|
setInterval: (callback, ms, ...args) => this.#managedTimers.setInterval(callback, ms, ...args),
|
|
755
860
|
setTimeout: (callback, ms, ...args) => this.#managedTimers.setTimeout(callback, ms, ...args),
|
|
756
861
|
clearTimer: timer => this.#managedTimers.clear(timer),
|
|
862
|
+
invokeTool:
|
|
863
|
+
delegation !== undefined && this.hasNativeTool(delegation.toolName)
|
|
864
|
+
? (params, options) =>
|
|
865
|
+
this.invokeNativeTool(delegation.toolName, params, {
|
|
866
|
+
// Inherit the wrapper's own channels so a bare `ctx.invokeTool(params)` aborts
|
|
867
|
+
// and streams with the outer call. Explicit options win.
|
|
868
|
+
signal: options?.signal ?? delegation.signal,
|
|
869
|
+
onUpdate: options?.onUpdate ?? delegation.onUpdate,
|
|
870
|
+
depth: (delegation.depth ?? 0) + 1,
|
|
871
|
+
callerContext: delegation.context,
|
|
872
|
+
})
|
|
873
|
+
: undefined,
|
|
757
874
|
};
|
|
758
875
|
}
|
|
759
876
|
|
|
@@ -464,6 +464,21 @@ export interface ExtensionContext {
|
|
|
464
464
|
setTimeout(callback: (...args: unknown[]) => void, ms?: number, ...args: unknown[]): Timer;
|
|
465
465
|
/** Clear a timer scheduled via {@link setInterval} or {@link setTimeout}. */
|
|
466
466
|
clearTimer(timer: Timer): void;
|
|
467
|
+
/**
|
|
468
|
+
* Run the NATIVE built-in implementation of the tool this handler re-registered, with `params`,
|
|
469
|
+
* and return its result. Lets a tool that re-registers a built-in (e.g. wrapping `write` to add
|
|
470
|
+
* logging or a policy check) delegate to the original instead of reimplementing it — the native
|
|
471
|
+
* tool performs its own side effects and internal bookkeeping.
|
|
472
|
+
*
|
|
473
|
+
* Delegation is same-tool only: it invokes the built-in of the SAME name as the registering tool,
|
|
474
|
+
* never an arbitrary target, so it cannot escalate past the approval already granted for this
|
|
475
|
+
* call. Present only when a native built-in of that name exists (undefined otherwise, e.g. for a
|
|
476
|
+
* net-new tool that shadows no built-in). Recursion is depth-guarded per call chain.
|
|
477
|
+
*/
|
|
478
|
+
invokeTool?<TDetails = unknown>(
|
|
479
|
+
params: Record<string, unknown>,
|
|
480
|
+
options?: { signal?: AbortSignal; onUpdate?: AgentToolUpdateCallback<TDetails> },
|
|
481
|
+
): Promise<AgentToolResult<TDetails>>;
|
|
467
482
|
}
|
|
468
483
|
|
|
469
484
|
/**
|
|
@@ -64,9 +64,26 @@ export class RegisteredToolAdapter implements AgentTool<any, any, any> {
|
|
|
64
64
|
params: any,
|
|
65
65
|
signal?: AbortSignal,
|
|
66
66
|
onUpdate?: AgentToolUpdateCallback<any>,
|
|
67
|
-
|
|
67
|
+
context?: AgentToolContext,
|
|
68
68
|
) {
|
|
69
|
-
|
|
69
|
+
// Bind the extension context to this tool's own name so `ctx.invokeTool` delegates to the
|
|
70
|
+
// native built-in of the same name (present only when this tool re-registers a built-in). The
|
|
71
|
+
// wrapper's own context, abort signal, and progress callback are inherited by the delegated
|
|
72
|
+
// call, so a bare `ctx.invokeTool(params)` keeps the caller's `toolCall`/provider metadata
|
|
73
|
+
// (write/edit LSP batching, computer safety acknowledgement), stops when the outer call is
|
|
74
|
+
// aborted, and still streams native progress.
|
|
75
|
+
return this.registeredTool.definition.execute(
|
|
76
|
+
toolCallId,
|
|
77
|
+
params,
|
|
78
|
+
signal,
|
|
79
|
+
onUpdate,
|
|
80
|
+
this.runner.createContext(undefined, {
|
|
81
|
+
toolName: this.registeredTool.definition.name,
|
|
82
|
+
context,
|
|
83
|
+
signal,
|
|
84
|
+
onUpdate,
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
70
87
|
}
|
|
71
88
|
}
|
|
72
89
|
|
|
@@ -1416,11 +1416,11 @@ export function getPackageDir(): string {
|
|
|
1416
1416
|
}
|
|
1417
1417
|
|
|
1418
1418
|
// Legacy pi's `@earendil-works/pi-coding-agent` re-exported `estimateTokens`
|
|
1419
|
-
// from its package root (via `./core/compaction/index.ts`). In
|
|
1420
|
-
// `@oh-my-pi/pi-agent-core/compaction`, and the coding-agent
|
|
1421
|
-
// not forward
|
|
1422
|
-
// check during validation (
|
|
1423
|
-
export { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
|
|
1419
|
+
// and `compact` from its package root (via `./core/compaction/index.ts`). In
|
|
1420
|
+
// omp both live in `@oh-my-pi/pi-agent-core/compaction`, and the coding-agent
|
|
1421
|
+
// barrel below does not forward them, so legacy extensions importing either
|
|
1422
|
+
// fail Bun's static export check during validation (issues #6583, #7174).
|
|
1423
|
+
export { compact, estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
|
|
1424
1424
|
|
|
1425
1425
|
// Same barrel gap for two more legacy package-root exports: pi re-exported the
|
|
1426
1426
|
// `CONFIG_DIR_NAME` constant and the CLI parser `parseArgs`. In omp
|