@oh-my-pi/pi-coding-agent 17.1.6 → 17.1.8
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 +58 -0
- package/dist/{CHANGELOG-x9zt79k8.md → CHANGELOG-vt8ene9g.md} +58 -0
- package/dist/cli.js +4309 -7287
- package/dist/template-dys3vk5b.js +1671 -0
- package/dist/template-f8wx9vfn.css +1355 -0
- package/dist/template-qat058wr.html +55 -0
- package/dist/tool-views.generated-jdfmzwmn.js +35 -0
- package/dist/types/advisor/advise-tool.d.ts +0 -7
- package/dist/types/advisor/runtime.d.ts +0 -9
- package/dist/types/async/job-manager.d.ts +11 -0
- package/dist/types/cleanse/agent.d.ts +19 -0
- package/dist/types/cleanse/balance.d.ts +7 -0
- package/dist/types/cleanse/checkers.d.ts +13 -0
- package/dist/types/cleanse/index.d.ts +16 -0
- package/dist/types/cleanse/loop.d.ts +16 -0
- package/dist/types/cleanse/parsers.d.ts +13 -0
- package/dist/types/cleanse/progress.d.ts +14 -0
- package/dist/types/cleanse/types.d.ts +64 -0
- package/dist/types/commands/cleanse.d.ts +23 -0
- package/dist/types/config/settings-schema.d.ts +16 -1
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/export/html/index.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -3
- package/dist/types/session/agent-session-types.d.ts +5 -5
- package/dist/types/session/agent-session.d.ts +26 -1
- package/dist/types/session/async-job-delivery.d.ts +8 -0
- package/dist/types/session/model-controls.d.ts +4 -4
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/turn-recovery.d.ts +1 -1
- package/dist/types/task/isolation-ownership.d.ts +34 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
- package/dist/types/tools/browser/tab-worker.d.ts +1 -4
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/tui/output-block.d.ts +5 -5
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/package.json +16 -12
- package/scripts/bundle-dist.ts +3 -1
- package/src/advisor/advise-tool.ts +0 -11
- package/src/advisor/runtime.ts +0 -12
- package/src/async/job-manager.ts +30 -7
- package/src/cleanse/agent.ts +226 -0
- package/src/cleanse/balance.ts +79 -0
- package/src/cleanse/checkers.ts +996 -0
- package/src/cleanse/index.ts +190 -0
- package/src/cleanse/loop.ts +51 -0
- package/src/cleanse/parsers.ts +726 -0
- package/src/cleanse/progress.ts +50 -0
- package/src/cleanse/prompts/assignment.md +47 -0
- package/src/cleanse/types.ts +72 -0
- package/src/cli/update-cli.ts +3 -0
- package/src/cli/usage-cli.ts +29 -4
- package/src/cli/worktree-cli.ts +28 -11
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +2 -1
- package/src/commands/cleanse.ts +45 -0
- package/src/config/settings-schema.ts +15 -1
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/discovery/claude-plugins.ts +144 -34
- package/src/export/html/index.ts +17 -10
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/launch/broker.ts +14 -4
- package/src/modes/acp/acp-agent.ts +12 -9
- package/src/modes/acp/acp-event-mapper.ts +38 -4
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/settings-selector.ts +9 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/extension-ui-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/interactive-mode.ts +36 -47
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/prompts/goals/guided-goal-interview.md +41 -6
- package/src/prompts/system/vibe-mode-active.md +4 -1
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/goal.md +1 -1
- package/src/sdk.ts +47 -50
- package/src/session/agent-session-types.ts +5 -5
- package/src/session/agent-session.ts +151 -11
- package/src/session/async-job-delivery.ts +8 -0
- package/src/session/model-controls.ts +9 -9
- package/src/session/session-advisors.ts +2 -7
- package/src/session/session-history-format.ts +31 -6
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +158 -52
- package/src/session/streaming-output.ts +18 -6
- package/src/session/turn-recovery.ts +4 -4
- package/src/slash-commands/builtin-registry.ts +74 -2
- package/src/task/isolation-ownership.ts +106 -0
- package/src/task/worktree.ts +8 -0
- package/src/tools/ask.ts +3 -3
- package/src/tools/ast-edit.ts +9 -2
- package/src/tools/bash.ts +16 -9
- package/src/tools/browser/cmux/cmux-tab.ts +9 -14
- package/src/tools/browser/tab-worker.ts +12 -35
- package/src/tools/browser.ts +2 -2
- package/src/tools/gh-renderer.ts +3 -3
- package/src/tools/index.ts +46 -17
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +87 -13
- package/src/tools/write.ts +51 -7
- package/src/tools/xdev.ts +198 -210
- package/src/tui/code-cell.ts +4 -4
- package/src/tui/output-block.ts +25 -8
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/shell-snapshot-fn-env.sh +5 -2
- package/src/web/search/render.ts +2 -2
- package/dist/types/goals/guided-setup.d.ts +0 -30
- package/src/goals/guided-setup.ts +0 -171
- package/src/prompts/goals/guided-goal-system.md +0 -33
|
@@ -41,6 +41,20 @@ interface ResolvedPluginDir {
|
|
|
41
41
|
warnings: string[];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
interface ResolvedMCPConfig {
|
|
45
|
+
/** On-disk config file to read, or null when servers are inline or nothing applies. */
|
|
46
|
+
path: string | null;
|
|
47
|
+
/** Server map declared inline in the plugin manifest, or null when the source is a file. */
|
|
48
|
+
inlineServers: Record<string, unknown> | null;
|
|
49
|
+
/** Path recorded as each discovered server's capability source. */
|
|
50
|
+
sourcePath: string;
|
|
51
|
+
/** Directory that relative stdio `command`/`cwd` values resolve against. */
|
|
52
|
+
baseDir: string;
|
|
53
|
+
/** True when a plugin manifest named this source, false for the conventional fallback. */
|
|
54
|
+
declared: boolean;
|
|
55
|
+
warnings: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
async function readPluginManifest(root: ClaudePluginRoot): Promise<ClaudePluginManifest | null> {
|
|
45
59
|
const manifestPath = path.join(root.path, ".claude-plugin", "plugin.json");
|
|
46
60
|
const raw = await readFile(manifestPath);
|
|
@@ -375,50 +389,144 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
|
|
|
375
389
|
// MCP Servers
|
|
376
390
|
// =============================================================================
|
|
377
391
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
392
|
+
/**
|
|
393
|
+
* Unwrap a parsed MCP config file to its server map. Supports the nested
|
|
394
|
+
* `{ mcpServers: { … } }` project shape and the flat `{ name: cfg, … }`
|
|
395
|
+
* marketplace-plugin shape. Returns null when a `mcpServers` field is present
|
|
396
|
+
* but not an object map (malformed) so the caller skips the file.
|
|
397
|
+
*/
|
|
398
|
+
function extractServerMap(obj: Record<string, unknown>): Record<string, unknown> | null {
|
|
399
|
+
if (isRecord(obj.mcpServers)) return obj.mcpServers;
|
|
400
|
+
if (!("mcpServers" in obj)) return obj;
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
384
403
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
404
|
+
/**
|
|
405
|
+
* Resolve where a plugin's MCP servers come from, honoring the manifest's
|
|
406
|
+
* `mcpServers` field before the conventional root `.mcp.json`.
|
|
407
|
+
*
|
|
408
|
+
* `.omp-plugin/plugin.json` takes precedence over `.claude-plugin/plugin.json`.
|
|
409
|
+
* The field may be an inline object (the server map itself) or a string path to
|
|
410
|
+
* a config file within the plugin root; a path escaping the root is rejected
|
|
411
|
+
* with a warning. When no manifest declares the field, `<root>/.mcp.json` is the
|
|
412
|
+
* fallback source.
|
|
413
|
+
*/
|
|
414
|
+
async function resolvePluginMCPConfig(root: ClaudePluginRoot): Promise<ResolvedMCPConfig> {
|
|
415
|
+
const fallback = path.join(root.path, ".mcp.json");
|
|
416
|
+
for (const manifestDir of [".omp-plugin", ".claude-plugin"]) {
|
|
417
|
+
const manifestPath = path.join(root.path, manifestDir, "plugin.json");
|
|
418
|
+
const raw = await readFile(manifestPath);
|
|
419
|
+
if (raw === null) continue;
|
|
389
420
|
|
|
390
421
|
let parsed: unknown;
|
|
391
422
|
try {
|
|
392
423
|
parsed = JSON.parse(raw);
|
|
393
424
|
} catch {
|
|
394
|
-
warnings.push(`[claude-plugins] Invalid JSON in ${mcpPath}`);
|
|
395
|
-
logger.warn(`[claude-plugins] Invalid JSON in ${mcpPath}`);
|
|
396
425
|
continue;
|
|
397
426
|
}
|
|
427
|
+
if (!isRecord(parsed)) continue;
|
|
428
|
+
const pointer = parsed.mcpServers;
|
|
429
|
+
|
|
430
|
+
// Inline object form: the manifest value is the server map itself, rooted
|
|
431
|
+
// at the plugin directory (Claude's ${CLAUDE_PLUGIN_ROOT} base).
|
|
432
|
+
if (isRecord(pointer)) {
|
|
433
|
+
return {
|
|
434
|
+
path: null,
|
|
435
|
+
inlineServers: pointer,
|
|
436
|
+
sourcePath: manifestPath,
|
|
437
|
+
baseDir: root.path,
|
|
438
|
+
declared: true,
|
|
439
|
+
warnings: [],
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// File-pointer form: resolve the named config file within the plugin root.
|
|
444
|
+
if (typeof pointer === "string") {
|
|
445
|
+
const configured = pointer.trim();
|
|
446
|
+
if (configured.length === 0) continue;
|
|
447
|
+
const resolved = path.resolve(root.path, configured);
|
|
448
|
+
if (!isWithinPluginRoot(root.path, resolved)) {
|
|
449
|
+
return {
|
|
450
|
+
path: null,
|
|
451
|
+
inlineServers: null,
|
|
452
|
+
sourcePath: manifestPath,
|
|
453
|
+
baseDir: root.path,
|
|
454
|
+
declared: true,
|
|
455
|
+
warnings: [
|
|
456
|
+
`[claude-plugins] Ignoring mcpServers path outside plugin root for ${root.id}: ${configured}`,
|
|
457
|
+
],
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
path: resolved,
|
|
462
|
+
inlineServers: null,
|
|
463
|
+
sourcePath: resolved,
|
|
464
|
+
baseDir: path.dirname(resolved),
|
|
465
|
+
declared: true,
|
|
466
|
+
warnings: [],
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
path: fallback,
|
|
473
|
+
inlineServers: null,
|
|
474
|
+
sourcePath: fallback,
|
|
475
|
+
baseDir: path.dirname(fallback),
|
|
476
|
+
declared: false,
|
|
477
|
+
warnings: [],
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
|
|
482
|
+
const items: MCPServer[] = [];
|
|
483
|
+
const warnings: string[] = [];
|
|
398
484
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
485
|
+
const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
|
|
486
|
+
warnings.push(...rootWarnings);
|
|
487
|
+
|
|
488
|
+
for (const root of roots) {
|
|
489
|
+
const resolved = await resolvePluginMCPConfig(root);
|
|
490
|
+
warnings.push(...resolved.warnings);
|
|
491
|
+
|
|
492
|
+
let servers: Record<string, unknown> | null;
|
|
493
|
+
if (resolved.inlineServers) {
|
|
494
|
+
servers = resolved.inlineServers;
|
|
495
|
+
} else if (resolved.path !== null) {
|
|
496
|
+
const raw = await readFile(resolved.path);
|
|
497
|
+
if (raw === null) {
|
|
498
|
+
// The conventional fallback is optional, but a manifest that names a
|
|
499
|
+
// missing file is an authoring error that would otherwise register
|
|
500
|
+
// zero servers with no explanation.
|
|
501
|
+
if (resolved.declared) {
|
|
502
|
+
const warning = `[claude-plugins] Missing mcpServers file declared by ${root.id}: ${resolved.path}`;
|
|
503
|
+
warnings.push(warning);
|
|
504
|
+
logger.warn(warning);
|
|
505
|
+
}
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
let parsed: unknown;
|
|
510
|
+
try {
|
|
511
|
+
parsed = JSON.parse(raw);
|
|
512
|
+
} catch {
|
|
513
|
+
warnings.push(`[claude-plugins] Invalid JSON in ${resolved.path}`);
|
|
514
|
+
logger.warn(`[claude-plugins] Invalid JSON in ${resolved.path}`);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
// Two file shapes are supported:
|
|
518
|
+
// nested: { "mcpServers": { name: cfg, ... } } (OMP/Claude Code project shape)
|
|
519
|
+
// flat: { name: cfg, ... } (Claude marketplace plugin shape)
|
|
520
|
+
if (!isRecord(parsed)) continue;
|
|
521
|
+
servers = extractServerMap(parsed);
|
|
417
522
|
} else {
|
|
418
523
|
continue;
|
|
419
524
|
}
|
|
525
|
+
if (servers === null) continue;
|
|
420
526
|
|
|
421
|
-
|
|
527
|
+
const { sourcePath, baseDir } = resolved;
|
|
528
|
+
for (const serverName in servers) {
|
|
529
|
+
const serverCfg = servers[serverName];
|
|
422
530
|
if (!serverCfg || typeof serverCfg !== "object" || Array.isArray(serverCfg)) continue;
|
|
423
531
|
const raw = serverCfg as {
|
|
424
532
|
enabled?: boolean;
|
|
@@ -437,7 +545,9 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
437
545
|
// occasionally ship .mcp.json entries with neither, which would register a useless
|
|
438
546
|
// server and surface as a connection error at runtime.
|
|
439
547
|
if (typeof raw.command !== "string" && typeof raw.url !== "string") {
|
|
440
|
-
warnings.push(
|
|
548
|
+
warnings.push(
|
|
549
|
+
`[claude-plugins] Skipping MCP server "${serverName}" in ${sourcePath}: missing command or url`,
|
|
550
|
+
);
|
|
441
551
|
continue;
|
|
442
552
|
}
|
|
443
553
|
const namespacedName = root.plugin ? `${root.plugin}:${serverName}` : serverName;
|
|
@@ -446,7 +556,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
446
556
|
const substitutedCwd = raw.cwd !== undefined ? substitutePluginRoot(raw.cwd, root.path) : undefined;
|
|
447
557
|
// Root relative command/cwd at the plugin's config directory, not the
|
|
448
558
|
// session cwd (MCP stdio spawning resolves relative values there).
|
|
449
|
-
const rooted = resolvePluginStdioPaths({ command: substitutedCommand, cwd: substitutedCwd },
|
|
559
|
+
const rooted = resolvePluginStdioPaths({ command: substitutedCommand, cwd: substitutedCwd }, baseDir);
|
|
450
560
|
const server: MCPServer = {
|
|
451
561
|
name: namespacedName,
|
|
452
562
|
...(raw.enabled !== undefined && { enabled: raw.enabled }),
|
|
@@ -460,7 +570,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
460
570
|
...(raw.auth !== undefined && { auth: raw.auth }),
|
|
461
571
|
...(raw.oauth !== undefined && { oauth: raw.oauth }),
|
|
462
572
|
...(raw.type !== undefined && { transport: raw.type as MCPServer["transport"] }),
|
|
463
|
-
_source: createSourceMeta(PROVIDER_ID,
|
|
573
|
+
_source: createSourceMeta(PROVIDER_ID, sourcePath, root.scope),
|
|
464
574
|
};
|
|
465
575
|
items.push(server);
|
|
466
576
|
}
|
package/src/export/html/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as fs from "node:fs
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentState } from "@oh-my-pi/pi-agent-core";
|
|
4
4
|
import { APP_NAME, isEnoent } from "@oh-my-pi/pi-utils";
|
|
@@ -7,31 +7,38 @@ import type { SessionEntry, SessionHeader } from "../../session/session-entries"
|
|
|
7
7
|
import { loadEntriesFromFile } from "../../session/session-loader";
|
|
8
8
|
import { SessionManager } from "../../session/session-manager";
|
|
9
9
|
import type { ExportThemeNames } from "./args";
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
10
|
+
import templateCssPath from "./template.css" with { type: "file" };
|
|
11
|
+
import templateHtmlPath from "./template.html" with { type: "file" };
|
|
12
|
+
import templateJsPath from "./template.js" with { type: "file" };
|
|
13
13
|
// Pre-built React tool renderers: built by `gen:tool-views` (`bun run gen:tool-views`),
|
|
14
14
|
// run automatically by root `prepare` on install and by `prepack` at publish.
|
|
15
|
-
import
|
|
15
|
+
import toolViewsJsPath from "./tool-views.generated.js" with { type: "file" };
|
|
16
16
|
import { webExportThemeVars } from "./web-palette";
|
|
17
17
|
|
|
18
18
|
export { type ExportThemeNames, parseExportArgs } from "./args";
|
|
19
19
|
|
|
20
20
|
let cachedTemplate: string | undefined;
|
|
21
|
+
/** Resolve a Bun file-loader value without parsing Windows drive letters as URL schemes. */
|
|
22
|
+
export function resolveBundledHtmlAssetPath(assetPath: string, moduleDir: string = import.meta.dir): string {
|
|
23
|
+
if (path.isAbsolute(assetPath) || path.win32.isAbsolute(assetPath)) return assetPath;
|
|
24
|
+
return path.resolve(moduleDir, assetPath);
|
|
25
|
+
}
|
|
21
26
|
|
|
22
27
|
/** Compose the standalone export template: minified CSS, tool renderers, and viewer JS inlined. */
|
|
23
28
|
export function getTemplate(): string {
|
|
24
29
|
if (cachedTemplate) return cachedTemplate;
|
|
30
|
+
const templateCss = fs.readFileSync(resolveBundledHtmlAssetPath(templateCssPath), "utf8");
|
|
31
|
+
const templateHtml = fs.readFileSync(resolveBundledHtmlAssetPath(templateHtmlPath as unknown as string), "utf8");
|
|
32
|
+
const templateJs = fs.readFileSync(resolveBundledHtmlAssetPath(templateJsPath), "utf8");
|
|
33
|
+
const toolViewsJs = fs.readFileSync(resolveBundledHtmlAssetPath(toolViewsJsPath), "utf8");
|
|
25
34
|
const minifiedCss = templateCss
|
|
26
35
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
27
36
|
.replace(/\s+/g, " ")
|
|
28
37
|
.replace(/\s*([{}:;,])\s*/g, "$1")
|
|
29
38
|
.trim();
|
|
30
39
|
// Function replacements so `$'`, `$&`, `$$`, etc. inside the embedded
|
|
31
|
-
// CSS/JS are not interpreted as substitution patterns.
|
|
32
|
-
|
|
33
|
-
// every *.html import as HTMLBundle (TS can't vary types by import attribute).
|
|
34
|
-
cachedTemplate = (templateHtml as unknown as string)
|
|
40
|
+
// CSS/JS are not interpreted as substitution patterns.
|
|
41
|
+
cachedTemplate = templateHtml
|
|
35
42
|
.replace("<template-css/>", () => `<style>${minifiedCss}</style>`)
|
|
36
43
|
.replace("<template-tool-views/>", () => `<script>${toolViewsJs}</script>`)
|
|
37
44
|
.replace("<template-js/>", () => `<script>${templateJs}</script>`);
|
|
@@ -207,7 +214,7 @@ async function collectSubSessionsFromDir(
|
|
|
207
214
|
): Promise<void> {
|
|
208
215
|
let names: string[];
|
|
209
216
|
try {
|
|
210
|
-
names = await fs.readdir(dir);
|
|
217
|
+
names = await fs.promises.readdir(dir);
|
|
211
218
|
} catch (err) {
|
|
212
219
|
if (isEnoent(err)) return;
|
|
213
220
|
throw err;
|
|
@@ -356,6 +356,32 @@ export class ExtensionRunner {
|
|
|
356
356
|
#managedTimers = new ManagedTimers((event, error, stack) =>
|
|
357
357
|
this.emitError({ extensionPath: "<timer>", event, error, stack }),
|
|
358
358
|
);
|
|
359
|
+
/**
|
|
360
|
+
* Dedup markers for `tool_call` emission, keyed `${toolCallId}:${toolName}`.
|
|
361
|
+
* The agent loop emits `tool_call` at arg-prep time (before scheduling and
|
|
362
|
+
* `tool_execution_start`) via the session's `beforeToolCall` wiring; the
|
|
363
|
+
* marker tells `ExtensionToolWrapper.execute` not to emit a second event for
|
|
364
|
+
* the same dispatch. Keyed by call id + tool name because a nested xd://
|
|
365
|
+
* device dispatch reuses the model's toolCallId under a different tool name
|
|
366
|
+
* and must still emit its own event. Bounded: markers for calls whose
|
|
367
|
+
* execute path never runs (policy deny, validation failure) would otherwise
|
|
368
|
+
* accumulate for the session's lifetime.
|
|
369
|
+
*/
|
|
370
|
+
#emittedToolCalls = new Set<string>();
|
|
371
|
+
|
|
372
|
+
/** Records that the loop already emitted `tool_call` for this dispatch. */
|
|
373
|
+
markToolCallEmitted(toolCallId: string, toolName: string): void {
|
|
374
|
+
if (this.#emittedToolCalls.size >= 512) {
|
|
375
|
+
const oldest = this.#emittedToolCalls.values().next().value;
|
|
376
|
+
if (oldest !== undefined) this.#emittedToolCalls.delete(oldest);
|
|
377
|
+
}
|
|
378
|
+
this.#emittedToolCalls.add(`${toolCallId}:${toolName}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Consumes a {@link markToolCallEmitted} marker; true when the loop already emitted. */
|
|
382
|
+
consumeToolCallEmitted(toolCallId: string, toolName: string): boolean {
|
|
383
|
+
return this.#emittedToolCalls.delete(`${toolCallId}:${toolName}`);
|
|
384
|
+
}
|
|
359
385
|
|
|
360
386
|
constructor(
|
|
361
387
|
private readonly extensions: Extension[],
|
|
@@ -157,15 +157,70 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
157
157
|
onUpdate?: AgentToolUpdateCallback<TDetails, TParameters>,
|
|
158
158
|
context?: AgentToolContext,
|
|
159
159
|
): Promise<AgentToolResult<TDetails, TParameters>> {
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
160
|
+
// The agent loop emits `tool_call` at arg-prep time (session
|
|
161
|
+
// `beforeToolCall` wiring) so a handler revision lands before concurrency
|
|
162
|
+
// scheduling and `tool_execution_start`. Consume the marker
|
|
163
|
+
// unconditionally so it cannot go stale; emit here only for dispatches
|
|
164
|
+
// the loop never saw — nested xd:// device dispatches and direct
|
|
165
|
+
// (non-loop) execution such as Cursor exec handlers.
|
|
166
|
+
const loopEmittedToolCall = this.runner.consumeToolCallEmitted(toolCallId, this.tool.name);
|
|
167
|
+
// Resolve approval settings up front. A `deny` on the original input short-circuits before the
|
|
168
|
+
// runner is touched — an already-denied tool never emits `tool_call` — while the full gate below
|
|
169
|
+
// re-resolves against the (possibly revised) input so a handler cannot rewrite into a denied or
|
|
170
|
+
// newly prompt-gated command and have it run unapproved.
|
|
163
171
|
const cliAutoApprove = context?.autoApprove === true;
|
|
164
172
|
const settings: Settings | undefined = context?.settings;
|
|
165
173
|
const configuredMode = (settings?.get("tools.approvalMode") ?? "yolo") as ApprovalMode;
|
|
166
174
|
const approvalMode: ApprovalMode = cliAutoApprove ? "yolo" : configuredMode;
|
|
167
175
|
const userPolicies = (settings?.get("tools.approval") ?? {}) as Record<string, unknown>;
|
|
168
|
-
|
|
176
|
+
if (resolveApproval(this.tool, approvalArgs(params, context), approvalMode, userPolicies).policy === "deny") {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Tool "${this.tool.name}" is blocked by user policy.\n` +
|
|
179
|
+
`To allow: remove "tools.approval.${this.tool.name}: deny" from config.`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 1. Emit tool_call event first - extensions can block execution or revise the input the tool
|
|
184
|
+
// runs with. Doing this BEFORE the approval gate means approval (below) resolves against the
|
|
185
|
+
// input that actually executes, closing the "approve one thing, run another" gap: the prompt
|
|
186
|
+
// text, policy resolution, and provider safety checks all see `effectiveParams`.
|
|
187
|
+
let effectiveParams = params;
|
|
188
|
+
if (!loopEmittedToolCall && this.runner.hasHandlers("tool_call")) {
|
|
189
|
+
try {
|
|
190
|
+
const callResult = (await this.runner.emitToolCall({
|
|
191
|
+
type: "tool_call",
|
|
192
|
+
toolName: this.tool.name,
|
|
193
|
+
toolCallId,
|
|
194
|
+
input: normalizeToolEventInput(
|
|
195
|
+
this.tool.name,
|
|
196
|
+
resolveToolEventInput(this.tool, toolEventArgs(params, context)),
|
|
197
|
+
),
|
|
198
|
+
})) as ToolCallEventResult | undefined;
|
|
199
|
+
|
|
200
|
+
if (callResult?.block) {
|
|
201
|
+
const reason = callResult.reason || "Tool execution was blocked by an extension";
|
|
202
|
+
throw new Error(reason);
|
|
203
|
+
}
|
|
204
|
+
// A non-blocking handler may replace the execution input. The returned object is the raw
|
|
205
|
+
// input passed to `execute` (handler-owned; not re-normalized). Skipped for `computer`
|
|
206
|
+
// tool calls, whose event input is a synthetic {actions,pendingSafetyChecks} view
|
|
207
|
+
// (see toolEventArgs) rather than the real execution params.
|
|
208
|
+
if (callResult?.input !== undefined && context?.toolCall?.providerMetadata?.type !== "computer") {
|
|
209
|
+
effectiveParams = callResult.input as typeof params;
|
|
210
|
+
}
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if (err instanceof Error) {
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
throw new Error(`Extension failed, blocking execution: ${String(err)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 2. Full approval gate against the (possibly revised) input that will actually run — resolves
|
|
220
|
+
// policy and prompts on `effectiveParams`, so the user approves exactly what executes. A revised
|
|
221
|
+
// input that newly resolves to `deny` is caught here even though the original passed the
|
|
222
|
+
// short-circuit above.
|
|
223
|
+
const resolvedArgs = approvalArgs(effectiveParams, context);
|
|
169
224
|
const resolved = resolveApproval(this.tool, resolvedArgs, approvalMode, userPolicies);
|
|
170
225
|
if (resolved.policy === "deny") {
|
|
171
226
|
throw new Error(
|
|
@@ -175,15 +230,17 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
175
230
|
}
|
|
176
231
|
const pendingSafetyChecks = computerSafetyChecks(context);
|
|
177
232
|
// An xd:// device dispatch already cleared the write tool's outer gate at
|
|
178
|
-
// this tool's tier — re-prompting would double-ask for one action.
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
233
|
+
// this tool's tier — re-prompting would double-ask for one action. The
|
|
234
|
+
// bypass only holds while the input is exactly what that outer gate
|
|
235
|
+
// approved: a handler revision here may have raised the tier, so revised
|
|
236
|
+
// input always faces the full gate. Explicit per-tool "prompt" policies
|
|
237
|
+
// and tool-demanded overrides still prompt. Provider safety checks are
|
|
238
|
+
// stronger: yolo, per-tool allow, and xdev approval never acknowledge
|
|
239
|
+
// them on the user's behalf.
|
|
182
240
|
const explicitPrompt = resolved.override || Object.hasOwn(userPolicies, this.tool.name);
|
|
241
|
+
const xdevBypass = context?.xdevApproved === true && effectiveParams === params;
|
|
183
242
|
const approvalCheck = {
|
|
184
|
-
required:
|
|
185
|
-
pendingSafetyChecks.length > 0 ||
|
|
186
|
-
(resolved.policy === "prompt" && (explicitPrompt || context?.xdevApproved !== true)),
|
|
243
|
+
required: pendingSafetyChecks.length > 0 || (resolved.policy === "prompt" && (explicitPrompt || !xdevBypass)),
|
|
187
244
|
reason: resolved.reason,
|
|
188
245
|
};
|
|
189
246
|
|
|
@@ -202,7 +259,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
202
259
|
});
|
|
203
260
|
}
|
|
204
261
|
|
|
205
|
-
const
|
|
262
|
+
const emitApprovalResolved = async (approved: boolean, reason?: string) => {
|
|
206
263
|
if (!hasApprovalHandlers) return;
|
|
207
264
|
await this.runner.emit({
|
|
208
265
|
type: "tool_approval_resolved",
|
|
@@ -218,7 +275,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
218
275
|
// ordinary tier approval, no setting or yolo mode may bypass this gate.
|
|
219
276
|
if (!this.runner.hasUI()) {
|
|
220
277
|
const reason = "no interactive UI available";
|
|
221
|
-
await
|
|
278
|
+
await emitApprovalResolved(false, reason);
|
|
222
279
|
if (pendingSafetyChecks.length > 0) {
|
|
223
280
|
throw new Error(
|
|
224
281
|
`Tool "${this.tool.name}" has pending provider safety checks but no interactive UI is available.`,
|
|
@@ -243,11 +300,11 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
243
300
|
try {
|
|
244
301
|
choice = await uiContext.select(safetyPrompt, ["Approve", "Deny"]);
|
|
245
302
|
} catch (err) {
|
|
246
|
-
await
|
|
303
|
+
await emitApprovalResolved(false, err instanceof Error ? err.message : "approval aborted");
|
|
247
304
|
throw err;
|
|
248
305
|
}
|
|
249
306
|
const approved = choice === "Approve";
|
|
250
|
-
await
|
|
307
|
+
await emitApprovalResolved(approved, approved ? undefined : "denied by user");
|
|
251
308
|
if (!approved) {
|
|
252
309
|
throw new Error(`Tool call denied by user: ${this.tool.name}`);
|
|
253
310
|
}
|
|
@@ -257,37 +314,12 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
257
314
|
}
|
|
258
315
|
}
|
|
259
316
|
|
|
260
|
-
// 2. Emit tool_call event - extensions can block execution
|
|
261
|
-
if (this.runner.hasHandlers("tool_call")) {
|
|
262
|
-
try {
|
|
263
|
-
const callResult = (await this.runner.emitToolCall({
|
|
264
|
-
type: "tool_call",
|
|
265
|
-
toolName: this.tool.name,
|
|
266
|
-
toolCallId,
|
|
267
|
-
input: normalizeToolEventInput(
|
|
268
|
-
this.tool.name,
|
|
269
|
-
resolveToolEventInput(this.tool, toolEventArgs(params, context)),
|
|
270
|
-
),
|
|
271
|
-
})) as ToolCallEventResult | undefined;
|
|
272
|
-
|
|
273
|
-
if (callResult?.block) {
|
|
274
|
-
const reason = callResult.reason || "Tool execution was blocked by an extension";
|
|
275
|
-
throw new Error(reason);
|
|
276
|
-
}
|
|
277
|
-
} catch (err) {
|
|
278
|
-
if (err instanceof Error) {
|
|
279
|
-
throw err;
|
|
280
|
-
}
|
|
281
|
-
throw new Error(`Extension failed, blocking execution: ${String(err)}`);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
317
|
// Execute the actual tool
|
|
286
318
|
let result: AgentToolResult<TDetails, TParameters>;
|
|
287
319
|
let executionError: Error | undefined;
|
|
288
320
|
|
|
289
321
|
try {
|
|
290
|
-
result = await this.tool.execute(toolCallId,
|
|
322
|
+
result = await this.tool.execute(toolCallId, effectiveParams, signal, onUpdate, context);
|
|
291
323
|
} catch (err) {
|
|
292
324
|
executionError = err instanceof Error ? err : new Error(String(err));
|
|
293
325
|
result = {
|
|
@@ -304,7 +336,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
304
336
|
toolCallId,
|
|
305
337
|
input: normalizeToolEventInput(
|
|
306
338
|
this.tool.name,
|
|
307
|
-
resolveToolEventInput(this.tool, toolEventArgs(
|
|
339
|
+
resolveToolEventInput(this.tool, toolEventArgs(effectiveParams, context)),
|
|
308
340
|
),
|
|
309
341
|
content: result.content,
|
|
310
342
|
details: result.details,
|
|
@@ -39,8 +39,9 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
39
39
|
onUpdate?: AgentToolUpdateCallback<TDetails, TParameters>,
|
|
40
40
|
context?: AgentToolContext,
|
|
41
41
|
) {
|
|
42
|
-
// Emit tool_call event - hooks can block execution
|
|
42
|
+
// Emit tool_call event - hooks can block execution or revise the input the tool runs with.
|
|
43
43
|
// If hook errors/times out, block by default (fail-safe)
|
|
44
|
+
let effectiveParams = params;
|
|
44
45
|
if (this.hookRunner.hasHandlers("tool_call")) {
|
|
45
46
|
try {
|
|
46
47
|
const callResult = (await this.hookRunner.emitToolCall({
|
|
@@ -57,6 +58,12 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
57
58
|
const reason = callResult.reason || "Tool execution was blocked by a hook";
|
|
58
59
|
throw new Error(reason);
|
|
59
60
|
}
|
|
61
|
+
// A non-blocking handler may replace the execution input. The returned object is the raw
|
|
62
|
+
// input the tool runs with (handler-owned); it is not re-normalized. Skipped for `computer`
|
|
63
|
+
// tool calls, whose real parameters are not represented by the event input.
|
|
64
|
+
if (callResult?.input !== undefined && context?.toolCall?.providerMetadata?.type !== "computer") {
|
|
65
|
+
effectiveParams = callResult.input as Static<TParameters>;
|
|
66
|
+
}
|
|
60
67
|
} catch (err) {
|
|
61
68
|
// Hook error or block - throw to mark as error
|
|
62
69
|
if (err instanceof Error) {
|
|
@@ -68,7 +75,7 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
68
75
|
|
|
69
76
|
// Execute the actual tool, forwarding onUpdate for progress streaming
|
|
70
77
|
try {
|
|
71
|
-
const result = await this.tool.execute(toolCallId,
|
|
78
|
+
const result = await this.tool.execute(toolCallId, effectiveParams, signal, onUpdate, context);
|
|
72
79
|
|
|
73
80
|
// Emit tool_result event - hooks can modify the result
|
|
74
81
|
if (this.hookRunner.hasHandlers("tool_result")) {
|
|
@@ -78,7 +85,7 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
78
85
|
toolCallId,
|
|
79
86
|
input: normalizeToolEventInput(
|
|
80
87
|
this.tool.name,
|
|
81
|
-
resolveToolEventInput(this.tool,
|
|
88
|
+
resolveToolEventInput(this.tool, effectiveParams as Record<string, unknown>),
|
|
82
89
|
),
|
|
83
90
|
content: result.content,
|
|
84
91
|
details: result.details,
|
|
@@ -104,7 +111,7 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
104
111
|
toolCallId,
|
|
105
112
|
input: normalizeToolEventInput(
|
|
106
113
|
this.tool.name,
|
|
107
|
-
resolveToolEventInput(this.tool,
|
|
114
|
+
resolveToolEventInput(this.tool, effectiveParams as Record<string, unknown>),
|
|
108
115
|
),
|
|
109
116
|
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
|
|
110
117
|
details: undefined,
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* `types.ts` via the `export *` below — pi-ai still exports both as types,
|
|
20
20
|
* only the runtime `Type` builder and `StringEnum()` helper were removed.
|
|
21
21
|
*/
|
|
22
|
-
import type { Api, Model } from "@oh-my-pi/pi-ai";
|
|
22
|
+
import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
|
|
23
23
|
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
24
24
|
import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
25
25
|
import {
|
|
@@ -80,6 +80,33 @@ export function clampThinkingLevel<TApi extends Api>(model: Model<TApi>, level:
|
|
|
80
80
|
return clampThinkingLevelForModel(model, level) ?? "off";
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Provider-error classification patterns ported verbatim from historical pi-ai
|
|
85
|
+
* (`@earendil-works/pi-ai` `utils/retry.ts`). Legacy extensions call
|
|
86
|
+
* {@link isRetryableAssistantError} to decide whether to restart a failed
|
|
87
|
+
* assistant turn, so the wording tables must match the upstream semantics they
|
|
88
|
+
* were authored against rather than OMP's own `Error`-based classifiers.
|
|
89
|
+
*/
|
|
90
|
+
const NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN =
|
|
91
|
+
/GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i;
|
|
92
|
+
const RETRYABLE_PROVIDER_ERROR_PATTERN =
|
|
93
|
+
/overloaded|rate.?limit|too many requests|429|500|502|503|504|524|service.?unavailable|server.?error|internal.?error|provider.?returned.?error|network.?error|connection.?error|connection.?refused|connection.?lost|other side closed|fetch failed|getaddrinfo|ENOTFOUND|EAI_AGAIN|upstream.?connect|reset before headers|socket hang up|socket connection was closed|timed? out|timeout|terminated|websocket.?closed|websocket.?error|ended without|stream ended before message_stop|stream ended before a terminal response event|http2 request did not get a response|retry delay|you can retry your request|try your request again|please retry your request|ResourceExhausted/i;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Compatibility implementation of historical pi-ai's `isRetryableAssistantError`.
|
|
97
|
+
*
|
|
98
|
+
* Classifies whether a failed assistant message looks like a transient provider
|
|
99
|
+
* or transport error so legacy extensions can decide if the last assistant turn
|
|
100
|
+
* should be restarted. Account/quota limits are treated as non-retryable. This
|
|
101
|
+
* does not implement any retry policy; callers own budget, backoff, and reporting.
|
|
102
|
+
*/
|
|
103
|
+
export function isRetryableAssistantError(message: AssistantMessage): boolean {
|
|
104
|
+
if (message.stopReason !== "error" || !message.errorMessage) return false;
|
|
105
|
+
const errorMessage = message.errorMessage;
|
|
106
|
+
if (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false;
|
|
107
|
+
return RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);
|
|
108
|
+
}
|
|
109
|
+
|
|
83
110
|
export * from "@oh-my-pi/pi-ai";
|
|
84
111
|
/**
|
|
85
112
|
* Compatibility re-exports for catalog symbols that pi-ai historically exposed
|
|
@@ -93,3 +120,21 @@ export * from "@oh-my-pi/pi-ai";
|
|
|
93
120
|
export { calculateCost, getBundledModel, getBundledModels, getBundledProviders, modelsAreEqual, Type };
|
|
94
121
|
export const getModel = getBundledModel;
|
|
95
122
|
export const getModels = getBundledModels;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Compatibility re-exports for runtime helpers that upstream
|
|
126
|
+
* `@earendil-works/pi-ai` exposed from its package root but omp's
|
|
127
|
+
* `@oh-my-pi/pi-ai` barrel no longer forwards. Each symbol still exists in the
|
|
128
|
+
* host graph — only its root re-export was dropped — so bridging it here keeps
|
|
129
|
+
* legacy extensions importing it from the pi-ai root resolving through Bun's
|
|
130
|
+
* static named-export check (e.g. `omp plugin install pi-blackhole`).
|
|
131
|
+
*
|
|
132
|
+
* This is the full set derived from an audit of the upstream root surface: the
|
|
133
|
+
* error-classification predicate `isContextOverflow` (now under
|
|
134
|
+
* `@oh-my-pi/pi-ai/error`) and the JSON-repair helpers that omp relocated to
|
|
135
|
+
* `@oh-my-pi/pi-utils`. Upstream root symbols with no omp equivalent are
|
|
136
|
+
* intentionally not shimmed — the package has diverged and there is nothing to
|
|
137
|
+
* forward.
|
|
138
|
+
*/
|
|
139
|
+
export { isContextOverflow } from "@oh-my-pi/pi-ai/error";
|
|
140
|
+
export { parseJsonWithRepair, parseStreamingJson, repairJson } from "@oh-my-pi/pi-utils";
|
|
@@ -289,13 +289,30 @@ export interface TodoReminderEvent {
|
|
|
289
289
|
|
|
290
290
|
/**
|
|
291
291
|
* Return type for `tool_call` handlers.
|
|
292
|
-
* Allows handlers to block tool execution.
|
|
292
|
+
* Allows handlers to block tool execution or revise the input the tool runs with.
|
|
293
293
|
*/
|
|
294
294
|
export interface ToolCallEventResult {
|
|
295
295
|
/** If true, block the tool from executing */
|
|
296
296
|
block?: boolean;
|
|
297
297
|
/** Reason for blocking (returned to LLM as error) */
|
|
298
298
|
reason?: string;
|
|
299
|
+
/**
|
|
300
|
+
* Replacement input the tool executes with, instead of the original arguments. Ignored when
|
|
301
|
+
* `block` is true. This is the raw execution input passed to the tool's `execute` (the handler
|
|
302
|
+
* owns its correctness) — not the normalized `event.input` view, which may carry derived
|
|
303
|
+
* gate-only fields (e.g. hashline `edit` `path`/`paths`) that are not real parameters. When
|
|
304
|
+
* multiple handlers set `input`, the last one wins; handlers do not observe each other's
|
|
305
|
+
* revisions (each sees the original `event.input`). Not applied to `computer` tool calls.
|
|
306
|
+
*
|
|
307
|
+
* For model-issued tool calls the event fires at arg-prep time in the agent loop, before
|
|
308
|
+
* concurrency scheduling, `tool_execution_start`, and the approval gate: the revision is
|
|
309
|
+
* revalidated against the tool schema and becomes what the loop schedules, displays, persists,
|
|
310
|
+
* and executes — the user always approves what actually runs. For dispatches the loop never
|
|
311
|
+
* sees (nested `write xd://` device calls, Cursor direct execution) the tool wrapper applies
|
|
312
|
+
* the revision before its own approval gate; a revised nested xd:// input forfeits the outer
|
|
313
|
+
* write gate's approval and faces the full prompt again.
|
|
314
|
+
*/
|
|
315
|
+
input?: Record<string, unknown>;
|
|
299
316
|
}
|
|
300
317
|
|
|
301
318
|
/**
|