@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.
Files changed (80) hide show
  1. package/CHANGELOG.md +80 -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/mnemopi/state.d.ts +32 -9
  10. package/dist/types/modes/components/agent-hub.d.ts +15 -0
  11. package/dist/types/registry/persisted-agents.d.ts +3 -0
  12. package/dist/types/sdk.d.ts +14 -3
  13. package/dist/types/session/session-entries.d.ts +6 -1
  14. package/dist/types/session/session-manager.d.ts +5 -0
  15. package/dist/types/task/executor.d.ts +26 -1
  16. package/dist/types/task/index.d.ts +1 -1
  17. package/dist/types/task/parallel.d.ts +14 -0
  18. package/dist/types/task/structured-subagent.d.ts +111 -0
  19. package/dist/types/task/types.d.ts +51 -0
  20. package/dist/types/tools/fetch.d.ts +4 -5
  21. package/dist/types/tools/hub/messaging.d.ts +5 -1
  22. package/dist/types/tools/index.d.ts +16 -1
  23. package/dist/types/tools/todo.d.ts +31 -0
  24. package/dist/types/tui/tree-list.d.ts +7 -0
  25. package/dist/types/utils/markit.d.ts +11 -0
  26. package/package.json +12 -12
  27. package/src/cli/file-processor.ts +1 -2
  28. package/src/config/settings-schema.ts +4 -3
  29. package/src/eval/__tests__/agent-bridge.test.ts +133 -47
  30. package/src/eval/__tests__/prelude-agent.test.ts +29 -0
  31. package/src/eval/agent-bridge.ts +104 -477
  32. package/src/eval/jl/prelude.jl +7 -6
  33. package/src/eval/js/shared/prelude.txt +5 -4
  34. package/src/eval/py/prelude.py +11 -39
  35. package/src/eval/rb/prelude.rb +5 -6
  36. package/src/extensibility/legacy-pi-coding-agent-shim.ts +53 -7
  37. package/src/lsp/client.ts +57 -0
  38. package/src/lsp/index.ts +62 -6
  39. package/src/lsp/types.ts +3 -0
  40. package/src/mcp/oauth-flow.ts +20 -0
  41. package/src/mcp/transports/stdio.test.ts +269 -1
  42. package/src/mcp/transports/stdio.ts +152 -1
  43. package/src/mnemopi/backend.ts +1 -1
  44. package/src/mnemopi/embed-worker.ts +8 -7
  45. package/src/mnemopi/state.ts +45 -18
  46. package/src/modes/components/agent-hub.ts +1 -72
  47. package/src/modes/components/bash-execution.ts +7 -3
  48. package/src/modes/components/eval-execution.ts +3 -1
  49. package/src/modes/components/transcript-container.ts +13 -7
  50. package/src/modes/interactive-mode.ts +30 -8
  51. package/src/prompts/system/system-prompt.md +1 -1
  52. package/src/prompts/tools/eval.md +5 -2
  53. package/src/prompts/tools/read.md +1 -1
  54. package/src/prompts/tools/task.md +12 -8
  55. package/src/prompts/tools/write.md +1 -1
  56. package/src/registry/persisted-agents.ts +74 -0
  57. package/src/sdk.ts +129 -87
  58. package/src/session/agent-session.ts +75 -21
  59. package/src/session/session-entries.ts +6 -1
  60. package/src/session/session-loader.ts +31 -3
  61. package/src/session/session-manager.ts +9 -0
  62. package/src/session/streaming-output.ts +41 -1
  63. package/src/stt/recorder.ts +68 -55
  64. package/src/system-prompt.ts +7 -2
  65. package/src/task/executor.ts +99 -21
  66. package/src/task/index.ts +258 -429
  67. package/src/task/parallel.ts +43 -0
  68. package/src/task/persisted-revive.ts +19 -7
  69. package/src/task/structured-subagent.ts +642 -0
  70. package/src/task/types.ts +61 -0
  71. package/src/tools/__tests__/eval-description.test.ts +1 -1
  72. package/src/tools/fetch.ts +28 -105
  73. package/src/tools/hub/messaging.ts +16 -3
  74. package/src/tools/index.ts +47 -14
  75. package/src/tools/path-utils.ts +1 -0
  76. package/src/tools/read.ts +14 -26
  77. package/src/tools/todo.ts +126 -13
  78. package/src/tui/tree-list.ts +39 -0
  79. package/src/utils/markit.ts +12 -0
  80. package/src/utils/zip.ts +16 -1
@@ -5,6 +5,32 @@ import type { ConfiguredThinkingLevel } from "../thinking.js";
5
5
  import type { NestedRepoPatch } from "./worktree.js";
6
6
  /** Source of an agent definition */
7
7
  export type AgentSource = "bundled" | "user" | "project";
