@oh-my-pi/pi-coding-agent 17.0.2 → 17.0.4
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 +80 -20
- package/dist/cli.js +4087 -4108
- package/dist/types/config/settings-schema.d.ts +7 -3
- package/dist/types/eval/agent-bridge.d.ts +7 -19
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +15 -1
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/types.d.ts +3 -0
- package/dist/types/mcp/transports/stdio.d.ts +29 -0
- package/dist/types/mnemopi/state.d.ts +32 -9
- package/dist/types/modes/components/agent-hub.d.ts +15 -0
- package/dist/types/registry/persisted-agents.d.ts +3 -0
- package/dist/types/sdk.d.ts +14 -3
- package/dist/types/session/session-entries.d.ts +6 -1
- package/dist/types/session/session-manager.d.ts +5 -0
- package/dist/types/task/executor.d.ts +26 -1
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/parallel.d.ts +14 -0
- package/dist/types/task/structured-subagent.d.ts +111 -0
- package/dist/types/task/types.d.ts +51 -0
- package/dist/types/tools/fetch.d.ts +4 -5
- package/dist/types/tools/hub/messaging.d.ts +5 -1
- package/dist/types/tools/index.d.ts +16 -1
- package/dist/types/tools/todo.d.ts +31 -0
- package/dist/types/tui/tree-list.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +11 -0
- package/package.json +12 -12
- package/src/cli/file-processor.ts +1 -2
- package/src/config/settings-schema.ts +4 -3
- package/src/eval/__tests__/agent-bridge.test.ts +133 -47
- package/src/eval/__tests__/prelude-agent.test.ts +29 -0
- package/src/eval/agent-bridge.ts +104 -477
- package/src/eval/jl/prelude.jl +7 -6
- package/src/eval/js/shared/prelude.txt +5 -4
- package/src/eval/py/prelude.py +11 -39
- package/src/eval/rb/prelude.rb +5 -6
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +53 -7
- package/src/lsp/client.ts +57 -0
- package/src/lsp/index.ts +62 -6
- package/src/lsp/types.ts +3 -0
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/mcp/transports/stdio.test.ts +269 -1
- package/src/mcp/transports/stdio.ts +152 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/mnemopi/embed-worker.ts +8 -7
- package/src/mnemopi/state.ts +45 -18
- package/src/modes/components/agent-hub.ts +1 -72
- package/src/modes/components/bash-execution.ts +7 -3
- package/src/modes/components/eval-execution.ts +3 -1
- package/src/modes/components/transcript-container.ts +13 -7
- package/src/modes/interactive-mode.ts +30 -8
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +12 -8
- package/src/prompts/tools/write.md +1 -1
- package/src/registry/persisted-agents.ts +74 -0
- package/src/sdk.ts +129 -87
- package/src/session/agent-session.ts +75 -21
- package/src/session/session-entries.ts +6 -1
- package/src/session/session-loader.ts +31 -3
- package/src/session/session-manager.ts +9 -0
- package/src/session/streaming-output.ts +41 -1
- package/src/stt/recorder.ts +68 -55
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +99 -21
- package/src/task/index.ts +258 -429
- package/src/task/parallel.ts +43 -0
- package/src/task/persisted-revive.ts +19 -7
- package/src/task/structured-subagent.ts +642 -0
- package/src/task/types.ts +61 -0
- package/src/tools/__tests__/eval-description.test.ts +1 -1
- package/src/tools/fetch.ts +28 -105
- package/src/tools/hub/messaging.ts +16 -3
- package/src/tools/index.ts +47 -14
- package/src/tools/path-utils.ts +1 -0
- package/src/tools/read.ts +14 -26
- package/src/tools/todo.ts +126 -13
- package/src/tui/tree-list.ts +39 -0
- package/src/utils/markit.ts +12 -0
- package/src/utils/zip.ts +16 -1
package/src/sdk.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "./advisor";
|
|
35
35
|
import { type AsyncJob, AsyncJobManager } from "./async";
|
|
36
36
|
import { AutoLearnController, buildAutoLearnInstructions } from "./autolearn/controller";
|
|
37
|
+
import { createAutoresearchExtension } from "./autoresearch";
|
|
37
38
|
import { loadCapability } from "./capability";
|
|
38
39
|
import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
|
|
39
40
|
import { bucketRules } from "./capability/rule-buckets";
|
|
@@ -145,6 +146,7 @@ import {
|
|
|
145
146
|
} from "./system-prompt";
|
|
146
147
|
import { AgentOutputManager } from "./task/output-manager";
|
|
147
148
|
import { wrapStreamFnWithProviderConcurrency } from "./task/provider-concurrency";
|
|
149
|
+
import type { StructuredSubagentSchemaMode } from "./task/types";
|
|
148
150
|
import {
|
|
149
151
|
AUTO_THINKING,
|
|
150
152
|
type ConfiguredThinkingLevel,
|
|
@@ -482,20 +484,30 @@ export interface CreateAgentSessionOptions {
|
|
|
482
484
|
/** File-based slash commands. Default: discovered from commands/ directories */
|
|
483
485
|
slashCommands?: FileSlashCommand[];
|
|
484
486
|
|
|
485
|
-
/**
|
|
487
|
+
/**
|
|
488
|
+
* Enable MCP capabilities. `false` skips MCP discovery and ignores
|
|
489
|
+
* `mcpManager`, preventing process-global or inherited MCP access. Default:
|
|
490
|
+
* true.
|
|
491
|
+
*/
|
|
486
492
|
enableMCP?: boolean;
|
|
487
|
-
/** Existing MCP manager to reuse (skips discovery, propagates to toolSession). */
|
|
493
|
+
/** Existing MCP manager to reuse when MCP is enabled (skips discovery, propagates to toolSession). */
|
|
488
494
|
mcpManager?: MCPManager;
|
|
489
495
|
|
|
490
496
|
/** Enable LSP integration (tool, formatting, diagnostics, warmup). Default: true */
|
|
491
497
|
enableLsp?: boolean;
|
|
498
|
+
/** Whether this invocation may expose IRC. `false` removes it even for subagents. */
|
|
499
|
+
enableIrc?: boolean;
|
|
492
500
|
/** Skip subprocess-kernel availability checks and prelude warmup */
|
|
493
501
|
skipPythonPreflight?: boolean;
|
|
494
502
|
/** Tool names explicitly requested (enables disabled-by-default tools) */
|
|
495
503
|
toolNames?: string[];
|
|
504
|
+
/** Limit the session to explicitly supplied tool names, without discovered extras. */
|
|
505
|
+
restrictToolNames?: boolean;
|
|
496
506
|
|
|
497
|
-
/** Output schema for structured completion (subagents) */
|
|
507
|
+
/** Output schema for structured completion (subagents). */
|
|
498
508
|
outputSchema?: unknown;
|
|
509
|
+
/** Enforcement policy for {@link outputSchema}; defaults to legacy permissive behavior. */
|
|
510
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
499
511
|
/** Whether to include the yield tool by default */
|
|
500
512
|
requireYieldTool?: boolean;
|
|
501
513
|
/** Task recursion depth (for subagent sessions). Default: 0 */
|
|
@@ -1544,7 +1556,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1544
1556
|
let session!: AgentSession;
|
|
1545
1557
|
let hasSession = false;
|
|
1546
1558
|
let hasRegistered = false;
|
|
1547
|
-
const
|
|
1559
|
+
const restrictToolNames = options.restrictToolNames === true;
|
|
1560
|
+
const enableLsp = !restrictToolNames && (options.enableLsp ?? true);
|
|
1548
1561
|
const asyncMaxJobs = Math.min(100, Math.max(1, settings.get("async.maxJobs") ?? 100));
|
|
1549
1562
|
const ASYNC_INLINE_RESULT_MAX_CHARS = 12_000;
|
|
1550
1563
|
const ASYNC_PREVIEW_MAX_CHARS = 4_000;
|
|
@@ -1641,9 +1654,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1641
1654
|
setActiveToolNames,
|
|
1642
1655
|
hasUI: options.hasUI ?? false,
|
|
1643
1656
|
enableLsp,
|
|
1657
|
+
enableIrc: restrictToolNames ? false : options.enableIrc,
|
|
1658
|
+
restrictToolNames,
|
|
1644
1659
|
get hasEditTool() {
|
|
1645
1660
|
const requestedToolNames = options.toolNames ? normalizeToolNames(options.toolNames) : undefined;
|
|
1646
|
-
return
|
|
1661
|
+
return restrictToolNames
|
|
1662
|
+
? requestedToolNames?.includes("edit") === true
|
|
1663
|
+
: !requestedToolNames || requestedToolNames.includes("edit");
|
|
1647
1664
|
},
|
|
1648
1665
|
skipPythonPreflight: options.skipPythonPreflight,
|
|
1649
1666
|
contextFiles,
|
|
@@ -1655,6 +1672,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1655
1672
|
rules: allRules,
|
|
1656
1673
|
eventBus,
|
|
1657
1674
|
outputSchema: options.outputSchema,
|
|
1675
|
+
outputSchemaMode: options.outputSchemaMode,
|
|
1658
1676
|
requireYieldTool: options.requireYieldTool,
|
|
1659
1677
|
prewalkArmed: options.prewalk !== undefined,
|
|
1660
1678
|
taskDepth: options.taskDepth ?? 0,
|
|
@@ -1771,10 +1789,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1771
1789
|
// Create built-in tools (already wrapped with meta notice formatting)
|
|
1772
1790
|
const builtinTools = await logger.time("createAllTools", createTools, toolSession, options.toolNames);
|
|
1773
1791
|
|
|
1774
|
-
//
|
|
1775
|
-
|
|
1792
|
+
// Restricted sessions cannot inherit or discover MCP capabilities.
|
|
1793
|
+
const enableMCP = !restrictToolNames && (options.enableMCP ?? true);
|
|
1794
|
+
let mcpManager: MCPManager | undefined = enableMCP ? options.mcpManager : undefined;
|
|
1776
1795
|
toolSession.mcpManager = mcpManager;
|
|
1777
|
-
|
|
1796
|
+
toolSession.enableMCP = enableMCP;
|
|
1778
1797
|
const deferMCPDiscoveryForUI = enableMCP && !mcpManager && options.hasUI === true;
|
|
1779
1798
|
const customTools: CustomTool[] = [];
|
|
1780
1799
|
let startDeferredMCPDiscovery: ((liveSession: AgentSession) => void) | undefined;
|
|
@@ -1860,58 +1879,62 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1860
1879
|
// to mirror the AsyncJobManager ownership rule.
|
|
1861
1880
|
if (mcpManager && !options.parentTaskPrefix) MCPManager.setInstance(mcpManager);
|
|
1862
1881
|
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1882
|
+
const builtInToolNames = builtinTools.map(t => t.name);
|
|
1883
|
+
let customToolPaths: ToolPathWithSource[] = [];
|
|
1884
|
+
const inlineExtensions: ExtensionFactory[] = [];
|
|
1885
|
+
if (!restrictToolNames) {
|
|
1886
|
+
// Add image tools when generation is enabled and either no explicit tool
|
|
1887
|
+
// whitelist was given or it names `generate_image`. Unlike built-in tools
|
|
1888
|
+
// (filtered in `createTools`), custom tools are force-activated via
|
|
1889
|
+
// `alwaysInclude` below, so an explicit `--no-tools`/whitelist must be
|
|
1890
|
+
// honored here or image-gen would leak past every filter (issue #5305).
|
|
1891
|
+
const imageGenRequested = !options.toolNames || options.toolNames.includes("generate_image");
|
|
1892
|
+
if (settings.get("generate_image.enabled") && imageGenRequested) {
|
|
1893
|
+
const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model));
|
|
1894
|
+
if (imageGenTools.length > 0) {
|
|
1895
|
+
customTools.push(...(imageGenTools as unknown as CustomTool[]));
|
|
1896
|
+
}
|
|
1873
1897
|
}
|
|
1874
|
-
}
|
|
1875
1898
|
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1899
|
+
if (settings.get("speechgen.enabled")) {
|
|
1900
|
+
customTools.push(ttsTool as unknown as CustomTool);
|
|
1901
|
+
}
|
|
1879
1902
|
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1903
|
+
// Add web search tools
|
|
1904
|
+
if (options.toolNames?.includes("web_search")) {
|
|
1905
|
+
customTools.push(...getSearchTools());
|
|
1906
|
+
}
|
|
1884
1907
|
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1908
|
+
// Discover custom tools from `.omp/tools/`, `.claude/tools/`, plugins, etc.
|
|
1909
|
+
// Subagents reuse the parent's scan via `preloadedCustomToolPaths` to skip
|
|
1910
|
+
// the FS walk, but ALWAYS re-call `loadCustomTools` here so factories bind
|
|
1911
|
+
// to THIS session's `CustomToolAPI` (cwd, exec, pushPendingAction, UI).
|
|
1912
|
+
// Forwarding the parent's `LoadedCustomTool[]` directly would route tool
|
|
1913
|
+
// execution back through the parent — wrong for isolated tasks and for
|
|
1914
|
+
// pending-action queueing.
|
|
1915
|
+
customToolPaths =
|
|
1916
|
+
options.preloadedCustomToolPaths ??
|
|
1917
|
+
(await logger.time("discoverCustomToolPaths", () => discoverCustomToolPaths([], cwd)));
|
|
1918
|
+
const customToolsLoadResult = await logger.time("loadCustomTools", () =>
|
|
1919
|
+
loadCustomTools(customToolPaths, cwd, builtInToolNames, action => queueResolveHandler(toolSession, action)),
|
|
1920
|
+
);
|
|
1921
|
+
for (const { path, error } of customToolsLoadResult.errors) {
|
|
1922
|
+
logger.error("Custom tool load failed", { path, error });
|
|
1923
|
+
}
|
|
1924
|
+
if (customToolsLoadResult.tools.length > 0) {
|
|
1925
|
+
customTools.push(...customToolsLoadResult.tools.map(loaded => loaded.tool));
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
inlineExtensions.push(...(options.extensions ?? []));
|
|
1929
|
+
inlineExtensions.push(createAutoresearchExtension);
|
|
1930
|
+
if (customTools.length > 0) {
|
|
1931
|
+
inlineExtensions.push(createCustomToolsExtension(customTools));
|
|
1932
|
+
}
|
|
1904
1933
|
}
|
|
1905
1934
|
// Forward the path list (NOT the loaded tools) to subagents so they
|
|
1906
1935
|
// re-bind under their own `CustomToolAPI` while skipping the FS scan.
|
|
1907
1936
|
toolSession.customToolPaths = customToolPaths;
|
|
1908
1937
|
|
|
1909
|
-
const inlineExtensions: ExtensionFactory[] = options.extensions ? [...options.extensions] : [];
|
|
1910
|
-
inlineExtensions.push((await import("./autoresearch")).createAutoresearchExtension);
|
|
1911
|
-
if (customTools.length > 0) {
|
|
1912
|
-
inlineExtensions.push(createCustomToolsExtension(customTools));
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
1938
|
// Load extensions. Three paths:
|
|
1916
1939
|
// 1. `preloadedExtensions` (CLI): caller already loaded — reuse the
|
|
1917
1940
|
// Extension instances. Shallow-clone `extensions` so the inline
|
|
@@ -1926,7 +1949,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1926
1949
|
// the flag and pre-resolved the result already reflects that choice.
|
|
1927
1950
|
let extensionPaths: string[];
|
|
1928
1951
|
let extensionsResult: LoadExtensionsResult;
|
|
1929
|
-
if (
|
|
1952
|
+
if (restrictToolNames) {
|
|
1953
|
+
// Allocate a session runtime without evaluating caller-provided extension
|
|
1954
|
+
// instances, paths, or factories.
|
|
1955
|
+
extensionPaths = [];
|
|
1956
|
+
extensionsResult = await loadExtensions([], cwd, eventBus);
|
|
1957
|
+
} else if (options.preloadedExtensions) {
|
|
1930
1958
|
extensionsResult = {
|
|
1931
1959
|
...options.preloadedExtensions,
|
|
1932
1960
|
extensions: [...options.preloadedExtensions.extensions],
|
|
@@ -2235,11 +2263,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2235
2263
|
}
|
|
2236
2264
|
}
|
|
2237
2265
|
|
|
2238
|
-
//
|
|
2239
|
-
const customCommandsResult: CustomCommandsLoadResult =
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2266
|
+
// Restricted sessions do not discover or evaluate custom command modules.
|
|
2267
|
+
const customCommandsResult: CustomCommandsLoadResult =
|
|
2268
|
+
options.disableExtensionDiscovery || restrictToolNames
|
|
2269
|
+
? { commands: [], errors: [] }
|
|
2270
|
+
: await logger.time("discoverCustomCommands", loadCustomCommandsInternal, { cwd, agentDir });
|
|
2271
|
+
if (!options.disableExtensionDiscovery && !restrictToolNames) {
|
|
2243
2272
|
for (const { path, error } of customCommandsResult.errors) {
|
|
2244
2273
|
logger.error("Failed to load custom command", { path, error });
|
|
2245
2274
|
}
|
|
@@ -2284,8 +2313,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2284
2313
|
});
|
|
2285
2314
|
const toolContextStore = new ToolContextStore(getSessionContext);
|
|
2286
2315
|
|
|
2287
|
-
const registeredTools = extensionRunner.getAllRegisteredTools();
|
|
2288
|
-
const sdkCustomTools =
|
|
2316
|
+
const registeredTools = restrictToolNames ? [] : extensionRunner.getAllRegisteredTools();
|
|
2317
|
+
const sdkCustomTools = restrictToolNames
|
|
2318
|
+
? []
|
|
2319
|
+
: (options.customTools?.filter(tool => !isLegacyBuiltinToolDefinition(tool)) ?? []);
|
|
2289
2320
|
const allCustomTools = [
|
|
2290
2321
|
...registeredTools,
|
|
2291
2322
|
...sdkCustomTools.map(tool => {
|
|
@@ -2308,7 +2339,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2308
2339
|
toolRegistry.set(tool.name, tool);
|
|
2309
2340
|
builtInRegistryToolNames.add(tool.name);
|
|
2310
2341
|
}
|
|
2311
|
-
if (!toolRegistry.has("goal") && settings.get("goal.enabled")) {
|
|
2342
|
+
if (!restrictToolNames && !toolRegistry.has("goal") && settings.get("goal.enabled")) {
|
|
2312
2343
|
const goalTool = await logger.time("createTools:goal:session", HIDDEN_TOOLS.goal, toolSession);
|
|
2313
2344
|
if (goalTool) {
|
|
2314
2345
|
toolRegistry.set(goalTool.name, wrapToolWithMetaNotice(goalTool));
|
|
@@ -2361,7 +2392,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2361
2392
|
const hasDeferrableTools = Array.from(toolRegistry.values()).some(tool => tool.deferrable === true);
|
|
2362
2393
|
const hasXdevTools = (toolSession.xdevRegistry?.size ?? 0) > 0;
|
|
2363
2394
|
const planModeAvailable = settings.get("plan.enabled");
|
|
2364
|
-
if (hasDeferrableTools || hasXdevTools || planModeAvailable || deferMCPDiscoveryForUI) {
|
|
2395
|
+
if (!restrictToolNames && (hasDeferrableTools || hasXdevTools || planModeAvailable || deferMCPDiscoveryForUI)) {
|
|
2365
2396
|
await ensureWriteRegistered();
|
|
2366
2397
|
}
|
|
2367
2398
|
|
|
@@ -2401,8 +2432,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2401
2432
|
): Promise<BuildSystemPromptResult> => {
|
|
2402
2433
|
toolContextStore.setToolNames(toolNames);
|
|
2403
2434
|
const promptTools = buildSystemPromptToolMetadata(tools);
|
|
2404
|
-
const memoryBackend = await resolveMemoryBackend(settings);
|
|
2405
|
-
const memoryInstructions =
|
|
2435
|
+
const memoryBackend = restrictToolNames ? undefined : await resolveMemoryBackend(settings);
|
|
2436
|
+
const memoryInstructions = memoryBackend
|
|
2437
|
+
? await memoryBackend.buildDeveloperInstructions(agentDir, settings, session)
|
|
2438
|
+
: undefined;
|
|
2406
2439
|
|
|
2407
2440
|
// Build combined append prompt: memory instructions + auto-learn guidance
|
|
2408
2441
|
// + MCP server instructions. For UI sessions MCP discovery is deferred, so
|
|
@@ -2417,10 +2450,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2417
2450
|
// session-start build — so a subagent that filtered them out, a mid-session
|
|
2418
2451
|
// enable that never built them, or a same-named custom tool while auto-learn
|
|
2419
2452
|
// is off all get no guidance.
|
|
2420
|
-
const autoLearnInstructions =
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2453
|
+
const autoLearnInstructions = restrictToolNames
|
|
2454
|
+
? undefined
|
|
2455
|
+
: buildAutoLearnInstructions({
|
|
2456
|
+
manageSkill: builtInToolNames.includes("manage_skill"),
|
|
2457
|
+
learn: builtInToolNames.includes("learn"),
|
|
2458
|
+
});
|
|
2424
2459
|
const appendParts: string[] = [];
|
|
2425
2460
|
if (memoryInstructions) appendParts.push(memoryInstructions);
|
|
2426
2461
|
if (autoLearnInstructions) appendParts.push(autoLearnInstructions);
|
|
@@ -2452,7 +2487,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2452
2487
|
cwd,
|
|
2453
2488
|
xdevTools: toolSession.xdevRegistry?.entries() ?? [],
|
|
2454
2489
|
xdevDocs: toolSession.xdevRegistry?.docsAll() ?? "",
|
|
2455
|
-
autoQaEnabled: isAutoQaEnabled(settings),
|
|
2490
|
+
autoQaEnabled: !restrictToolNames && isAutoQaEnabled(settings),
|
|
2456
2491
|
resolvedCustomPrompt: options.customSystemPrompt,
|
|
2457
2492
|
skills: session?.skills ?? skills,
|
|
2458
2493
|
contextFiles,
|
|
@@ -2469,11 +2504,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2469
2504
|
eagerTasksAlways,
|
|
2470
2505
|
taskBatch: settings.get("task.batch"),
|
|
2471
2506
|
taskMaxConcurrency: settings.get("task.maxConcurrency"),
|
|
2472
|
-
taskIrcEnabled: isIrcEnabled(settings, options.taskDepth ?? 0),
|
|
2507
|
+
taskIrcEnabled: !restrictToolNames && isIrcEnabled(settings, options.taskDepth ?? 0),
|
|
2473
2508
|
secretsEnabled,
|
|
2474
2509
|
workspaceTree: workspaceTreePromise,
|
|
2475
2510
|
includeWorkspaceTree,
|
|
2476
|
-
memoryRootEnabled: memoryBackend
|
|
2511
|
+
memoryRootEnabled: memoryBackend?.id === "local",
|
|
2477
2512
|
model: getActiveModelString(),
|
|
2478
2513
|
includeModelInPrompt: settings.get("includeModelInPrompt"),
|
|
2479
2514
|
personality: agentKind === "sub" ? "none" : settings.get("personality"),
|
|
@@ -2514,7 +2549,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2514
2549
|
// exactly the builtins createTools built (`builtInToolNames` — provenance, so a
|
|
2515
2550
|
// same-named custom/extension tool is never force-activated when auto-learn is
|
|
2516
2551
|
// off) to keep guidance, controller, and the active set consistent.
|
|
2517
|
-
if (explicitlyRequestedToolNames) {
|
|
2552
|
+
if (!restrictToolNames && explicitlyRequestedToolNames) {
|
|
2518
2553
|
for (const name of ["manage_skill", "learn"]) {
|
|
2519
2554
|
if (builtInToolNames.includes(name) && !explicitlyRequestedToolNames.includes(name)) {
|
|
2520
2555
|
explicitlyRequestedToolNames.push(name);
|
|
@@ -2538,11 +2573,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2538
2573
|
: requestedActiveToolNames.filter(name => !defaultInactiveToolNames.has(name));
|
|
2539
2574
|
let initialToolNames = [...initialRequestedActiveToolNames];
|
|
2540
2575
|
|
|
2541
|
-
// Custom tools and extension-registered tools are always included regardless of toolNames filter
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2576
|
+
// Custom tools and extension-registered tools are always included regardless of toolNames filter.
|
|
2577
|
+
// Restricted callers own the list, so never widen it with registered tools.
|
|
2578
|
+
const alwaysInclude: string[] = restrictToolNames
|
|
2579
|
+
? []
|
|
2580
|
+
: [
|
|
2581
|
+
...sdkCustomTools.map(t => (isCustomTool(t) ? t.name : t.name)),
|
|
2582
|
+
...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
|
|
2583
|
+
];
|
|
2546
2584
|
for (const name of alwaysInclude) {
|
|
2547
2585
|
if (toolRegistry.has(name) && !initialToolNames.includes(name)) {
|
|
2548
2586
|
initialToolNames.push(name);
|
|
@@ -2712,6 +2750,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2712
2750
|
}
|
|
2713
2751
|
return result;
|
|
2714
2752
|
};
|
|
2753
|
+
const kimiApiFormatSetting = settings.get("providers.kimiApiFormat");
|
|
2754
|
+
const kimiApiFormat = kimiApiFormatSetting === "auto" ? undefined : kimiApiFormatSetting;
|
|
2715
2755
|
agent = new Agent({
|
|
2716
2756
|
initialState: {
|
|
2717
2757
|
systemPrompt,
|
|
@@ -2745,7 +2785,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2745
2785
|
presencePenalty: settings.get("presencePenalty") >= 0 ? settings.get("presencePenalty") : undefined,
|
|
2746
2786
|
repetitionPenalty: settings.get("repetitionPenalty") >= 0 ? settings.get("repetitionPenalty") : undefined,
|
|
2747
2787
|
hideThinkingSummary: settings.get("omitThinking"),
|
|
2748
|
-
kimiApiFormat
|
|
2788
|
+
kimiApiFormat,
|
|
2749
2789
|
preferWebsockets: preferOpenAICodexWebsockets,
|
|
2750
2790
|
getToolContext: tc => toolContextStore.getContext(tc),
|
|
2751
2791
|
getApiKey: requestModel => modelRegistry.resolver(requestModel, agent.sessionId),
|
|
@@ -3078,7 +3118,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3078
3118
|
serviceTierResolver: agent.serviceTierResolver,
|
|
3079
3119
|
hideThinkingSummary: agent.hideThinkingSummary,
|
|
3080
3120
|
maxRetryDelayMs: agent.maxRetryDelayMs,
|
|
3081
|
-
kimiApiFormat
|
|
3121
|
+
kimiApiFormat,
|
|
3082
3122
|
preferWebsockets: preferOpenAICodexWebsockets,
|
|
3083
3123
|
getToolContext: toolCall => toolContextStore.getContext(toolCall),
|
|
3084
3124
|
streamFn: settingsAwareStreamFn,
|
|
@@ -3111,15 +3151,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3111
3151
|
// and the tools; the fire-time re-check in `#onAgentEnd` still handles a
|
|
3112
3152
|
// mid-session DISABLE. The subscription lives for the session's lifetime; the
|
|
3113
3153
|
// reference is intentionally discarded (the listener retains it).
|
|
3114
|
-
if (
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3154
|
+
if (!restrictToolNames) {
|
|
3155
|
+
if (settings.get("autolearn.enabled") && taskDepth === 0) {
|
|
3156
|
+
await logger.time("startMemoryStartupTask", startMemoryBackend);
|
|
3157
|
+
new AutoLearnController({
|
|
3158
|
+
session,
|
|
3159
|
+
settings,
|
|
3160
|
+
capture: content => session.runAutolearnCapture(signal => runAutoLearnCapture(content, signal)),
|
|
3161
|
+
});
|
|
3162
|
+
} else {
|
|
3163
|
+
void logger.time("startMemoryStartupTask", startMemoryBackend);
|
|
3164
|
+
}
|
|
3123
3165
|
}
|
|
3124
3166
|
|
|
3125
3167
|
// Wire MCP manager callbacks to session for reactive tool updates.
|
|
@@ -2095,6 +2095,9 @@ export class AgentSession {
|
|
|
2095
2095
|
#xdevRegistry: XdevRegistry | undefined;
|
|
2096
2096
|
/** Names of discoverable tools currently mounted under `xd://` (dynamic mounts only, not built-in devices). */
|
|
2097
2097
|
#mountedXdevToolNames = new Set<string>();
|
|
2098
|
+
/** Coalesced xd:// mount delta not yet announced to the model; delivered as a
|
|
2099
|
+
* hidden notice alongside the next prompt (see {@link #notifyXdevMountDelta}). */
|
|
2100
|
+
#pendingXdevMountDelta: { added: Set<string>; removed: Set<string> } | undefined;
|
|
2098
2101
|
|
|
2099
2102
|
// TTSR manager for time-traveling stream rules
|
|
2100
2103
|
#ttsrManager: TtsrManager | undefined = undefined;
|
|
@@ -2242,6 +2245,17 @@ export class AgentSession {
|
|
|
2242
2245
|
* queue was consumed normally or a new turn already started. */
|
|
2243
2246
|
#drainStrandedQueuedMessages(): void {
|
|
2244
2247
|
if (this.#abortInProgress) return;
|
|
2248
|
+
// Session transitions (newSession/`/new`, compact, model-switch, session-switch,
|
|
2249
|
+
// dispose) call #disconnectFromAgent() BEFORE `await abort()`, so abort's own
|
|
2250
|
+
// finally lands here with no listener attached. Auto-resuming now would snapshot
|
|
2251
|
+
// the still-old context (the transition hasn't reached agent.reset() yet), start a
|
|
2252
|
+
// stale provider turn that races the reset, and — once reconnected — append its
|
|
2253
|
+
// output to the fresh session (issue #5800). A disconnected session never owns the
|
|
2254
|
+
// queue: the transition does. newSession/switchSession drop the queue (reset /
|
|
2255
|
+
// clearAllQueues), so nothing survives; compaction preserves it and re-drains itself
|
|
2256
|
+
// after #reconnectToAgent (see compact()'s finally); an explicit prompt flushes it
|
|
2257
|
+
// in every case.
|
|
2258
|
+
if (this.#unsubscribeAgent === undefined) return;
|
|
2245
2259
|
// A concern steered into a resumed streaming run after a user interrupt can
|
|
2246
2260
|
// strand at the turn tail (steered past the loop's final boundary poll). While
|
|
2247
2261
|
// that interrupt's suppression is still in effect, reclaim such advisor steers
|
|
@@ -7307,35 +7321,62 @@ export class AgentSession {
|
|
|
7307
7321
|
}
|
|
7308
7322
|
|
|
7309
7323
|
/**
|
|
7310
|
-
*
|
|
7311
|
-
*
|
|
7312
|
-
*
|
|
7313
|
-
*
|
|
7314
|
-
*
|
|
7315
|
-
*
|
|
7324
|
+
* Record a mid-session `xd://` mount delta for the model without rewriting
|
|
7325
|
+
* the system prompt: the prompt (and its provider cache prefix) stays
|
|
7326
|
+
* byte-stable across MCP connects and disconnects. The delta is NOT steered
|
|
7327
|
+
* immediately — a steered notice landing at a run's stop boundary (or while
|
|
7328
|
+
* the session is idle) forces an unsolicited extra assistant turn — it is
|
|
7329
|
+
* coalesced into {@link #pendingXdevMountDelta} and rides along with the
|
|
7330
|
+
* next prompt (docs + schema stay one `read xd://<tool>` away). The full
|
|
7331
|
+
* docs join the system prompt opportunistically on the next unrelated
|
|
7332
|
+
* rebuild.
|
|
7316
7333
|
*/
|
|
7317
7334
|
#notifyXdevMountDelta(previousMounted: ReadonlySet<string>): void {
|
|
7318
7335
|
const registry = this.#xdevRegistry;
|
|
7319
7336
|
if (!registry) return;
|
|
7320
7337
|
const current = this.#mountedXdevToolNames;
|
|
7321
7338
|
const addedNames = [...current].filter(name => !previousMounted.has(name));
|
|
7322
|
-
const
|
|
7323
|
-
if (addedNames.length === 0 &&
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7339
|
+
const removedNames = [...previousMounted].filter(name => !current.has(name));
|
|
7340
|
+
if (addedNames.length === 0 && removedNames.length === 0) return;
|
|
7341
|
+
// Coalesce against the unannounced delta: an unmount cancels a pending
|
|
7342
|
+
// mount the model never learned about, and a remount cancels a pending
|
|
7343
|
+
// unmount.
|
|
7344
|
+
const pending = this.#pendingXdevMountDelta ?? { added: new Set<string>(), removed: new Set<string>() };
|
|
7345
|
+
for (const name of addedNames) {
|
|
7346
|
+
if (!pending.removed.delete(name)) pending.added.add(name);
|
|
7347
|
+
}
|
|
7348
|
+
for (const name of removedNames) {
|
|
7349
|
+
if (!pending.added.delete(name)) pending.removed.add(name);
|
|
7350
|
+
}
|
|
7351
|
+
this.#pendingXdevMountDelta = pending.added.size > 0 || pending.removed.size > 0 ? pending : undefined;
|
|
7352
|
+
if (this.settings.get("startup.quiet")) return;
|
|
7353
|
+
const parts: string[] = [];
|
|
7354
|
+
if (addedNames.length > 0) parts.push(`mounted ${addedNames.join(", ")}`);
|
|
7355
|
+
if (removedNames.length > 0) parts.push(`unmounted ${removedNames.join(", ")}`);
|
|
7356
|
+
this.emitNotice("info", `xd://: ${parts.join("; ")}`, "xdev");
|
|
7357
|
+
}
|
|
7358
|
+
|
|
7359
|
+
/**
|
|
7360
|
+
* Render and consume the pending xd:// mount delta as a hidden notice, or
|
|
7361
|
+
* `undefined` when nothing unannounced is queued. Called from the prompt
|
|
7362
|
+
* paths so the notice rides along with user input instead of forcing its
|
|
7363
|
+
* own model turn.
|
|
7364
|
+
*/
|
|
7365
|
+
#takePendingXdevMountNotice(): CustomMessage | undefined {
|
|
7366
|
+
const pending = this.#pendingXdevMountDelta;
|
|
7367
|
+
if (!pending) return undefined;
|
|
7368
|
+
this.#pendingXdevMountDelta = undefined;
|
|
7369
|
+
const summaries = new Map(this.#xdevRegistry?.entries().map(entry => [entry.name, entry.summary]) ?? []);
|
|
7370
|
+
const added = [...pending.added].map(name => ({ name, summary: summaries.get(name) ?? "" }));
|
|
7371
|
+
const removed = [...pending.removed].map(name => ({ name }));
|
|
7372
|
+
return {
|
|
7327
7373
|
role: "custom",
|
|
7328
7374
|
customType: XDEV_MOUNT_NOTICE_MESSAGE_TYPE,
|
|
7329
7375
|
content: prompt.render(xdevMountNoticePrompt, { added, removed }),
|
|
7330
7376
|
attribution: "agent",
|
|
7331
7377
|
display: false,
|
|
7332
7378
|
timestamp: Date.now(),
|
|
7333
|
-
}
|
|
7334
|
-
if (this.settings.get("startup.quiet")) return;
|
|
7335
|
-
const parts: string[] = [];
|
|
7336
|
-
if (added.length > 0) parts.push(`mounted ${added.map(entry => entry.name).join(", ")}`);
|
|
7337
|
-
if (removed.length > 0) parts.push(`unmounted ${removed.map(entry => entry.name).join(", ")}`);
|
|
7338
|
-
this.emitNotice("info", `xd://: ${parts.join("; ")}`, "xdev");
|
|
7379
|
+
};
|
|
7339
7380
|
}
|
|
7340
7381
|
|
|
7341
7382
|
/**
|
|
@@ -8693,14 +8734,19 @@ export class AgentSession {
|
|
|
8693
8734
|
messages.push(...options.prependMessages);
|
|
8694
8735
|
}
|
|
8695
8736
|
|
|
8696
|
-
messages.push(message);
|
|
8697
|
-
|
|
8698
8737
|
// Early bail-out: if a newer abort/prompt cycle started during setup,
|
|
8699
8738
|
// return before mutating shared state (nextTurn messages, system prompt).
|
|
8700
8739
|
if (this.#promptGeneration !== generation) {
|
|
8701
8740
|
return;
|
|
8702
8741
|
}
|
|
8703
8742
|
|
|
8743
|
+
// A pending xd:// delta accompanies the next user-authored prompt,
|
|
8744
|
+
// never an agent-initiated continuation.
|
|
8745
|
+
const xdevMountNotice = isUserQueuedMessage(message) ? this.#takePendingXdevMountNotice() : undefined;
|
|
8746
|
+
if (xdevMountNotice) {
|
|
8747
|
+
messages.push(xdevMountNotice);
|
|
8748
|
+
}
|
|
8749
|
+
messages.push(message);
|
|
8704
8750
|
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
8705
8751
|
for (const msg of this.#pendingNextTurnMessages) {
|
|
8706
8752
|
messages.push(msg);
|
|
@@ -11014,6 +11060,13 @@ export class AgentSession {
|
|
|
11014
11060
|
this.#compactionAbortController = undefined;
|
|
11015
11061
|
}
|
|
11016
11062
|
this.#reconnectToAgent();
|
|
11063
|
+
// Compaction disconnected before `await abort()`, so abort's finally drain
|
|
11064
|
+
// (and any steer/follow-up that arrived mid-compaction — async IRC, an
|
|
11065
|
+
// `xd://` mount notice, an SDK/RPC steer) was suppressed while disconnected
|
|
11066
|
+
// (issue #5800). Unlike `/new`/switchSession, compaction preserves the agent
|
|
11067
|
+
// queues, so nothing else resumes them: re-drain now that the listener is back
|
|
11068
|
+
// and `isCompacting` is false, or the queued turn hangs until the next prompt.
|
|
11069
|
+
this.#drainStrandedQueuedMessages();
|
|
11017
11070
|
}
|
|
11018
11071
|
}
|
|
11019
11072
|
|
|
@@ -11912,11 +11965,12 @@ export class AgentSession {
|
|
|
11912
11965
|
#isEmptyAssistantStop(assistantMessage: AssistantMessage): boolean {
|
|
11913
11966
|
switch (assistantMessage.stopReason) {
|
|
11914
11967
|
case "stop":
|
|
11915
|
-
//
|
|
11916
|
-
//
|
|
11968
|
+
// Unsigned thinking alone is not actionable, but a signature is
|
|
11969
|
+
// provider-authenticated content and makes the stop terminal.
|
|
11917
11970
|
for (const content of assistantMessage.content) {
|
|
11918
11971
|
if (content.type === "toolCall") return false;
|
|
11919
11972
|
if (content.type === "text" && hasNonWhitespace(content.text)) return false;
|
|
11973
|
+
if (content.type === "thinking" && hasNonWhitespace(content.thinkingSignature ?? "")) return false;
|
|
11920
11974
|
}
|
|
11921
11975
|
return true;
|
|
11922
11976
|
case "toolUse":
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { ImageContent, MessageAttribution, ServiceTierByFamily, TextContent } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import type { StructuredSubagentSchemaMode } from "../task/types";
|
|
3
4
|
|
|
4
5
|
export const CURRENT_SESSION_VERSION = 3;
|
|
5
6
|
|
|
@@ -164,8 +165,12 @@ export interface SessionInitEntry extends SessionEntryBase {
|
|
|
164
165
|
task: string;
|
|
165
166
|
/** Tools available to the agent */
|
|
166
167
|
tools: string[];
|
|
167
|
-
/** Output schema if structured output was requested */
|
|
168
|
+
/** Output schema if structured output was requested. */
|
|
168
169
|
outputSchema?: unknown;
|
|
170
|
+
/** Enforcement policy recorded with the output schema for faithful revival. */
|
|
171
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
172
|
+
/** Whether revival must retain only the explicitly persisted tool names. */
|
|
173
|
+
restrictToolNames?: boolean;
|
|
169
174
|
/** Spawn allowlist the subagent ran with ("" = none, "*" = any, else CSV); absent on pre-spawns files. */
|
|
170
175
|
spawns?: string;
|
|
171
176
|
/** The agent's `readSummarize` setting (`false` = read summarization disabled); absent uses the session default. */
|
|
@@ -271,10 +271,38 @@ async function resolvePersistedBlobRefs(value: unknown, blobStore: BlobStore, ke
|
|
|
271
271
|
);
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Cheap synchronous precheck: does this value's tree contain any `blob:sha256:` string?
|
|
276
|
+
* Early-exits on the first hit and allocates no promises, so blob-free entries skip the
|
|
277
|
+
* async {@link resolvePersistedBlobRefs} descent entirely. Conservative — a blob ref in a
|
|
278
|
+
* non-resolved position still returns true, which only costs an extra (no-op) walk.
|
|
279
|
+
*/
|
|
280
|
+
function containsBlobRef(value: unknown): boolean {
|
|
281
|
+
if (typeof value === "string") return isBlobRef(value);
|
|
282
|
+
if (Array.isArray(value)) {
|
|
283
|
+
for (const item of value) {
|
|
284
|
+
if (containsBlobRef(item)) return true;
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
if (typeof value !== "object" || value === null) return false;
|
|
289
|
+
for (const key in value) {
|
|
290
|
+
if (containsBlobRef((value as Record<string, unknown>)[key])) return true;
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
274
295
|
export async function resolveBlobRefsInEntries(entries: FileEntry[], blobStore: BlobStore): Promise<void> {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
296
|
+
const pending: Promise<void>[] = [];
|
|
297
|
+
// Interleave precheck + initiation per entry so a positive entry begins resolution at the same
|
|
298
|
+
// relative point as the old filter+map schedule (no scan-all-first pass that could observe a
|
|
299
|
+
// later entry before an earlier resolution mutates it).
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
if (entry.type === "session") continue;
|
|
302
|
+
if (!containsBlobRef(entry)) continue;
|
|
303
|
+
pending.push(resolvePersistedBlobRefs(entry, blobStore));
|
|
304
|
+
}
|
|
305
|
+
await Promise.all(pending);
|
|
278
306
|
}
|
|
279
307
|
|
|
280
308
|
/**
|