@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.
- package/CHANGELOG.md +70 -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/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/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/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-manager.ts +9 -0
- package/src/session/streaming-output.ts +41 -1
- 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 +58 -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
|
@@ -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.
|
|
4
|
+
"version": "17.0.3",
|
|
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.
|
|
56
|
-
"@oh-my-pi/omp-stats": "17.0.
|
|
57
|
-
"@oh-my-pi/pi-agent-core": "17.0.
|
|
58
|
-
"@oh-my-pi/pi-ai": "17.0.
|
|
59
|
-
"@oh-my-pi/pi-catalog": "17.0.
|
|
60
|
-
"@oh-my-pi/pi-mnemopi": "17.0.
|
|
61
|
-
"@oh-my-pi/pi-natives": "17.0.
|
|
62
|
-
"@oh-my-pi/pi-tui": "17.0.
|
|
63
|
-
"@oh-my-pi/pi-utils": "17.0.
|
|
64
|
-
"@oh-my-pi/pi-wire": "17.0.
|
|
65
|
-
"@oh-my-pi/snapcompact": "17.0.
|
|
55
|
+
"@oh-my-pi/hashline": "17.0.3",
|
|
56
|
+
"@oh-my-pi/omp-stats": "17.0.3",
|
|
57
|
+
"@oh-my-pi/pi-agent-core": "17.0.3",
|
|
58
|
+
"@oh-my-pi/pi-ai": "17.0.3",
|
|
59
|
+
"@oh-my-pi/pi-catalog": "17.0.3",
|
|
60
|
+
"@oh-my-pi/pi-mnemopi": "17.0.3",
|
|
61
|
+
"@oh-my-pi/pi-natives": "17.0.3",
|
|
62
|
+
"@oh-my-pi/pi-tui": "17.0.3",
|
|
63
|
+
"@oh-my-pi/pi-utils": "17.0.3",
|
|
64
|
+
"@oh-my-pi/pi-wire": "17.0.3",
|
|
65
|
+
"@oh-my-pi/snapcompact": "17.0.3",
|
|
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: "
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
)
|
|
260
|
-
|
|
261
|
-
expect(runSpy).not.toHaveBeenCalled();
|
|
250
|
+
}),
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
expect(runSpy).toHaveBeenCalledTimes(1);
|
|
262
254
|
});
|
|
263
255
|
|
|
264
|
-
it("
|
|
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(
|
|
269
|
-
"
|
|
270
|
-
)
|
|
271
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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, {
|
|
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(
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
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(
|
|
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);
|
|
@@ -53,6 +53,35 @@ describe("eval js agent() handle", () => {
|
|
|
53
53
|
expect(out).toBe("hello world");
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
it("keeps positional isolation controls stable while appending schemaMode", async () => {
|
|
57
|
+
let seenArgs: Record<string, unknown> | undefined;
|
|
58
|
+
const sandbox = loadPrelude(async (_name, args) => {
|
|
59
|
+
seenArgs = args as Record<string, unknown>;
|
|
60
|
+
return { text: '{"ok":true}', details: { agent: "task", id: "legacy", structured: false } };
|
|
61
|
+
});
|
|
62
|
+
const positionalAgent = sandbox.agent as (
|
|
63
|
+
prompt: string,
|
|
64
|
+
options?: unknown,
|
|
65
|
+
...rest: unknown[]
|
|
66
|
+
) => Promise<unknown>;
|
|
67
|
+
const schema = { type: "object", properties: { ok: { type: "boolean" } } };
|
|
68
|
+
|
|
69
|
+
await positionalAgent("scout", "reviewer", "p/model", "Legacy", schema, true, false, true, "strict");
|
|
70
|
+
|
|
71
|
+
expect(seenArgs).toEqual({
|
|
72
|
+
prompt: "scout",
|
|
73
|
+
agent: "reviewer",
|
|
74
|
+
model: "p/model",
|
|
75
|
+
label: "Legacy",
|
|
76
|
+
schema,
|
|
77
|
+
isolated: true,
|
|
78
|
+
apply: false,
|
|
79
|
+
merge: true,
|
|
80
|
+
schemaMode: "strict",
|
|
81
|
+
handle: false,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
56
85
|
it("carries the parsed object under data when schema and handle combine", async () => {
|
|
57
86
|
const payload = JSON.stringify({ k: 1 });
|
|
58
87
|
const sandbox = loadPrelude(async () => ({
|