8
+ /**
9
+ * Enforcement policy for a structured subagent output schema.
10
+ *
11
+ * `permissive` preserves legacy retry-budget overrides; `strict` turns every
12
+ * invalid final payload, including an exhausted retry override, into a failed
13
+ * `schema_violation` result.
14
+ */
15
+ export type StructuredSubagentSchemaMode = "permissive" | "strict";
16
+ /** Origin of the schema selected for a structured subagent invocation. */
17
+ export type StructuredSubagentSchemaSource = "caller" | "agent" | "session" | "none";
18
+ /** Final validation state of a structured subagent invocation. */
19
+ export type StructuredSubagentValidationStatus = "valid" | "invalid" | "unavailable";
20
+ /**
21
+ * Parsed structured completion and its schema-validation metadata.
22
+ *
23
+ * `data` is present whenever a payload could be assembled or parsed, even when
24
+ * strict validation rejects it. `error` explains unavailable or invalid
25
+ * validation without requiring consumers to parse presentation text.
26
+ */
27
+ export interface StructuredSubagentOutput {
28
+ source: StructuredSubagentSchemaSource;
29
+ mode: StructuredSubagentSchemaMode;
30
+ status: StructuredSubagentValidationStatus;
31
+ data?: unknown;
32
+ error?: string;
33
+ }
8
34
  /** Maximum output bytes per agent */
9
35
  export declare const MAX_OUTPUT_BYTES: number;
10
36
  /** Maximum output lines per agent */
@@ -57,6 +83,8 @@ export declare const taskItemSchema: import("arktype/internal/variants/object.ts
57
83
  name?: string | undefined;
58
84
  agent: import("arktype/internal/attributes.ts").Default<string, "task">;
59
85
  task: string;
86
+ outputSchema?: string | boolean | object | null | undefined;
87
+ schemaMode?: "permissive" | "strict" | undefined;
60
88
  }, {}>;
61
89
  /** Single task item. Fields are optional defensively: args stream in token by token. */
62
90
  export interface TaskItem {
@@ -66,6 +94,10 @@ export interface TaskItem {
66
94
  agent?: string;
67
95
  /** The work; required by the schema. */
68
96
  task?: string;
97
+ /** Caller-provided output schema; its presence overrides the selected agent's schema. */
98
+ outputSchema?: unknown;
99
+ /** Validation behavior for a caller-provided or inherited output schema. */
100
+ schemaMode?: "permissive" | "strict";
69
101
  /** Run this spawn in an isolated worktree (batch form; flat form carries it top-level). */
70
102
  isolated?: boolean;
71
103
  }
@@ -73,23 +105,31 @@ export declare const taskSchema: import("arktype/internal/variants/object.ts").O
73
105
  name?: string | undefined;
74
106
  agent: import("arktype/internal/attributes.ts").Default<string, "task">;
75
107
  task: string;
108
+ outputSchema?: string | boolean | object | null | undefined;
109
+ schemaMode?: "permissive" | "strict" | undefined;
76
110
  isolated?: boolean | undefined;
77
111
  }, {}>;
