@oh-my-pi/pi-coding-agent 15.10.8 → 15.10.9
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 +15 -1
- package/dist/types/extensibility/custom-tools/loader.d.ts +22 -3
- package/dist/types/extensibility/extensions/index.d.ts +1 -1
- package/dist/types/extensibility/extensions/loader.d.ts +17 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +8 -0
- package/dist/types/mcp/transports/stdio.d.ts +12 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -2
- package/dist/types/sdk.d.ts +42 -2
- package/dist/types/task/executor.d.ts +16 -0
- package/dist/types/tools/index.d.ts +17 -0
- package/dist/types/tui/hyperlink.d.ts +8 -0
- package/package.json +9 -9
- package/src/extensibility/custom-tools/loader.ts +43 -19
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/loader.ts +29 -6
- package/src/extensibility/plugins/legacy-pi-compat.ts +30 -6
- package/src/mcp/transports/stdio.ts +139 -3
- package/src/modes/components/custom-editor.ts +69 -9
- package/src/modes/components/transcript-container.ts +77 -25
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/mcp-command-controller.ts +2 -2
- package/src/sdk.ts +138 -56
- package/src/ssh/ssh-executor.ts +60 -4
- package/src/task/executor.ts +19 -0
- package/src/task/index.ts +4 -0
- package/src/tools/index.ts +17 -0
- package/src/tui/hyperlink.ts +27 -3
package/src/sdk.ts
CHANGED
|
@@ -62,10 +62,11 @@ import {
|
|
|
62
62
|
type LoadedCustomCommand,
|
|
63
63
|
loadCustomCommands as loadCustomCommandsInternal,
|
|
64
64
|
} from "./extensibility/custom-commands";
|
|
65
|
-
import {
|
|
65
|
+
import { discoverCustomToolPaths, loadCustomTools, type ToolPathWithSource } from "./extensibility/custom-tools";
|
|
66
66
|
import type { CustomTool, CustomToolContext, CustomToolSessionEvent } from "./extensibility/custom-tools/types";
|
|
67
67
|
import {
|
|
68
68
|
discoverAndLoadExtensions,
|
|
69
|
+
discoverExtensionPaths,
|
|
69
70
|
type ExtensionContext,
|
|
70
71
|
type ExtensionFactory,
|
|
71
72
|
ExtensionRunner,
|
|
@@ -337,10 +338,41 @@ export interface CreateAgentSessionOptions {
|
|
|
337
338
|
/** Disable extension discovery (explicit paths still load). */
|
|
338
339
|
disableExtensionDiscovery?: boolean;
|
|
339
340
|
/**
|
|
340
|
-
* Pre-loaded extensions (skips file discovery
|
|
341
|
-
*
|
|
341
|
+
* Pre-loaded extensions (skips file discovery and the per-session factory
|
|
342
|
+
* call). Used by the CLI when extensions are loaded early to parse custom
|
|
343
|
+
* flags — the same process owns the returned instances, so reusing them is
|
|
344
|
+
* safe.
|
|
345
|
+
*
|
|
346
|
+
* NEVER pass this across session boundaries (e.g. parent → subagent).
|
|
347
|
+
* `Extension` instances close over a parent-bound `ExtensionAPI` (cwd,
|
|
348
|
+
* eventBus, runtime), and reusing them would route tools/handlers/commands
|
|
349
|
+
* back through the parent. For subagents, forward
|
|
350
|
+
* {@link preloadedExtensionPaths} instead.
|
|
351
|
+
*
|
|
352
|
+
* @internal
|
|
342
353
|
*/
|
|
343
354
|
preloadedExtensions?: LoadExtensionsResult;
|
|
355
|
+
/**
|
|
356
|
+
* Pre-discovered extension source paths. When provided, the filesystem-scan
|
|
357
|
+
* inside `discoverExtensionPaths()` is skipped — the session still calls
|
|
358
|
+
* `loadExtensions()` itself so each `Extension` is bound to THIS session's
|
|
359
|
+
* `ExtensionAPI` (cwd, eventBus, runtime).
|
|
360
|
+
*
|
|
361
|
+
* This is the safe pass-through for parent → subagent forwarding.
|
|
362
|
+
*/
|
|
363
|
+
preloadedExtensionPaths?: string[];
|
|
364
|
+
/**
|
|
365
|
+
* Pre-discovered custom-tool source paths from `.omp/tools/`, `.claude/tools/`,
|
|
366
|
+
* plugins, etc. When provided, the filesystem-scan inside
|
|
367
|
+
* `discoverCustomToolPaths()` is skipped — subagents inherit the parent's
|
|
368
|
+
* scan result and call `loadCustomTools()` themselves so each session binds
|
|
369
|
+
* tools to its OWN `CustomToolAPI` (cwd, exec, pushPendingAction, UI).
|
|
370
|
+
*
|
|
371
|
+
* Forwarding the loaded `LoadedCustomTool[]` instances directly would reuse
|
|
372
|
+
* the parent's session-bound API and route tool execution back through the
|
|
373
|
+
* parent — wrong for isolated tasks and for pending-action routing.
|
|
374
|
+
*/
|
|
375
|
+
preloadedCustomToolPaths?: ToolPathWithSource[];
|
|
344
376
|
|
|
345
377
|
/** Shared event bus for tool/extension communication. Default: creates new bus. */
|
|
346
378
|
eventBus?: EventBus;
|
|
@@ -565,6 +597,26 @@ export async function discoverExtensions(cwd?: string): Promise<LoadExtensionsRe
|
|
|
565
597
|
return discoverAndLoadExtensions([], resolvedCwd);
|
|
566
598
|
}
|
|
567
599
|
|
|
600
|
+
/**
|
|
601
|
+
* Path-only counterpart of {@link loadSessionExtensions}: the FS-heavy scan
|
|
602
|
+
* without the per-session module load. Subagents reuse the parent's path list
|
|
603
|
+
* (cached on {@link ToolSession.extensionPaths}) and rebuild Extension
|
|
604
|
+
* instances themselves so each session's `ExtensionAPI` (cwd, eventBus,
|
|
605
|
+
* runtime) is its own.
|
|
606
|
+
*/
|
|
607
|
+
export async function discoverSessionExtensionPaths(
|
|
608
|
+
options: Pick<CreateAgentSessionOptions, "disableExtensionDiscovery" | "additionalExtensionPaths">,
|
|
609
|
+
cwd: string,
|
|
610
|
+
settings: Settings,
|
|
611
|
+
): Promise<string[]> {
|
|
612
|
+
if (options.disableExtensionDiscovery) {
|
|
613
|
+
return options.additionalExtensionPaths ?? [];
|
|
614
|
+
}
|
|
615
|
+
const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...(settings.get("extensions") ?? [])];
|
|
616
|
+
const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
|
|
617
|
+
return discoverExtensionPaths(configuredPaths, cwd, disabledExtensionIds);
|
|
618
|
+
}
|
|
619
|
+
|
|
568
620
|
/**
|
|
569
621
|
* Load the discovered/configured extensions for a session — everything {@link
|
|
570
622
|
* createAgentSession} would load except the inline factory extensions it appends
|
|
@@ -580,23 +632,8 @@ export async function loadSessionExtensions(
|
|
|
580
632
|
settings: Settings,
|
|
581
633
|
eventBus: EventBus,
|
|
582
634
|
): Promise<LoadExtensionsResult> {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const configuredPaths = options.additionalExtensionPaths ?? [];
|
|
586
|
-
result = await logger.time("loadExtensions", loadExtensions, configuredPaths, cwd, eventBus);
|
|
587
|
-
} else {
|
|
588
|
-
// Merge CLI extension paths with settings extension paths.
|
|
589
|
-
const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...(settings.get("extensions") ?? [])];
|
|
590
|
-
const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
|
|
591
|
-
result = await logger.time(
|
|
592
|
-
"discoverAndLoadExtensions",
|
|
593
|
-
discoverAndLoadExtensions,
|
|
594
|
-
configuredPaths,
|
|
595
|
-
cwd,
|
|
596
|
-
eventBus,
|
|
597
|
-
disabledExtensionIds,
|
|
598
|
-
);
|
|
599
|
-
}
|
|
635
|
+
const paths = await discoverSessionExtensionPaths(options, cwd, settings);
|
|
636
|
+
const result = await logger.time("loadExtensions", loadExtensions, paths, cwd, eventBus);
|
|
600
637
|
for (const { path, error } of result.errors) {
|
|
601
638
|
logger.error("Failed to load extension", { path, error });
|
|
602
639
|
}
|
|
@@ -1193,23 +1230,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1193
1230
|
}
|
|
1194
1231
|
|
|
1195
1232
|
// Discover rules and bucket them in one pass to avoid repeated scans over large rule sets.
|
|
1196
|
-
const { ttsrManager, rulebookRules, alwaysApplyRules } = await logger.time(
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1233
|
+
const { ttsrManager, rulebookRules, alwaysApplyRules, allRules } = await logger.time(
|
|
1234
|
+
"discoverTtsrRules",
|
|
1235
|
+
async () => {
|
|
1236
|
+
const { TtsrManager } = await import("./export/ttsr");
|
|
1237
|
+
const ttsrSettings = settings.getGroup("ttsr");
|
|
1238
|
+
const ttsrManager = new TtsrManager(ttsrSettings);
|
|
1239
|
+
const rulesResult =
|
|
1240
|
+
options.rules !== undefined
|
|
1241
|
+
? { items: options.rules, warnings: undefined }
|
|
1242
|
+
: await loadCapability<Rule>(ruleCapability.id, { cwd });
|
|
1243
|
+
const { rulebookRules, alwaysApplyRules } = bucketRules(rulesResult.items, ttsrManager, {
|
|
1244
|
+
builtinRules: ttsrSettings.builtinRules,
|
|
1245
|
+
disabledRules: ttsrSettings.disabledRules,
|
|
1246
|
+
});
|
|
1247
|
+
if (existingSession.injectedTtsrRules.length > 0) {
|
|
1248
|
+
ttsrManager.restoreInjected(existingSession.injectedTtsrRules);
|
|
1249
|
+
}
|
|
1250
|
+
return { ttsrManager, rulebookRules, alwaysApplyRules, allRules: rulesResult.items };
|
|
1251
|
+
},
|
|
1252
|
+
);
|
|
1213
1253
|
|
|
1214
1254
|
// Resolve contextFiles up-front (it's needed before tool creation). The
|
|
1215
1255
|
// workspace tree scan is slow on large repos and we MUST NOT block startup on
|
|
@@ -1331,6 +1371,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1331
1371
|
contextFiles,
|
|
1332
1372
|
workspaceTree: resolvedWorkspaceTree,
|
|
1333
1373
|
skills,
|
|
1374
|
+
rules: allRules,
|
|
1334
1375
|
eventBus,
|
|
1335
1376
|
outputSchema: options.outputSchema,
|
|
1336
1377
|
requireYieldTool: options.requireYieldTool,
|
|
@@ -1514,22 +1555,29 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1514
1555
|
customTools.push(...getSearchTools());
|
|
1515
1556
|
}
|
|
1516
1557
|
|
|
1517
|
-
// Discover
|
|
1558
|
+
// Discover custom tools from `.omp/tools/`, `.claude/tools/`, plugins, etc.
|
|
1559
|
+
// Subagents reuse the parent's scan via `preloadedCustomToolPaths` to skip
|
|
1560
|
+
// the FS walk, but ALWAYS re-call `loadCustomTools` here so factories bind
|
|
1561
|
+
// to THIS session's `CustomToolAPI` (cwd, exec, pushPendingAction, UI).
|
|
1562
|
+
// Forwarding the parent's `LoadedCustomTool[]` directly would route tool
|
|
1563
|
+
// execution back through the parent — wrong for isolated tasks and for
|
|
1564
|
+
// pending-action queueing.
|
|
1518
1565
|
const builtInToolNames = builtinTools.map(t => t.name);
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
cwd,
|
|
1524
|
-
builtInToolNames,
|
|
1525
|
-
action => queueResolveHandler(toolSession, action),
|
|
1566
|
+
const customToolPaths: ToolPathWithSource[] =
|
|
1567
|
+
options.preloadedCustomToolPaths ??
|
|
1568
|
+
(await logger.time("discoverCustomToolPaths", () => discoverCustomToolPaths([], cwd)));
|
|
1569
|
+
const customToolsLoadResult = await logger.time("loadCustomTools", () =>
|
|
1570
|
+
loadCustomTools(customToolPaths, cwd, builtInToolNames, action => queueResolveHandler(toolSession, action)),
|
|
1526
1571
|
);
|
|
1527
|
-
for (const { path, error } of
|
|
1572
|
+
for (const { path, error } of customToolsLoadResult.errors) {
|
|
1528
1573
|
logger.error("Custom tool load failed", { path, error });
|
|
1529
1574
|
}
|
|
1530
|
-
if (
|
|
1531
|
-
customTools.push(...
|
|
1575
|
+
if (customToolsLoadResult.tools.length > 0) {
|
|
1576
|
+
customTools.push(...customToolsLoadResult.tools.map(loaded => loaded.tool));
|
|
1532
1577
|
}
|
|
1578
|
+
// Forward the path list (NOT the loaded tools) to subagents so they
|
|
1579
|
+
// re-bind under their own `CustomToolAPI` while skipping the FS scan.
|
|
1580
|
+
toolSession.customToolPaths = customToolPaths;
|
|
1533
1581
|
|
|
1534
1582
|
const inlineExtensions: ExtensionFactory[] = options.extensions ? [...options.extensions] : [];
|
|
1535
1583
|
inlineExtensions.push((await import("./autoresearch")).createAutoresearchExtension);
|
|
@@ -1537,14 +1585,48 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1537
1585
|
inlineExtensions.push(createCustomToolsExtension(customTools));
|
|
1538
1586
|
}
|
|
1539
1587
|
|
|
1540
|
-
// Load extensions.
|
|
1541
|
-
//
|
|
1542
|
-
//
|
|
1543
|
-
//
|
|
1544
|
-
//
|
|
1545
|
-
//
|
|
1546
|
-
|
|
1547
|
-
|
|
1588
|
+
// Load extensions. Three paths:
|
|
1589
|
+
// 1. `preloadedExtensions` (CLI): caller already loaded — reuse the
|
|
1590
|
+
// Extension instances. Shallow-clone `extensions` so the inline
|
|
1591
|
+
// push below cannot mutate the caller's array. `runtime` is shared
|
|
1592
|
+
// so flag values set pre-creation flow into the live session.
|
|
1593
|
+
// 2. `preloadedExtensionPaths` (subagent): caller resolved paths;
|
|
1594
|
+
// skip the FS scan but always re-call `loadExtensions` here so
|
|
1595
|
+
// each `Extension` binds to THIS session's `ExtensionAPI`
|
|
1596
|
+
// (cwd, eventBus, runtime).
|
|
1597
|
+
// 3. No preload: run the full session discovery.
|
|
1598
|
+
// `disableExtensionDiscovery` is honored implicitly: a caller that set
|
|
1599
|
+
// the flag and pre-resolved the result already reflects that choice.
|
|
1600
|
+
let extensionPaths: string[];
|
|
1601
|
+
let extensionsResult: LoadExtensionsResult;
|
|
1602
|
+
if (options.preloadedExtensions) {
|
|
1603
|
+
extensionsResult = {
|
|
1604
|
+
...options.preloadedExtensions,
|
|
1605
|
+
extensions: [...options.preloadedExtensions.extensions],
|
|
1606
|
+
};
|
|
1607
|
+
// Capture paths for downstream forwarding; filter inline-factory
|
|
1608
|
+
// entries (`<inline-N>`) — those are per-session, not source paths.
|
|
1609
|
+
extensionPaths = extensionsResult.extensions
|
|
1610
|
+
.map(ext => ext.resolvedPath)
|
|
1611
|
+
.filter(p => !p.startsWith("<inline"));
|
|
1612
|
+
} else if (options.preloadedExtensionPaths) {
|
|
1613
|
+
extensionPaths = options.preloadedExtensionPaths;
|
|
1614
|
+
extensionsResult = await logger.time("loadExtensions", loadExtensions, extensionPaths, cwd, eventBus);
|
|
1615
|
+
for (const { path, error } of extensionsResult.errors) {
|
|
1616
|
+
logger.error("Failed to load extension", { path, error });
|
|
1617
|
+
}
|
|
1618
|
+
} else {
|
|
1619
|
+
extensionPaths = await logger.time("discoverSessionExtensionPaths", () =>
|
|
1620
|
+
discoverSessionExtensionPaths(options, cwd, settings),
|
|
1621
|
+
);
|
|
1622
|
+
extensionsResult = await logger.time("loadExtensions", loadExtensions, extensionPaths, cwd, eventBus);
|
|
1623
|
+
for (const { path, error } of extensionsResult.errors) {
|
|
1624
|
+
logger.error("Failed to load extension", { path, error });
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
// Forward the source-path list (NOT the loaded instances) so subagents
|
|
1628
|
+
// rebuild their own session-scoped extensions.
|
|
1629
|
+
toolSession.extensionPaths = extensionPaths;
|
|
1548
1630
|
|
|
1549
1631
|
// Load inline extensions from factories
|
|
1550
1632
|
if (inlineExtensions.length > 0) {
|
package/src/ssh/ssh-executor.ts
CHANGED
|
@@ -42,6 +42,42 @@ export interface SSHResult {
|
|
|
42
42
|
artifactId?: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
type SSHExitEvent = { kind: "exit"; exitCode: number } | { kind: "error"; error: unknown };
|
|
46
|
+
|
|
47
|
+
function sshExitEvent(exitCode: number): SSHExitEvent {
|
|
48
|
+
return { kind: "exit", exitCode };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sshErrorEvent(error: unknown): SSHExitEvent {
|
|
52
|
+
return { kind: "error", error };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createAbortWaiter(
|
|
56
|
+
signal: AbortSignal | undefined,
|
|
57
|
+
streamAbort: AbortController,
|
|
58
|
+
): { promise: Promise<ptree.AbortError> | undefined; cleanup: () => void } {
|
|
59
|
+
if (!signal) {
|
|
60
|
+
return { promise: undefined, cleanup: () => {} };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { promise, resolve } = Promise.withResolvers<ptree.AbortError>();
|
|
64
|
+
const onAbort = () => {
|
|
65
|
+
const error = new ptree.AbortError(signal.reason, "<cancelled>");
|
|
66
|
+
if (!streamAbort.signal.aborted) {
|
|
67
|
+
streamAbort.abort(error);
|
|
68
|
+
}
|
|
69
|
+
resolve(error);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (signal.aborted) {
|
|
73
|
+
onAbort();
|
|
74
|
+
return { promise, cleanup: () => {} };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
78
|
+
return { promise, cleanup: () => signal.removeEventListener("abort", onAbort) };
|
|
79
|
+
}
|
|
80
|
+
|
|
45
81
|
function quoteForCompatShell(command: string): string {
|
|
46
82
|
if (command.length === 0) {
|
|
47
83
|
return "''";
|
|
@@ -94,19 +130,37 @@ export async function executeSSH(
|
|
|
94
130
|
maxColumns: resolveOutputMaxColumns(settings),
|
|
95
131
|
});
|
|
96
132
|
|
|
97
|
-
const
|
|
133
|
+
const streamAbort = new AbortController();
|
|
134
|
+
const abortWaiter = createAbortWaiter(options?.signal, streamAbort);
|
|
135
|
+
const streamOptions = { signal: streamAbort.signal };
|
|
136
|
+
const streams = [child.stdout.pipeTo(sink.createInput(), streamOptions)];
|
|
98
137
|
if (child.stderr) {
|
|
99
|
-
streams.push(child.stderr.pipeTo(sink.createInput()));
|
|
138
|
+
streams.push(child.stderr.pipeTo(sink.createInput(), streamOptions));
|
|
100
139
|
}
|
|
101
|
-
|
|
140
|
+
const streamsSettled = Promise.allSettled(streams).then(() => {});
|
|
102
141
|
|
|
103
142
|
try {
|
|
143
|
+
const exitEvent = child.exited.then(sshExitEvent, sshErrorEvent);
|
|
144
|
+
const abortEvent = abortWaiter.promise?.then(sshErrorEvent);
|
|
145
|
+
const event = await (abortEvent ? Promise.race([exitEvent, abortEvent]) : exitEvent);
|
|
146
|
+
if (event.kind === "error") {
|
|
147
|
+
throw event.error;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const streamEvent = await (abortEvent ? Promise.race([streamsSettled, abortEvent]) : streamsSettled);
|
|
151
|
+
if (streamEvent?.kind === "error") {
|
|
152
|
+
throw streamEvent.error;
|
|
153
|
+
}
|
|
104
154
|
return {
|
|
105
|
-
exitCode:
|
|
155
|
+
exitCode: event.exitCode,
|
|
106
156
|
cancelled: false,
|
|
107
157
|
...(await sink.dump()),
|
|
108
158
|
};
|
|
109
159
|
} catch (err) {
|
|
160
|
+
if (!streamAbort.signal.aborted) {
|
|
161
|
+
streamAbort.abort(err);
|
|
162
|
+
}
|
|
163
|
+
void streamsSettled;
|
|
110
164
|
if (err instanceof ptree.Exception) {
|
|
111
165
|
if (err instanceof ptree.TimeoutError) {
|
|
112
166
|
return {
|
|
@@ -129,5 +183,7 @@ export async function executeSSH(
|
|
|
129
183
|
};
|
|
130
184
|
}
|
|
131
185
|
throw err;
|
|
186
|
+
} finally {
|
|
187
|
+
abortWaiter.cleanup();
|
|
132
188
|
}
|
|
133
189
|
}
|
package/src/task/executor.ts
CHANGED
|
@@ -8,11 +8,13 @@ import path from "node:path";
|
|
|
8
8
|
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
10
10
|
import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import type { Rule } from "../capability/rule";
|
|
11
12
|
import { ModelRegistry } from "../config/model-registry";
|
|
12
13
|
import { resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
|
|
13
14
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
14
15
|
import { Settings } from "../config/settings";
|
|
15
16
|
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
17
|
+
import type { ToolPathWithSource } from "../extensibility/custom-tools";
|
|
16
18
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
17
19
|
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
|
18
20
|
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
|
|
@@ -190,6 +192,20 @@ export interface ExecutorOptions {
|
|
|
190
192
|
skills?: Skill[];
|
|
191
193
|
promptTemplates?: PromptTemplate[];
|
|
192
194
|
workspaceTree?: WorkspaceTree;
|
|
195
|
+
/** Parent-discovered rules, forwarded to skip rule discovery in the subagent. */
|
|
196
|
+
rules?: Rule[];
|
|
197
|
+
/**
|
|
198
|
+
* Parent's discovered extension source paths. Forwarded to skip the
|
|
199
|
+
* extension FS scan in the subagent; the subagent then re-binds each
|
|
200
|
+
* extension against its own `ExtensionAPI` (cwd, eventBus, runtime).
|
|
201
|
+
*/
|
|
202
|
+
preloadedExtensionPaths?: string[];
|
|
203
|
+
/**
|
|
204
|
+
* Parent's discovered custom-tool source paths. Forwarded to skip the
|
|
205
|
+
* `.omp/tools/` FS scan in the subagent; the subagent then re-binds each
|
|
206
|
+
* tool against its own `CustomToolAPI` (cwd, exec, pushPendingAction, UI).
|
|
207
|
+
*/
|
|
208
|
+
preloadedCustomToolPaths?: ToolPathWithSource[];
|
|
193
209
|
mcpManager?: MCPManager;
|
|
194
210
|
authStorage?: AuthStorage;
|
|
195
211
|
modelRegistry?: ModelRegistry;
|
|
@@ -1284,6 +1300,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1284
1300
|
skills: options.skills,
|
|
1285
1301
|
promptTemplates: options.promptTemplates,
|
|
1286
1302
|
workspaceTree: options.workspaceTree,
|
|
1303
|
+
rules: options.rules,
|
|
1304
|
+
preloadedExtensionPaths: options.preloadedExtensionPaths,
|
|
1305
|
+
preloadedCustomToolPaths: options.preloadedCustomToolPaths,
|
|
1287
1306
|
systemPrompt: defaultPrompt => {
|
|
1288
1307
|
const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
|
|
1289
1308
|
agent: agent.systemPrompt,
|
package/src/task/index.ts
CHANGED
|
@@ -990,6 +990,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
990
990
|
autoloadSkills: resolvedAutoloadSkills,
|
|
991
991
|
workspaceTree: this.session.workspaceTree,
|
|
992
992
|
promptTemplates,
|
|
993
|
+
rules: this.session.rules,
|
|
994
|
+
preloadedExtensionPaths: this.session.extensionPaths,
|
|
995
|
+
preloadedCustomToolPaths: this.session.customToolPaths,
|
|
993
996
|
localProtocolOptions,
|
|
994
997
|
parentArtifactManager,
|
|
995
998
|
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
@@ -1048,6 +1051,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1048
1051
|
autoloadSkills: resolvedAutoloadSkills,
|
|
1049
1052
|
workspaceTree: this.session.workspaceTree,
|
|
1050
1053
|
promptTemplates,
|
|
1054
|
+
rules: this.session.rules,
|
|
1051
1055
|
localProtocolOptions,
|
|
1052
1056
|
parentArtifactManager,
|
|
1053
1057
|
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
package/src/tools/index.ts
CHANGED
|
@@ -3,10 +3,12 @@ import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
|
3
3
|
import type { FetchImpl, ToolChoice } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import type { AsyncJobManager } from "../async/job-manager";
|
|
6
|
+
import type { Rule } from "../capability/rule";
|
|
6
7
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
7
8
|
import type { Settings } from "../config/settings";
|
|
8
9
|
import { EditTool } from "../edit";
|
|
9
10
|
import { checkPythonKernelAvailability } from "../eval/py/kernel";
|
|
11
|
+
import type { ToolPathWithSource } from "../extensibility/custom-tools";
|
|
10
12
|
import type { Skill } from "../extensibility/skills";
|
|
11
13
|
import type { GoalModeState, GoalRuntime } from "../goals";
|
|
12
14
|
import { GoalTool } from "../goals/tools/goal-tool";
|
|
@@ -154,6 +156,21 @@ export interface ToolSession {
|
|
|
154
156
|
skills?: Skill[];
|
|
155
157
|
/** Pre-loaded prompt templates */
|
|
156
158
|
promptTemplates?: PromptTemplate[];
|
|
159
|
+
/** Pre-loaded rules (forwarded to subagents to skip re-discovery). */
|
|
160
|
+
rules?: Rule[];
|
|
161
|
+
/**
|
|
162
|
+
* Pre-discovered extension source paths. Forwarded to subagents so they
|
|
163
|
+
* skip the FS scan but still re-bind extensions to their own session-scoped
|
|
164
|
+
* `ExtensionAPI` (cwd, eventBus, runtime). Inline extension factories
|
|
165
|
+
* (`<inline-N>`) are NOT included — those are session-local.
|
|
166
|
+
*/
|
|
167
|
+
extensionPaths?: string[];
|
|
168
|
+
/**
|
|
169
|
+
* Pre-discovered custom-tool source paths from `.omp/tools/`, `.claude/tools/`,
|
|
170
|
+
* plugins, etc. Forwarded to subagents so they skip the FS scan but still
|
|
171
|
+
* re-bind tools to their own session-scoped `CustomToolAPI`.
|
|
172
|
+
*/
|
|
173
|
+
customToolPaths?: ToolPathWithSource[];
|
|
157
174
|
/** Whether LSP integrations are enabled */
|
|
158
175
|
enableLsp?: boolean;
|
|
159
176
|
/** Whether an edit-capable tool is available in this session (controls hashline output) */
|
package/src/tui/hyperlink.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
|
|
19
19
|
const OSC = "\x1b]";
|
|
20
20
|
const ST = "\x1b\\";
|
|
21
|
+
const BEL = "\x07";
|
|
21
22
|
|
|
22
23
|
/** Stable 8-char hex ID derived from a URI — hints terminals to coalesce identical adjacent links. */
|
|
23
24
|
function buildLinkId(uri: string): string {
|
|
@@ -60,14 +61,18 @@ function safeHyperlinkUri(uri: string): string | undefined {
|
|
|
60
61
|
return uri;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
function
|
|
64
|
-
if (!isHyperlinkEnabled()) return displayText;
|
|
64
|
+
function wrapHyperlinkCore(uri: string, displayText: string, terminator: typeof ST | typeof BEL): string {
|
|
65
65
|
// Do not double-wrap if the text already embeds an OSC 8 sequence.
|
|
66
66
|
if (displayText.includes("\x1b]8;")) return displayText;
|
|
67
67
|
const safeUri = safeHyperlinkUri(uri);
|
|
68
68
|
if (!safeUri) return displayText;
|
|
69
69
|
const id = buildLinkId(safeUri);
|
|
70
|
-
return `${OSC}8;id=${id};${safeUri}${
|
|
70
|
+
return `${OSC}8;id=${id};${safeUri}${terminator}${displayText}${OSC}8;;${terminator}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function wrapHyperlink(uri: string, displayText: string): string {
|
|
74
|
+
if (!isHyperlinkEnabled()) return displayText;
|
|
75
|
+
return wrapHyperlinkCore(uri, displayText, ST);
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
/**
|
|
@@ -95,6 +100,25 @@ export function urlHyperlink(url: string, displayText: string): string {
|
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Wrap `displayText` in an OSC 8 hyperlink pointing at an HTTP(S) URL,
|
|
105
|
+
* bypassing terminal capability auto-detection. Used for auth prompts where
|
|
106
|
+
* an inert "click" label blocks login on terminals whose capabilities are
|
|
107
|
+
* not advertised. Still returns plain text when the user has explicitly
|
|
108
|
+
* opted out via `tui.hyperlinks=off`.
|
|
109
|
+
*/
|
|
110
|
+
export function urlHyperlinkAlways(url: string, displayText: string): string {
|
|
111
|
+
if (settings.get("tui.hyperlinks") === "off") return displayText;
|
|
112
|
+
const normalized = url.match(/^www\./i) ? `https://${url}` : url;
|
|
113
|
+
try {
|
|
114
|
+
const parsed = new URL(normalized);
|
|
115
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return displayText;
|
|
116
|
+
return wrapHyperlinkCore(parsed.href, displayText, BEL);
|
|
117
|
+
} catch {
|
|
118
|
+
return displayText;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
98
122
|
/**
|
|
99
123
|
* Wrap `displayText` in an OSC 8 hyperlink pointing at a filesystem path.
|
|
100
124
|
*
|