@oh-my-pi/pi-coding-agent 17.0.2 → 17.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +70 -20
  2. package/dist/cli.js +4087 -4108
  3. package/dist/types/config/settings-schema.d.ts +7 -3
  4. package/dist/types/eval/agent-bridge.d.ts +7 -19
  5. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +15 -1
  6. package/dist/types/lsp/client.d.ts +2 -0
  7. package/dist/types/lsp/types.d.ts +3 -0
  8. package/dist/types/mcp/transports/stdio.d.ts +29 -0
  9. package/dist/types/modes/components/agent-hub.d.ts +15 -0
  10. package/dist/types/registry/persisted-agents.d.ts +3 -0
  11. package/dist/types/sdk.d.ts +14 -3
  12. package/dist/types/session/session-entries.d.ts +6 -1
  13. package/dist/types/session/session-manager.d.ts +5 -0
  14. package/dist/types/task/executor.d.ts +26 -1
  15. package/dist/types/task/index.d.ts +1 -1
  16. package/dist/types/task/parallel.d.ts +14 -0
  17. package/dist/types/task/structured-subagent.d.ts +111 -0
  18. package/dist/types/task/types.d.ts +51 -0
  19. package/dist/types/tools/fetch.d.ts +4 -5
  20. package/dist/types/tools/hub/messaging.d.ts +5 -1
  21. package/dist/types/tools/index.d.ts +16 -1
  22. package/dist/types/tools/todo.d.ts +31 -0
  23. package/dist/types/tui/tree-list.d.ts +7 -0
  24. package/dist/types/utils/markit.d.ts +11 -0
  25. package/package.json +12 -12
  26. package/src/cli/file-processor.ts +1 -2
  27. package/src/config/settings-schema.ts +4 -3
  28. package/src/eval/__tests__/agent-bridge.test.ts +133 -47
  29. package/src/eval/__tests__/prelude-agent.test.ts +29 -0
  30. package/src/eval/agent-bridge.ts +104 -477
  31. package/src/eval/jl/prelude.jl +7 -6
  32. package/src/eval/js/shared/prelude.txt +5 -4
  33. package/src/eval/py/prelude.py +11 -39
  34. package/src/eval/rb/prelude.rb +5 -6
  35. package/src/extensibility/legacy-pi-coding-agent-shim.ts +53 -7
  36. package/src/lsp/client.ts +57 -0
  37. package/src/lsp/index.ts +62 -6
  38. package/src/lsp/types.ts +3 -0
  39. package/src/mcp/oauth-flow.ts +20 -0
  40. package/src/mcp/transports/stdio.test.ts +269 -1
  41. package/src/mcp/transports/stdio.ts +152 -1
  42. package/src/modes/components/agent-hub.ts +1 -72
  43. package/src/modes/components/bash-execution.ts +7 -3
  44. package/src/modes/components/eval-execution.ts +3 -1
  45. package/src/modes/interactive-mode.ts +30 -8
  46. package/src/prompts/system/system-prompt.md +1 -1
  47. package/src/prompts/tools/eval.md +5 -2
  48. package/src/prompts/tools/read.md +1 -1
  49. package/src/prompts/tools/task.md +12 -8
  50. package/src/prompts/tools/write.md +1 -1
  51. package/src/registry/persisted-agents.ts +74 -0
  52. package/src/sdk.ts +129 -87
  53. package/src/session/agent-session.ts +75 -21
  54. package/src/session/session-entries.ts +6 -1
  55. package/src/session/session-manager.ts +9 -0
  56. package/src/session/streaming-output.ts +41 -1
  57. package/src/system-prompt.ts +7 -2
  58. package/src/task/executor.ts +99 -21
  59. package/src/task/index.ts +258 -429
  60. package/src/task/parallel.ts +43 -0
  61. package/src/task/persisted-revive.ts +19 -7
  62. package/src/task/structured-subagent.ts +642 -0
  63. package/src/task/types.ts +58 -0
  64. package/src/tools/__tests__/eval-description.test.ts +1 -1
  65. package/src/tools/fetch.ts +28 -105
  66. package/src/tools/hub/messaging.ts +16 -3
  67. package/src/tools/index.ts +47 -14
  68. package/src/tools/path-utils.ts +1 -0
  69. package/src/tools/read.ts +14 -26
  70. package/src/tools/todo.ts +126 -13
  71. package/src/tui/tree-list.ts +39 -0
  72. package/src/utils/markit.ts +12 -0
  73. package/src/utils/zip.ts +16 -1