78
112
  declare const ALL_TASK_SCHEMAS: readonly [import("arktype/internal/variants/object.ts").ObjectType<{
79
113
  name?: string | undefined;
80
114
  agent: import("arktype/internal/attributes.ts").Default<string, "task">;
81
115
  task: string;
116
+ outputSchema?: string | boolean | object | null | undefined;
117
+ schemaMode?: "permissive" | "strict" | undefined;
82
118
  isolated?: boolean | undefined;
83
119
  }, {}>, import("arktype/internal/variants/object.ts").ObjectType<{
84
120
  name?: string | undefined;
85
121
  agent: import("arktype/internal/attributes.ts").Default<string, "task">;
86
122
  task: string;
123
+ outputSchema?: string | boolean | object | null | undefined;
124
+ schemaMode?: "permissive" | "strict" | undefined;
87
125
  }, {}>, import("arktype/internal/variants/object.ts").ObjectType<{
88
126
  context: string;
89
127
  tasks: {
90
128
  name?: string | undefined;
91
129
  agent: import("arktype/internal/attributes.ts").Default<string, "task">;
92
130
  task: string;
131
+ outputSchema?: string | boolean | object | null | undefined;
132
+ schemaMode?: "permissive" | "strict" | undefined;
93
133
  isolated?: boolean | undefined;
94
134
  }[];
95
135
  }, {}>, import("arktype/internal/variants/object.ts").ObjectType<{
@@ -98,6 +138,8 @@ declare const ALL_TASK_SCHEMAS: readonly [import("arktype/internal/variants/obje
98
138
  name?: string | undefined;
99
139
  agent: import("arktype/internal/attributes.ts").Default<string, "task">;
100
140
  task: string;
141
+ outputSchema?: string | boolean | object | null | undefined;
142
+ schemaMode?: "permissive" | "strict" | undefined;
101
143
  }[];
102
144
  }, {}>];
103
145
  type DynamicTaskSchema = (typeof ALL_TASK_SCHEMAS)[number];
@@ -126,6 +168,10 @@ export interface TaskParams {
126
168
  agent?: string;
127
169
  /** The work (flat form). */
128
170
  task?: string;
171
+ /** Caller-provided output schema; its presence overrides the selected agent's schema. */
172
+ outputSchema?: unknown;
173
+ /** Validation behavior for a caller-provided or inherited output schema. */
174
+ schemaMode?: "permissive" | "strict";
129
175
  /** Batch form (`task.batch`): one subagent per item. */
130
176
  tasks?: TaskItem[];
131
177
  /** Batch form: shared background prepended to every assignment; required by the batch schema. */
@@ -294,6 +340,11 @@ export interface SingleResult {
294
340
  output: string;
295
341
  stderr: string;
296
342
  truncated: boolean;
343
+ /**
344
+ * Parsed structured completion and validation metadata, when this invocation
345
+ * selected an output schema or strict schema mode.
346
+ */
347
+ structuredOutput?: StructuredSubagentOutput;
297
348
  durationMs: number;
298
349
  /** Cumulative input + output + cacheWrite tokens across all turns. Excludes cacheRead (re-reads cached context every turn, making cumulative sum misleading). */
299
350
  tokens: number;
@@ -53,22 +53,21 @@ export interface ReadUrlToolDetails {
53
53
  notes: string[];
54
54
  meta?: OutputMeta;
55
55
  }
56
- interface ReadUrlCacheEntry {
56
+ interface ReadUrlEntry {
57
57
  artifactId?: string;
58
58
  artifactPath?: string;
59
- contentPath?: string;
60
59
  details: ReadUrlToolDetails;
61
60
  image?: FetchImagePayload;
62
61
  output: string;
63
62
  content: string;
64
63
  }
65
- export declare function loadReadUrlCacheEntry(session: ToolSession, params: {
64
+ /** Fetch and render a URL for a read or search operation. */
65
+ export declare function fetchReadUrl(session: ToolSession, params: {
66
66
  path: string;
67
67
  raw?: boolean;
68
68
  }, signal?: AbortSignal, options?: {
69
69
  ensureArtifact?: boolean;
70
- preferCached?: boolean;
71
- }): Promise<ReadUrlCacheEntry>;
70
+ }): Promise<ReadUrlEntry>;
72
71
  /** Materialize rendered URL body text to a local file for tools that require filesystem paths. */
73
72
  export declare function materializeReadUrlToFile(session: ToolSession, params: {
74
73
  path: string;
@@ -32,7 +32,11 @@ export declare function resolveMessageTimeoutMs(settings: Settings, explicit?: n
32
32
  export declare function drainPendingInbox(registry: AgentRegistry, senderId: string, from?: string): IrcMessage | undefined;
33
33
  /** `wait` result carrying a consumed message. */
34
34
  export declare function messageResult(senderId: string, waited: IrcMessage): AgentToolResult<CoordinationDetails>;
35
- export declare function executeList(registry: AgentRegistry, senderId: string): AgentToolResult<CoordinationDetails>;
35
+ /**
36
+ * List every addressable peer, restoring parked refs from disk when a resumed
37
+ * session has no in-memory roster.
38
+ */
39
+ export declare function executeList(registry: AgentRegistry, senderId: string): Promise<AgentToolResult<CoordinationDetails>>;
36
40
  export interface HubSendParams {
37
41
  to?: string;
38
42
  message?: string;
@@ -20,6 +20,7 @@ import type { CustomMessage } from "../session/messages.js";
20
20
  import type { UsageStatistics } from "../session/session-entries.js";
21
21
  import type { ToolChoiceQueue } from "../session/tool-choice-queue.js";
22
22
  import type { AgentOutputManager } from "../task/output-manager.js";
23
+ import { type StructuredSubagentSchemaMode } from "../task/types.js";
23
24
  import type { EventBus } from "../utils/event-bus.js";
24
25
  import type { WorkspaceTree } from "../workspace-tree.js";
25
26
  import { type BuiltinToolName, type HiddenToolName } from "./builtin-names.js";
@@ -144,17 +145,31 @@ export interface ToolSession {
144
145
  customToolPaths?: ToolPathWithSource[];
145
146
  /** Whether LSP integrations are enabled */
146
147
  enableLsp?: boolean;
148
+ /** Whether this invocation may expose IRC. `false` removes it even for subagents. */
149
+ enableIrc?: boolean;
150
+ /**
151
+ * Whether MCP capabilities may be forwarded to child sessions. `false`
152
+ * prohibits inherited-manager and process-global MCP fallback.
153
+ */
154
+ enableMCP?: boolean;
147
155
  /** Whether an edit-capable tool is available in this session (controls hashline output) */
148
156
  hasEditTool?: boolean;
149
157
  /** Event bus for tool/extension communication */
150
158
  eventBus?: EventBus;
151
- /** Output schema for structured completion (subagents) */
159
+ /** Output schema for structured completion (subagents). */
152
160
  outputSchema?: unknown;
161
+ /** Enforcement policy for {@link outputSchema}; defaults to legacy permissive behavior. */
162
+ outputSchemaMode?: StructuredSubagentSchemaMode;
153
163
  /** Whether to include the yield tool by default */
154
164
  requireYieldTool?: boolean;
155
165
  /** Session starts with a prewalk hand-off armed. Keeps `todo` in yield-gated
156
166
  * (subagent) registries: the prewalk plan nudge + todo gate need it. */
157
167
  prewalkArmed?: boolean;
168
+ /**
169
+ * Constrain the active set to the caller's explicit built-in names (plus a
170
+ * required yield tool). Suppresses automatic tool-set expansion.
171
+ */
172
+ restrictToolNames?: boolean;
158
173
  /** Task recursion depth (0 = top-level, 1 = first child, etc.) */
159
174
  taskDepth?: number;
160
175
  /** Get shared eval executor session ID. Subagents inherit this to share JS/Python/Ruby/Julia state. */
@@ -57,6 +57,35 @@ export declare function getLatestTodoPhasesFromEntries(entries: SessionEntry[]):
57
57
  * the contained side.
58
58
  */
59
59
  export declare function todoMatchesAnyDescription(content: string, descriptions: readonly string[]): boolean;
60
+ /** Result of {@link selectCollapsedTodos}: the rows to render plus an optional
61
+ * summary line (empty string ⇒ no summary row). */
62
+ export interface CollapsedTodoSelection<T> {
63
+ items: T[];
64
+ summary: string;
65
+ }
66
+ /**
67
+ * Walking-viewport selection for a phase's collapsed todo preview (#5873).
68
+ *
69
+ * Policy, applied to `tasks` in todo order:
70
+ * 1. While the phase has open work, completed/abandoned tasks are omitted. A
71
+ * phase with no open tasks left falls back to its closed tasks so the sticky
72
+ * HUD's closed-todo persistence still has something to render.
73
+ * 2. Every active task (in-progress, or pending matched to a live subagent) is
74
+ * placed at the head in stable todo order — never dropped for lying outside
75
+ * an ordinary window.
76
+ * 3. Remaining rows up to `cap` are filled with the pending tasks that follow
77
+ * the first active one, in todo order (falling back to leading pending tasks
78
+ * when no active task exists), so a freshly-promoted task leads the preview.
79
+ * 4. When active tasks alone exceed `cap`, only the first `cap` active tasks are
80
+ * shown and the summary counts the hidden *active* todos, never replacing
81
+ * them with unrelated pending rows.
82
+ *
83
+ * The summary otherwise counts the remaining tasks in the display base. Returns
84
+ * the whole base with an empty summary when it already fits.
85
+ */
86
+ export declare function selectCollapsedTodos<T extends {
87
+ status: TodoStatus;
88
+ }>(tasks: T[], isMatched: (task: T) => boolean, cap: number): CollapsedTodoSelection<T>;
60
89
  /** Apply an array of `todo`-style ops to existing phases. Used by /todo slash command. */
61
90
  export declare function applyOpsToPhases(currentPhases: TodoPhase[], ops: TodoParams[]): {
62
91
  phases: TodoPhase[];
@@ -111,6 +140,8 @@ export declare function formatPhaseDisplayName(name: string, oneBasedIndex: numb
111
140
  export declare const TODO_STRIKE_HOLD_FRAMES = 2;
112
141
  export declare const TODO_STRIKE_REVEAL_FRAMES = 12;
113
142
  export declare const TODO_STRIKE_TOTAL_FRAMES: number;
143
+ /** Wire the live-subagent description source for {@link todoToolRenderer}. */
144
+ export declare function setActiveTodoDescriptionsProvider(provider: () => readonly string[]): void;
114
145
  export declare const todoToolRenderer: {
115
146
  renderCall(args: TodoRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component;
116
147
  renderResult(result: {
@@ -13,6 +13,13 @@ export interface TreeListOptions<T> {
13
13
  maxCollapsedLines?: number;
14
14
  itemType?: string;
15
15
  truncateFrom?: "start" | "end";
16
+ /** Caller-supplied trailing summary line. When set (and not expanded),
17
+ * `renderTreeList` renders exactly the provided `items` (the caller has
18
+ * already applied its own selection/cap) and appends this text as the
19
+ * final `└` row, with the last item using `├`. Empty string renders the
20
+ * items with no summary. Bypasses the built-in truncation/`maxCollapsed`
21
+ * path. */
22
+ trailingSummary?: string;
16
23
  /** Called once per item with `isLast: false` during budget calculation;
17
24
  * line count MUST NOT vary based on `isLast`. */
18
25
  renderItem: (item: T, context: TreeContext) => string | string[];
@@ -1,4 +1,15 @@
1
1
  import { type MarkitConversionCacheStatus } from "./markit-cache.js";
2
+ /**
3
+ * File extensions markit can actually convert to markdown — one per registered
4
+ * converter in `src/markit/registry.ts` (pdf, docx, pptx, xlsx, epub). This is
5
+ * the single source of truth shared by the read, fetch, and CLI file tools so
6
+ * the advertised set never drifts from the converters that back it. Legacy
7
+ * binary formats (`.doc`, `.ppt`, `.xls`, `.rtf`) are intentionally absent:
8
+ * markit has no converter for them, so routing them here only produced an
9
+ * `Unsupported format` error instead of letting them fall through to the
10
+ * binary-file handling.
11
+ */
12
+ export declare const CONVERTIBLE_EXTENSIONS: ReadonlySet<string>;
2
13
  export interface MarkitConversionResult {
3
14
  content: string;
4
15
  ok: boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "17.0.2",
4
+ "version": "17.0.4",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -52,17 +52,17 @@
52
52
  "@agentclientprotocol/sdk": "1.2.1",
53
53
  "@babel/parser": "^7.29.7",
54
54
  "@mozilla/readability": "^0.6.0",
55
- "@oh-my-pi/hashline": "17.0.2",
56
- "@oh-my-pi/omp-stats": "17.0.2",
57
- "@oh-my-pi/pi-agent-core": "17.0.2",
58
- "@oh-my-pi/pi-ai": "17.0.2",
59
- "@oh-my-pi/pi-catalog": "17.0.2",
60
- "@oh-my-pi/pi-mnemopi": "17.0.2",
61
- "@oh-my-pi/pi-natives": "17.0.2",
62
- "@oh-my-pi/pi-tui": "17.0.2",
63
- "@oh-my-pi/pi-utils": "17.0.2",
64
- "@oh-my-pi/pi-wire": "17.0.2",
65
- "@oh-my-pi/snapcompact": "17.0.2",
55
+ "@oh-my-pi/hashline": "17.0.4",
56
+ "@oh-my-pi/omp-stats": "17.0.4",
57
+ "@oh-my-pi/pi-agent-core": "17.0.4",
58
+ "@oh-my-pi/pi-ai": "17.0.4",
59
+ "@oh-my-pi/pi-catalog": "17.0.4",
60
+ "@oh-my-pi/pi-mnemopi": "17.0.4",
61
+ "@oh-my-pi/pi-natives": "17.0.4",
62
+ "@oh-my-pi/pi-tui": "17.0.4",
63
+ "@oh-my-pi/pi-utils": "17.0.4",
64
+ "@oh-my-pi/pi-wire": "17.0.4",
65
+ "@oh-my-pi/snapcompact": "17.0.4",
66
66
  "@opentelemetry/api": "^1.9.1",
67
67
  "@opentelemetry/api-logs": "^0.220.0",
68
68
  "@opentelemetry/context-async-hooks": "^2.9.0",
@@ -9,13 +9,12 @@ import chalk from "chalk";
9
9
  import { resolveReadPath } from "../tools/path-utils";
10
10
  import { formatBytes } from "../tools/render-utils";
11
11
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
12
- import { convertFileWithMarkit } from "../utils/markit";
12
+ import { CONVERTIBLE_EXTENSIONS, convertFileWithMarkit } from "../utils/markit";
13
13
 
14
14
  // Keep CLI startup responsive and avoid OOM when users pass huge files.
15
15
  // If a file exceeds these limits, we include it as a path-only <file/> block.
16
16
  const MAX_CLI_TEXT_BYTES = 5 * 1024 * 1024; // 5MB
17
17
  const MAX_CLI_IMAGE_BYTES = 25 * 1024 * 1024; // 25MB
18
- const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
19
18
 
20
19
  export interface ProcessedFiles {
21
20
  text: string;
@@ -4748,14 +4748,15 @@ export const SETTINGS_SCHEMA = {
4748
4748
 
4749
4749
  "providers.kimiApiFormat": {
4750
4750
  type: "enum",
4751
- values: ["openai", "anthropic"] as const,
4752
- default: "anthropic",
4751
+ values: ["auto", "openai", "anthropic"] as const,
4752
+ default: "auto",
4753
4753
  ui: {
4754
4754
  tab: "providers",
4755
4755
  group: "Protocol",
4756
4756
  label: "Kimi API Format",
4757
- description: "API format for Kimi Code provider",
4757
+ description: "API format for Kimi Code provider (auto follows live model metadata)",
4758
4758
  options: [
4759
+ { value: "auto", label: "Auto", description: "Use the model's server-declared protocol" },
4759
4760
  { value: "openai", label: "OpenAI", description: "api.kimi.com" },
4760
4761
  { value: "anthropic", label: "Anthropic", description: "api.moonshot.ai" },
4761
4762
  ],
@@ -13,9 +13,9 @@ import type { ExecutorOptions } from "../../task/executor";
13
13
  import * as taskExecutor from "../../task/executor";
14
14
  import * as isolationRunner from "../../task/isolation-runner";
15
15
  import { AgentOutputManager } from "../../task/output-manager";
16
- import type { AgentDefinition, AgentProgress, SingleResult } from "../../task/types";
16
+ import type { AgentDefinition, AgentProgress, SingleResult, StructuredSubagentOutput } from "../../task/types";
17
17
  import type { ToolSession } from "../../tools";
18
- import { EVAL_AGENT_MAX_DEPTH, runEvalAgent } from "../agent-bridge";
18
+ import { runEvalAgent } from "../agent-bridge";
19
19
  import { EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP } from "../bridge-timeout";
20
20
  import { IdleTimeout } from "../idle-timeout";
21
21
  import { disposeAllVmContexts } from "../js/context-manager";
@@ -51,6 +51,7 @@ interface SessionOptions {
51
51
  settings?: Settings;
52
52
  outputManager?: AgentOutputManager;
53
53
  planMode?: boolean;
54
+ outputSchema?: unknown;
54
55
  }
55
56
 
56
57
  function makeSession(options: SessionOptions = {}): ToolSession {
@@ -76,6 +77,7 @@ function makeSession(options: SessionOptions = {}): ToolSession {
76
77
  getArtifactsDir: () => artifactsDir,
77
78
  getSessionId: () => "test-session",
78
79
  getEvalSessionId: () => "test-eval-session",
80
+ outputSchema: options.outputSchema,
79
81
  getPlanModeState: options.planMode
80
82
  ? () =>
81
83
  ({
@@ -189,7 +191,7 @@ describe("runEvalAgent", () => {
189
191
  );
190
192
  });
191
193
 
192
- it("enforces spawn restrictions and the eval recursion cap", async () => {
194
+ it("enforces shared spawn restrictions", async () => {
193
195
  mockAgents();
194
196
  const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
195
197
 
@@ -199,9 +201,6 @@ describe("runEvalAgent", () => {
199
201
  await expect(
200
202
  runEvalAgent({ prompt: "hello", agent: "task" }, { session: makeSession({ spawns: "reviewer" }) }),
201
203
  ).rejects.toThrow("Allowed: reviewer");
202
- await expect(
203
- runEvalAgent({ prompt: "hello" }, { session: makeSession({ depth: EVAL_AGENT_MAX_DEPTH }) }),
204
- ).rejects.toThrow("maximum depth");
205
204
  expect(runSpy).not.toHaveBeenCalled();
206
205
  });
207
206
 
@@ -219,12 +218,10 @@ describe("runEvalAgent", () => {
219
218
  expect(runSpy.mock.calls[0]?.[0].agent.name).toBe("reviewer");
220
219
  });
221
220
 
222
- it("honors task.maxRecursionDepth on top of the hard eval ceiling", async () => {
221
+ it("honors task.maxRecursionDepth without an eval-specific ceiling", async () => {
223
222
  mockAgents();
224
223
  const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
225
224
 
226
- // task.maxRecursionDepth=0 means "no spawning at all" — even depth 0 (the
227
- // top-level agent) must be blocked, matching canSpawnAtDepth().
228
225
  await expect(
229
226
  runEvalAgent(
230
227
  { prompt: "hello" },
@@ -240,35 +237,38 @@ describe("runEvalAgent", () => {
240
237
  ),
241
238
  ).rejects.toThrow("maximum depth is 0");
242
239
 
243
- // task.maxRecursionDepth=1 ("Single") lets the top spawn but a depth-1
244
- // subagent cannot spawn further — even though the hard ceiling is 3.
245
- await expect(
246
- runEvalAgent(
247
- { prompt: "hello" },
248
- {
249
- session: makeSession({
250
- depth: 1,
251
- settings: Settings.isolated({
252
- "async.enabled": false,
253
- "task.isolation.mode": "none",
254
- "task.maxRecursionDepth": 1,
255
- }),
240
+ await runEvalAgent(
241
+ { prompt: "hello" },
242
+ {
243
+ session: makeSession({
244
+ depth: 3,
245
+ settings: Settings.isolated({
246
+ "async.enabled": false,
247
+ "task.isolation.mode": "none",
248
+ "task.maxRecursionDepth": -1,
256
249
  }),
257
- },
258
- ),
259
- ).rejects.toThrow("maximum depth is 1");
260
-
261
- expect(runSpy).not.toHaveBeenCalled();
250
+ }),
251
+ },
252
+ );
253
+ expect(runSpy).toHaveBeenCalledTimes(1);
262
254
  });
263
255
 
264
- it("throws instead of spawning from plan mode", async () => {
265
- mockAgents();
256
+ it("runs plan-mode eval agents with an attenuated policy", async () => {
257
+ mockAgents([{ ...taskAgent, tools: ["ast_grep", "write"] }]);
266
258
  const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
267
259
 
268
- await expect(runEvalAgent({ prompt: "hello" }, { session: makeSession({ planMode: true }) })).rejects.toThrow(
269
- "unavailable in plan mode",
270
- );
271
- expect(runSpy).not.toHaveBeenCalled();
260
+ await expect(
261
+ runEvalAgent({ prompt: "hello" }, { session: makeSession({ planMode: true }) }),
262
+ ).resolves.toMatchObject({
263
+ text: "ok",
264
+ });
265
+ expect(runSpy).toHaveBeenCalledTimes(1);
266
+ expect(runSpy.mock.calls[0]?.[0].agent.tools).toEqual(["read", "grep", "glob", "web_search", "ast_grep"]);
267
+ expect(runSpy.mock.calls[0]?.[0].agent.spawns).toBeUndefined();
268
+ await expect(
269
+ runEvalAgent({ prompt: "unsafe", isolated: true }, { session: makeSession({ planMode: true }) }),
270
+ ).rejects.toThrow("isolation, apply, and merge controls are unavailable in plan mode");
271
+ expect(runSpy).toHaveBeenCalledTimes(1);
272
272
  });
273
273
 
274
274
  it("passes parent execution options and only sets outputSchema when schema is supplied", async () => {
@@ -310,8 +310,52 @@ describe("runEvalAgent", () => {
310
310
  expect(secondOptions.outputSchema).toBeUndefined();
311
311
  expect(secondOptions.outputSchemaOverridesAgent).toBeUndefined();
312
312
  });
313
+ it("returns host-parsed data for caller, agent, and inherited schemas", async () => {
314
+ const agentSchema = { type: "object" };
315
+ const sessionSchema = { type: "object" };
316
+ const callerSchema = { type: "object" };
317
+ const frontmatterAgent = { ...reviewerAgent, name: "structured", output: agentSchema };
318
+ mockAgents([taskAgent, frontmatterAgent]);
319
+ const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
320
+ const source = options.outputSchemaOverridesAgent
321
+ ? "caller"
322
+ : options.agent.name === "structured"
323
+ ? "agent"
324
+ : "session";
325
+ const structuredOutput: StructuredSubagentOutput = {
326
+ source,
327
+ mode: options.outputSchemaMode ?? "permissive",
328
+ status: "valid",
329
+ data: { source },
330
+ };
331
+ return singleResult(options, { output: "not JSON", structuredOutput });
332
+ });
333
+
334
+ const caller = await runEvalAgent(
335
+ { prompt: "caller", schema: callerSchema, schemaMode: "strict" },
336
+ { session: makeSession({ outputSchema: sessionSchema }) },
337
+ );
338
+ const frontmatter = await runEvalAgent(
339
+ { prompt: "agent", agent: "structured" },
340
+ { session: makeSession({ outputSchema: sessionSchema }) },
341
+ );
342
+ const inherited = await runEvalAgent(
343
+ { prompt: "session" },
344
+ { session: makeSession({ outputSchema: sessionSchema }) },
345
+ );
313
346
 
314
- it("forces LSP off for bridge subagents even when task.enableLsp is on", async () => {
347
+ expect(caller.data).toEqual({ source: "caller" });
348
+ expect(caller.details).toMatchObject({ schemaSource: "caller", schemaMode: "strict", schemaStatus: "valid" });
349
+ expect(frontmatter.data).toEqual({ source: "agent" });
350
+ expect(inherited.data).toEqual({ source: "session" });
351
+ expect(runSpy.mock.calls.map(([options]) => options.outputSchema)).toEqual([
352
+ callerSchema,
353
+ agentSchema,
354
+ sessionSchema,
355
+ ]);
356
+ });
357
+
358
+ it("inherits non-plan LSP and IRC policy for bridge subagents", async () => {
315
359
  mockAgents();
316
360
  const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
317
361
  // makeSession() defaults to enableLsp: true and task.enableLsp: true.
@@ -321,7 +365,8 @@ describe("runEvalAgent", () => {
321
365
 
322
366
  const options = runSpy.mock.calls[0]?.[0];
323
367
  if (!options) throw new Error("runSubprocess was not called");
324
- expect(options.enableLsp).toBe(false);
368
+ expect(options.enableLsp).toBe(true);
369
+ expect(options.enableIrc).toBe(true);
325
370
  expect(options.keepAlive).toBe(false);
326
371
  });
327
372
 
@@ -485,16 +530,30 @@ describe("agent() through eval runtimes", () => {
485
530
  vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options =>
486
531
  singleResult(options, {
487
532
  output: options.outputSchema ? '{"ok":true,"n":3}' : "hello from agent",
533
+ ...(options.outputSchema
534
+ ? {
535
+ structuredOutput: {
536
+ source: "caller",
537
+ mode: options.outputSchemaMode ?? "permissive",
538
+ status: "valid",
539
+ data: { ok: true, n: 3 },
540
+ } satisfies StructuredSubagentOutput,
541
+ }
542
+ : {}),
488
543
  }),
489
544
  );
490
545
 
491
546
  const result = await executeJs(
492
- 'const text = await agent("hi"); const data = await agent("json", { schema: { type: "object" } }); return JSON.stringify([text, data]);',
547
+ 'const text = await agent("hi"); const data = await agent("json", { schema: { type: "object" } }); const node = await agent("handle", { schema: { type: "object" }, handle: true }); return JSON.stringify({ text, data, node });',
493
548
  { cwd: tempDir.path(), sessionId: sharedJsSessionId, session, sessionFile },
494
549
  );
495
550
 
496
551
  expect(result.exitCode).toBe(0);
497
- expect(JSON.parse(result.output.trim())).toEqual(["hello from agent", { ok: true, n: 3 }]);
552
+ const output = JSON.parse(result.output.trim());
553
+ expect(output.text).toBe("hello from agent");
554
+ expect(output.data).toEqual({ ok: true, n: 3 });
555
+ expect(output.node.data).toEqual({ ok: true, n: 3 });
556
+ expect(output.node.handle).toBe(`agent://${output.node.id}`);
498
557
  });
499
558
 
500
559
  it("bounds JavaScript parallel() by the task.maxConcurrency setting while preserving order", async () => {
@@ -547,23 +606,43 @@ describe("agent() through eval runtimes", () => {
547
606
  const { session, sessionFile, sessionId } = makeEvalSession(tempDir, "py-agent");
548
607
  mockAgents();
549
608
  vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options =>
550
- singleResult(options, { output: "hello from python" }),
609
+ singleResult(options, {
610
+ output: options.outputSchema ? "not JSON" : "hello from python",
611
+ ...(options.outputSchema
612
+ ? {
613
+ structuredOutput: {
614
+ source: "caller",
615
+ mode: options.outputSchemaMode ?? "permissive",
616
+ status: "valid",
617
+ data: { ok: true },
618
+ } satisfies StructuredSubagentOutput,
619
+ }
620
+ : {}),
621
+ }),
551
622
  );
552
623
 
553
- const result = await executePython('print(agent("hi"))', {
554
- cwd: tempDir.path(),
555
- sessionId,
556
- sessionFile,
557
- kernelMode: "per-call",
558
- toolSession: session,
559
- });
624
+ const result = await executePython(
625
+ 'import json\nprint(agent("hi"))\nprint(json.dumps(agent("structured", schema={"type": "object"})))\nnode = agent("handle", schema={"type": "object"}, handle=True)\nprint(json.dumps({"data": node["data"], "handle": node["handle"], "id": node["id"]}))',
626
+ {
627
+ cwd: tempDir.path(),
628
+ sessionId,
629
+ sessionFile,
630
+ kernelMode: "per-call",
631
+ toolSession: session,
632
+ },
633
+ );
560
634
  if (result.exitCode === undefined && result.cancelled) {
561
635
  expect(result.output).toBe("");
562
636
  return; // kernel unavailable in this environment
563
637
  }
564
638
 
565
639
  expect(result.exitCode).toBe(0);
566
- expect(result.output.trim()).toBe("hello from python");
640
+ const lines = result.output.trim().split("\n");
641
+ expect(lines[0]).toBe("hello from python");
642
+ expect(JSON.parse(lines[1] ?? "")).toEqual({ ok: true });
643
+ const node = JSON.parse(lines[2] ?? "");
644
+ expect(node.data).toEqual({ ok: true });
645
+ expect(node.handle).toBe(`agent://${node.id}`);
567
646
  });
568
647
 
569
648
  it("bounds Python parallel() by the task.maxConcurrency setting while preserving order", async () => {
@@ -772,7 +851,11 @@ describe("agent() through eval runtimes", () => {
772
851
 
773
852
  it("pauses the idle watchdog while a quiet agent() runs past the budget", async () => {
774
853
  using tempDir = TempDir.createSync("@omp-eval-agent-timeout-pause-");
775
- const { session } = makeEvalSession(tempDir, "js-agent-timeout-pause");
854
+ const { session } = makeEvalSession(
855
+ tempDir,
856
+ "js-agent-timeout-pause",
857
+ Settings.isolated({ "task.maxRuntimeMs": 1 }),
858
+ );
776
859
  mockAgents();
777
860
 
778
861
  // runSubprocess runs far past the eval timeout budget and emits NO progress
@@ -788,7 +871,9 @@ describe("agent() through eval runtimes", () => {
788
871
  const inFlight = new Promise<void>(resolve => {
789
872
  markInFlight = resolve;
790
873
  });
874
+ let observedMaxRuntimeMs: number | undefined;
791
875
  vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
876
+ observedMaxRuntimeMs = options.maxRuntimeMs;
792
877
  markInFlight?.();
793
878
  await released;
794
879
  return singleResult(options, { output: "done" });
@@ -812,6 +897,7 @@ describe("agent() through eval runtimes", () => {
812
897
 
813
898
  // The bridge paused the watchdog; the subprocess is now blocked in flight.
814
899
  await inFlight;
900
+ expect(observedMaxRuntimeMs).toBe(0);
815
901
  // Burn far more than the 20ms budget while paused: the watchdog stays armed-off.
816
902
  vi.advanceTimersByTime(1_000);
817
903
  expect(idle.signal.aborted).toBe(false);