@@ -17,9 +17,12 @@ write(path, content) → str
17
17
  env(key?=None, value?=None) → str | None | dict
18
18
  output(*ids, format?="raw", query?=None, offset?=None, limit?=None) → str | dict | list[dict]
19
19
  tool.<name>(args) → unknown
20
+ Invoke any session tool; `args` = its parameter object.
20
21
  completion(prompt, model?="default"|"smol"|"slow", system?=None, schema?=None) → str | dict
21
- {{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, schema?=None, handle?=False) str | dict{{#if spawnAllowedAgentsText}} Allowed: {{spawnAllowedAgentsText}}.{{/if}}
22
- {{#if js}} JS: agent(prompt, { agent, schema, handle }).{{/if}}
22
+ Oneshot, stateless (no history/tools). `model`: "smol" fast | "default" session | "slow" most capable. `schema` (JSON-Schema) → parsed object.
23
+ {{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, label?=None, schema?=None, schema{{#if js}}Mode{{else}}_mode{{/if}}?="permissive", isolated?=None, apply?=None, merge?=None, handle?=False) → str | dict
24
+ Run a subagent → final output. `agent` selects a discovered agent; omit it to use `{{spawnDefaultAgent}}`.{{#if spawnAllowedAgentsText}} Allowed agents: {{spawnAllowedAgentsText}}.{{/if}} `schema` overrides agent/session schemas; `schemaMode`/`schema_mode`: "permissive" | "strict". Effective schemas return parsed data. `isolated` requests a worktree; `apply`/`merge` control its changes. Background via `local://` files named in the prompt. `handle` → { text, output, handle: "agent://<id>", id, agent }, parsed `data` when structured.
25
+ {{#if js}} JS: ONE trailing object — agent(prompt, { agent, model, label, schema, schemaMode, isolated, apply, merge, handle }).{{/if}}
23
26
  {{/if}}
24
27
  parallel(thunks) → list pipeline(items, ...stages) → list
25
28
  log(message) → None phase(title) → None
@@ -15,7 +15,7 @@ Read files, directories, archives, SQLite, images, documents, internal resources
15
15
  - {{#if IS_HL_MODE}}File + selector → `[foo.ts#1A2B]` snapshot header + numbered lines. Copy `[FILENAME#TAG]` for anchored edits; NEVER fabricate the tag.{{/if}}
16
16
  - Directory → depth-limited dirent listing.
17
17
  - SQLite (`.sqlite`, `.sqlite3`, `.db`, `.db3`): `file.db` (tables), `file.db:table` (schema+rows), `file.db:table:key` (by PK), `?limit=`/`?where=`/`?q=SELECT`.
18
- - Archives (`.tar`, `.tar.gz`, `.tgz`, `.zip`): `archive.ext:path/inside/archive` reads a member.
18
+ - Archives (`.tar`, `.tar.gz`, `.tgz`, `.zip`, plus ZIP-based `.jar`/`.war`/`.ear`/`.apk`): `archive.ext:path/inside/archive` reads a member.
19
19
  - Documents → extracted text. Notebooks → editable cells. Images → {{#if INSPECT_IMAGE_ENABLED}}metadata; call `inspect_image`{{else}}decoded inline{{/if}}. `:raw` bypasses converters.
20
20
  - URLs → reader-mode clean text/markdown; `:raw` → untouched HTML. Bare `host:port` needs trailing slash.
21
21
  - Internal URIs — all schemes take selectors. `artifact://<id>` recovers spilled output; page with `:N-M`/`:raw:N-M`.
@@ -10,18 +10,22 @@ Agents marked BLOCKING run inline — results return in this call; non-blocking
10
10
 
11
11
  # Inputs
12
12
  {{#if batchEnabled}}
13
- - `context`: Shared project state for the entire batch don't duplicate into individual tasks.
14
- - `tasks[]`: Subagents to spawn.
15
- - `name`: CamelCase ≤32 chars (auto-generated if omitted).
16
- - `agent`: specialist type (optional).
17
- - `task`: Complete, self-contained instructions — no one-liners, no missing acceptance criteria.
13
+ - `context`: Shared project state, constraints, and contracts. Applies to the entire batch; do not duplicate this background into individual tasks.
14
+ - `tasks[]`: Array of subagents to spawn.
15
+ - `name`: A stable CamelCase identifier (≤32 chars), used to address the agent (IRC, job ids). Generated automatically if omitted.
16
+ - `agent`: The agent type running this item (e.g. `scout`, `reviewer`). Omitting it gives you the general-purpose worker (`{{defaultAgent}}`) — NEVER pass that name explicitly. Only omit it after checking the agent list below and finding no specialist that fits.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
17
+ - `task`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
18
+ - `outputSchema`: Invocation-specific JSON Schema. Overrides the selected agent and parent-session schemas.
19
+ - `schemaMode`: `"permissive"` (default) accepts a retry-exhausted invalid result with a warning; `"strict"` fails it.
18
20
  {{#if isolationEnabled}}
19
21
  - `isolated`: Run in dedicated worktree, return patches. Destroyed on completion, cannot be addressed afterward.
20
22
  {{/if}}
21
23
  {{else}}
22
- - `name`: CamelCase ≤32 chars (auto-generated if omitted).
23
- - `agent`: specialist type (optional).
24
- - `task`: Complete, self-contained instructions — no one-liners, no missing acceptance criteria.
24
+ - `name`: A stable CamelCase identifier (≤32 chars), used to address the agent (IRC, job ids). Generated automatically if omitted.
25
+ - `agent`: The agent type to spawn (e.g. `scout`, `reviewer`). Omitting it gives you the general-purpose worker (`{{defaultAgent}}`) — NEVER pass that name explicitly. Only omit it after checking the agent list below and finding no specialist that fits.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
26
+ - `task`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
27
+ - `outputSchema`: Invocation-specific JSON Schema. Overrides the selected agent and parent-session schemas.
28
+ - `schemaMode`: `"permissive"` (default) accepts a retry-exhausted invalid result with a warning; `"strict"` fails it.
25
29
  {{#if isolationEnabled}}
26
30
  - `isolated`: Run in dedicated worktree, return patches.
27
31
  {{/if}}
@@ -3,7 +3,7 @@ Creates or overwrites file at specified path.
3
3
  <conditions>
4
4
  - Creating new files explicitly required by task
5
5
  - Replacing entire file contents when editing would be more complex
6
- - Supports `.tar`, `.tar.gz`, `.tgz`, and `.zip` archive entries via `archive.ext:path/inside/archive`
6
+ - Supports `.tar`, `.tar.gz`, `.tgz`, `.zip`, and ZIP-based `.jar`/`.war`/`.ear`/`.apk` archive entries via `archive.ext:path/inside/archive`
7
7
  - Supports SQLite row operations via `db.sqlite:table` (insert), `db.sqlite:table:key` (update with JSON content, delete with empty content)
8
8
  </conditions>
9
9
 
@@ -0,0 +1,74 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { ADVISOR_TRANSCRIPT_FILENAME, isAdvisorTranscriptName } from "../advisor/transcript-recorder";
4
+ import { type AgentRegistry, MAIN_AGENT_ID } from "./agent-registry";
5
+
6
+ /** Register persisted subagent and advisor transcripts as parked registry refs. */
7
+ export async function registerPersistedSubagents(
8
+ registry: AgentRegistry,
9
+ sessionFile: string | null | undefined,
10
+ ): Promise<void> {
11
+ if (!sessionFile?.endsWith(".jsonl")) return;
12
+ const root = sessionFile.slice(0, -6);
13
+ await registerPersistedSubagentsFromDir(registry, root, undefined);
14
+ }
15
+
16
+ async function registerPersistedSubagentsFromDir(
17
+ registry: AgentRegistry,
18
+ dir: string,
19
+ parentId: string | undefined,
20
+ ): Promise<void> {
21
+ let entries: fs.Dirent[];
22
+ try {
23
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
24
+ } catch {
25
+ return;
26
+ }
27
+ for (const entry of entries) {
28
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl") || entry.name.includes(".bak")) continue;
29
+ const sessionFile = path.join(dir, entry.name);
30
+ // The advisor transcript is observability-only: register it as a non-peer
31
+ // `advisor` kind under its owning session so the Hub can show its read-only
32
+ // transcript, but it never joins agent-facing rosters and is not revivable.
33
+ if (isAdvisorTranscriptName(entry.name)) {
34
+ const owner = parentId ?? MAIN_AGENT_ID;
35
+ // `__advisor.jsonl` → the default advisor (no slug); `__advisor.<slug>.jsonl`
36
+ // → a named advisor, keyed and labeled by its slug.
37
+ const slug =
38
+ entry.name === ADVISOR_TRANSCRIPT_FILENAME ? "" : entry.name.slice("__advisor.".length, -".jsonl".length);
39
+ const advisorId = slug ? `${owner}/advisor:${slug}` : `${owner}/advisor`;
40
+ const displayName = slug ? `advisor:${slug}` : "advisor";
41
+ const existing = registry.get(advisorId);
42
+ // Never clobber a non-advisor ref that happens to share this id (a freak
43
+ // user task literally named `<owner>/advisor`): leave it, skip the advisor.
44
+ if (existing && existing.kind !== "advisor") continue;
45
+ if (existing?.sessionFile !== sessionFile) {
46
+ // The id is reused across `/new`; refresh it to the current session's file.
47
+ if (existing) registry.unregister(advisorId);
48
+ registry.register({
49
+ id: advisorId,
50
+ displayName,
51
+ kind: "advisor",
52
+ parentId: owner,
53
+ session: null,
54
+ sessionFile,
55
+ status: "parked",
56
+ });
57
+ }
58
+ continue;
59
+ }
60
+ const id = entry.name.slice(0, -6);
61
+ if (!registry.get(id)) {
62
+ registry.register({
63
+ id,
64
+ displayName: id,
65
+ kind: "sub",
66
+ parentId: parentId ?? MAIN_AGENT_ID,
67
+ session: null,
68
+ sessionFile,
69
+ status: "parked",
70
+ });
71
+ }
72
+ await registerPersistedSubagentsFromDir(registry, path.join(dir, id), id);
73
+ }
74
+ }
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
- /** Enable MCP server discovery from .mcp.json files. Default: true */
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 enableLsp = options.enableLsp ?? true;
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 !requestedToolNames || requestedToolNames.includes("edit");
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
- // Discover MCP tools from .mcp.json files
1775
- let mcpManager: MCPManager | undefined = options.mcpManager;
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
- const enableMCP = options.enableMCP ?? true;
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
- // Add image tools when generation is enabled and either no explicit tool
1864
- // whitelist was given or it names `generate_image`. Image gen is a
1865
- // discoverable custom tool: once it enters the registry the common
1866
- // partition presents it under xd:// (or routes it to BM25 discovery), so no
1867
- // source-specific force-activation is needed only this eligibility gate.
1868
- const imageGenRequested = !options.toolNames || options.toolNames.includes("generate_image");
1869
- if (settings.get("generate_image.enabled") && imageGenRequested) {
1870
- const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model));
1871
- if (imageGenTools.length > 0) {
1872
- customTools.push(...(imageGenTools as unknown as CustomTool[]));
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
- if (settings.get("speechgen.enabled")) {
1877
- customTools.push(ttsTool as unknown as CustomTool);
1878
- }
1899
+ if (settings.get("speechgen.enabled")) {
1900
+ customTools.push(ttsTool as unknown as CustomTool);
1901
+ }
1879
1902
 
1880
- // Add web search tools
1881
- if (options.toolNames?.includes("web_search")) {
1882
- customTools.push(...getSearchTools());
1883
- }
1903
+ // Add web search tools
1904
+ if (options.toolNames?.includes("web_search")) {
1905
+ customTools.push(...getSearchTools());
1906
+ }
1884
1907
 
1885
- // Discover custom tools from `.omp/tools/`, `.claude/tools/`, plugins, etc.
1886
- // Subagents reuse the parent's scan via `preloadedCustomToolPaths` to skip
1887
- // the FS walk, but ALWAYS re-call `loadCustomTools` here so factories bind
1888
- // to THIS session's `CustomToolAPI` (cwd, exec, pushPendingAction, UI).
1889
- // Forwarding the parent's `LoadedCustomTool[]` directly would route tool
1890
- // execution back through the parent — wrong for isolated tasks and for
1891
- // pending-action queueing.
1892
- const builtInToolNames = builtinTools.map(t => t.name);
1893
- const customToolPaths: ToolPathWithSource[] =
1894
- options.preloadedCustomToolPaths ??
1895
- (await logger.time("discoverCustomToolPaths", () => discoverCustomToolPaths([], cwd)));
1896
- const customToolsLoadResult = await logger.time("loadCustomTools", () =>
1897
- loadCustomTools(customToolPaths, cwd, builtInToolNames, action => queueResolveHandler(toolSession, action)),
1898
- );
1899
- for (const { path, error } of customToolsLoadResult.errors) {
1900
- logger.error("Custom tool load failed", { path, error });
1901
- }
1902
- if (customToolsLoadResult.tools.length > 0) {
1903
- customTools.push(...customToolsLoadResult.tools.map(loaded => loaded.tool));
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 (options.preloadedExtensions) {
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
- // Discover custom commands (TypeScript slash commands)
2239
- const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
2240
- ? { commands: [], errors: [] }
2241
- : await logger.time("discoverCustomCommands", loadCustomCommandsInternal, { cwd, agentDir });
2242
- if (!options.disableExtensionDiscovery) {
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 = options.customTools?.filter(tool => !isLegacyBuiltinToolDefinition(tool)) ?? [];
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 = await memoryBackend.buildDeveloperInstructions(agentDir, settings, session);
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 = buildAutoLearnInstructions({
2421
- manageSkill: builtInToolNames.includes("manage_skill"),
2422
- learn: builtInToolNames.includes("learn"),
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.id === "local",
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
- const alwaysInclude: string[] = [
2543
- ...sdkCustomTools.map(t => (isCustomTool(t) ? t.name : t.name)),
2544
- ...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
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: settings.get("providers.kimiApiFormat") ?? "anthropic",
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: settings.get("providers.kimiApiFormat") ?? "anthropic",
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 (settings.get("autolearn.enabled") && taskDepth === 0) {
3115
- await logger.time("startMemoryStartupTask", startMemoryBackend);
3116
- new AutoLearnController({
3117
- session,
3118
- settings,
3119
- capture: content => session.runAutolearnCapture(signal => runAutoLearnCapture(content, signal)),
3120
- });
3121
- } else {
3122
- void logger.time("startMemoryStartupTask", startMemoryBackend);
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.