@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.7
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 +54 -0
- package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
- package/dist/cli.js +3342 -3274
- package/dist/types/capability/index.d.ts +1 -1
- package/dist/types/capability/types.d.ts +23 -1
- package/dist/types/config/settings-schema.d.ts +39 -2
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
- package/dist/types/internal-urls/parse.d.ts +12 -0
- package/dist/types/internal-urls/router.d.ts +6 -0
- package/dist/types/internal-urls/types.d.ts +6 -0
- package/dist/types/lsp/config.d.ts +1 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +5 -0
- package/dist/types/mcp/tool-bridge.d.ts +13 -0
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +8 -6
- package/dist/types/session/agent-session.d.ts +28 -1
- package/dist/types/session/model-controls.d.ts +4 -1
- package/dist/types/session/session-advisors.d.ts +7 -1
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -4
- package/dist/types/session/turn-recovery.d.ts +5 -3
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/types.d.ts +3 -11
- package/dist/types/thinking.d.ts +21 -2
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/dist/types/utils/title-generator.d.ts +17 -16
- package/package.json +12 -12
- package/src/capability/index.ts +43 -12
- package/src/capability/mcp.ts +21 -0
- package/src/capability/types.ts +20 -1
- package/src/cli/read-cli.ts +44 -2
- package/src/config/settings-schema.ts +42 -2
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/eval/py/runner.py +16 -2
- package/src/extensibility/extensions/runner.ts +117 -5
- package/src/extensibility/extensions/types.ts +0 -1
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/hooks/types.ts +0 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/internal-urls/mcp-protocol.ts +17 -3
- package/src/internal-urls/parse.ts +31 -0
- package/src/internal-urls/router.ts +24 -4
- package/src/internal-urls/types.ts +6 -0
- package/src/live/transport.ts +2 -2
- package/src/lsp/client.ts +2 -2
- package/src/lsp/config.ts +4 -0
- package/src/lsp/types.ts +2 -0
- package/src/mcp/config.ts +26 -14
- package/src/mcp/manager.ts +26 -9
- package/src/mcp/tool-bridge.ts +52 -1
- package/src/memories/index.ts +25 -6
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/command-controller.ts +10 -10
- package/src/modes/controllers/extension-ui-controller.ts +7 -7
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/rpc/rpc-mode.ts +60 -47
- package/src/prompts/steering/user-interjection.md +2 -5
- package/src/prompts/tools/task.md +4 -2
- package/src/sdk.ts +64 -58
- package/src/session/agent-session-types.ts +8 -5
- package/src/session/agent-session.ts +144 -11
- package/src/session/model-controls.ts +48 -12
- package/src/session/session-advisors.ts +30 -14
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +162 -54
- package/src/session/streaming-output.ts +18 -6
- package/src/session/tool-choice-queue.ts +19 -4
- package/src/session/turn-recovery.ts +25 -6
- package/src/slash-commands/builtin-registry.ts +69 -0
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +43 -32
- package/src/task/types.ts +12 -17
- package/src/thinking.ts +68 -5
- package/src/tools/bash.ts +16 -9
- package/src/tools/index.ts +36 -16
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +89 -15
- package/src/tools/write.ts +16 -7
- package/src/tools/xdev.ts +198 -210
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/title-generator.ts +88 -34
- package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
- package/dist/types/cli/update-cli.test.d.ts +0 -1
- package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
- package/dist/types/hindsight/client.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
- package/dist/types/launch/broker-list-order.test.d.ts +0 -1
- package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
- package/dist/types/launch/protocol.test.d.ts +0 -1
- package/dist/types/launch/spawn-options.test.d.ts +0 -1
- package/dist/types/launch/terminal-output.test.d.ts +0 -1
- package/dist/types/live/protocol.test.d.ts +0 -1
- package/dist/types/mcp/config-writer.test.d.ts +0 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
- package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
- package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
- package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
- package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
- package/dist/types/modes/print-mode.test.d.ts +0 -1
- package/dist/types/modes/session-teardown.test.d.ts +0 -1
- package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
- package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
- package/dist/types/modes/warp-events.test.d.ts +0 -1
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
- package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
- package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
- package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
- package/dist/types/session/blob-store.test.d.ts +0 -1
- package/dist/types/session/messages.test.d.ts +0 -1
- package/dist/types/session/session-context.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
- package/dist/types/system-prompt.test.d.ts +0 -1
- package/dist/types/task/render.test.d.ts +0 -1
- package/dist/types/task/spawn-policy.test.d.ts +0 -1
- package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
- package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
- package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
- package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
- package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
- package/src/advisor/__tests__/advisor.test.ts +0 -4889
- package/src/advisor/__tests__/config.test.ts +0 -349
- package/src/advisor/__tests__/emission-guard.test.ts +0 -147
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
- package/src/cli/update-cli.test.ts +0 -28
- package/src/config/__tests__/model-registry.test.ts +0 -182
- package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
- package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
- package/src/eval/__tests__/budget-bridge.test.ts +0 -80
- package/src/eval/__tests__/completion-bridge.test.ts +0 -412
- package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
- package/src/eval/__tests__/idle-timeout.test.ts +0 -80
- package/src/eval/__tests__/js-context-manager.test.ts +0 -456
- package/src/eval/__tests__/julia-prelude.test.ts +0 -66
- package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
- package/src/eval/__tests__/prelude-agent.test.ts +0 -156
- package/src/eval/__tests__/process-entry-import.test.ts +0 -137
- package/src/eval/py/__tests__/prelude.test.ts +0 -104
- package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
- package/src/hindsight/client.test.ts +0 -75
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
- package/src/launch/broker-list-order.test.ts +0 -89
- package/src/launch/broker-output-snapshot.test.ts +0 -126
- package/src/launch/protocol.test.ts +0 -59
- package/src/launch/spawn-options.test.ts +0 -31
- package/src/launch/terminal-output.test.ts +0 -107
- package/src/live/protocol.test.ts +0 -140
- package/src/mcp/config-writer.test.ts +0 -43
- package/src/mcp/smithery-auth.test.ts +0 -29
- package/src/mcp/smithery-registry.test.ts +0 -51
- package/src/mcp/transports/stdio.test.ts +0 -427
- package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
- package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
- package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
- package/src/modes/components/__tests__/skill-message.test.ts +0 -94
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
- package/src/modes/components/custom-editor.test.ts +0 -510
- package/src/modes/components/login-dialog.test.ts +0 -56
- package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
- package/src/modes/components/status-line/component.test.ts +0 -84
- package/src/modes/components/tool-execution.test.ts +0 -162
- package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
- package/src/modes/noninteractive-dispose.test.ts +0 -73
- package/src/modes/print-mode.test.ts +0 -71
- package/src/modes/session-teardown.test.ts +0 -219
- package/src/modes/theme/mermaid-rendering.test.ts +0 -53
- package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
- package/src/modes/warp-events.test.ts +0 -794
- package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
- package/src/plan-mode/model-transition.test.ts +0 -60
- package/src/plan-mode/reentry-prompt.test.ts +0 -41
- package/src/session/agent-session-error-log.test.ts +0 -59
- package/src/session/blob-store.test.ts +0 -56
- package/src/session/messages.test.ts +0 -282
- package/src/session/session-context.test.ts +0 -384
- package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
- package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
- package/src/system-prompt.test.ts +0 -236
- package/src/task/render.test.ts +0 -290
- package/src/task/spawn-policy.test.ts +0 -62
- package/src/tools/__tests__/eval-description.test.ts +0 -18
- package/src/tools/__tests__/glob.test.ts +0 -37
- package/src/tools/__tests__/json-tree.test.ts +0 -35
- package/src/tools/__tests__/vibe-render.test.ts +0 -210
- package/src/tools/hub/launch-compat.test.ts +0 -40
- package/src/vibe/__tests__/token-rate.test.ts +0 -96
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
EVAL_TIMEOUT_PAUSE_OP,
|
|
4
|
-
EVAL_TIMEOUT_RESUME_OP,
|
|
5
|
-
isEvalTimeoutControlEvent,
|
|
6
|
-
withBridgeTimeoutPause,
|
|
7
|
-
} from "../bridge-timeout";
|
|
8
|
-
import { executeWithKernelBase, type GenericKernel } from "../executor-base";
|
|
9
|
-
import type { JsStatusEvent } from "../js/shared/types";
|
|
10
|
-
import type { KernelDisplayOutput } from "../py/display";
|
|
11
|
-
|
|
12
|
-
describe("withBridgeTimeoutPause", () => {
|
|
13
|
-
it("emits one pause before the operation and one resume after it settles", async () => {
|
|
14
|
-
const events: JsStatusEvent[] = [];
|
|
15
|
-
|
|
16
|
-
const value = await withBridgeTimeoutPause(
|
|
17
|
-
event => events.push(event),
|
|
18
|
-
async () => {
|
|
19
|
-
await Bun.sleep(80);
|
|
20
|
-
return "done";
|
|
21
|
-
},
|
|
22
|
-
{ deferExternalAbort: true },
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
expect(value).toBe("done");
|
|
26
|
-
expect(events.map(event => event.op)).toEqual([EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP]);
|
|
27
|
-
expect(events.every(event => event.deferExternalAbort === true)).toBe(true);
|
|
28
|
-
|
|
29
|
-
const settledCount = events.length;
|
|
30
|
-
await Bun.sleep(40);
|
|
31
|
-
expect(events.length).toBe(settledCount);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("resumes timeout accounting even when the operation throws", async () => {
|
|
35
|
-
const events: JsStatusEvent[] = [];
|
|
36
|
-
|
|
37
|
-
await expect(
|
|
38
|
-
withBridgeTimeoutPause(
|
|
39
|
-
event => events.push(event),
|
|
40
|
-
async () => {
|
|
41
|
-
await Bun.sleep(20);
|
|
42
|
-
throw new Error("boom");
|
|
43
|
-
},
|
|
44
|
-
),
|
|
45
|
-
).rejects.toThrow("boom");
|
|
46
|
-
|
|
47
|
-
expect(events.map(event => event.op)).toEqual([EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP]);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("runs the operation without emitting when no status sink is wired", async () => {
|
|
51
|
-
let ran = 0;
|
|
52
|
-
|
|
53
|
-
const value = await withBridgeTimeoutPause(undefined, async () => {
|
|
54
|
-
ran++;
|
|
55
|
-
await Bun.sleep(20);
|
|
56
|
-
return 42;
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
expect(value).toBe(42);
|
|
60
|
-
expect(ran).toBe(1);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("identifies timeout-control events as non-renderable status", () => {
|
|
64
|
-
expect(isEvalTimeoutControlEvent({ op: EVAL_TIMEOUT_PAUSE_OP })).toBe(true);
|
|
65
|
-
expect(isEvalTimeoutControlEvent({ op: EVAL_TIMEOUT_RESUME_OP })).toBe(true);
|
|
66
|
-
expect(isEvalTimeoutControlEvent({ op: "agent", id: "subagent-1" })).toBe(false);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
class TestCancelledError extends Error {
|
|
71
|
-
readonly timedOut: boolean;
|
|
72
|
-
|
|
73
|
-
constructor(timedOut: boolean) {
|
|
74
|
-
super(timedOut ? "timed out" : "cancelled");
|
|
75
|
-
this.name = "TestCancelledError";
|
|
76
|
-
this.timedOut = timedOut;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
it("defers external aborts until an in-flight agent bridge call resumes", async () => {
|
|
81
|
-
const abortController = new AbortController();
|
|
82
|
-
const entered = Promise.withResolvers<void>();
|
|
83
|
-
const triggerAbort = Promise.withResolvers<void>();
|
|
84
|
-
const observed = Promise.withResolvers<boolean>();
|
|
85
|
-
const release = Promise.withResolvers<void>();
|
|
86
|
-
const kernel: GenericKernel<Record<string, string | null>> = {
|
|
87
|
-
async execute(_code, options) {
|
|
88
|
-
entered.resolve();
|
|
89
|
-
await triggerAbort.promise;
|
|
90
|
-
options.onDisplay({
|
|
91
|
-
type: "status",
|
|
92
|
-
event: { op: EVAL_TIMEOUT_PAUSE_OP, deferExternalAbort: true },
|
|
93
|
-
} satisfies KernelDisplayOutput);
|
|
94
|
-
abortController.abort(new Error("external interrupt"));
|
|
95
|
-
observed.resolve(options.signal?.aborted ?? false);
|
|
96
|
-
await release.promise;
|
|
97
|
-
options.onDisplay({
|
|
98
|
-
type: "status",
|
|
99
|
-
event: { op: EVAL_TIMEOUT_RESUME_OP, deferExternalAbort: true },
|
|
100
|
-
} satisfies KernelDisplayOutput);
|
|
101
|
-
return { status: "ok", cancelled: false, timedOut: false };
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const resultPromise = executeWithKernelBase({
|
|
106
|
-
kernel,
|
|
107
|
-
code: "agent('slow')",
|
|
108
|
-
options: { signal: abortController.signal },
|
|
109
|
-
runIdPrefix: "test",
|
|
110
|
-
errorLogLabel: "test",
|
|
111
|
-
cancelledErrorClass: TestCancelledError,
|
|
112
|
-
buildKernelEnvPatch: () => ({}),
|
|
113
|
-
formatKernelTimeoutAnnotation: () => "kernel timed out",
|
|
114
|
-
formatTimeoutAnnotation: () => "timed out",
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
await entered.promise;
|
|
118
|
-
triggerAbort.resolve();
|
|
119
|
-
expect(await observed.promise).toBe(false);
|
|
120
|
-
release.resolve();
|
|
121
|
-
const result = await resultPromise;
|
|
122
|
-
expect(result.cancelled).toBe(true);
|
|
123
|
-
expect(result.exitCode).toBeUndefined();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("does not defer external aborts for a completion bridge call", async () => {
|
|
127
|
-
const abortController = new AbortController();
|
|
128
|
-
const entered = Promise.withResolvers<void>();
|
|
129
|
-
const triggerAbort = Promise.withResolvers<void>();
|
|
130
|
-
const observed = Promise.withResolvers<boolean>();
|
|
131
|
-
const release = Promise.withResolvers<void>();
|
|
132
|
-
const kernel: GenericKernel<Record<string, string | null>> = {
|
|
133
|
-
async execute(_code, options) {
|
|
134
|
-
entered.resolve();
|
|
135
|
-
await triggerAbort.promise;
|
|
136
|
-
options.onDisplay({
|
|
137
|
-
type: "status",
|
|
138
|
-
event: { op: EVAL_TIMEOUT_PAUSE_OP },
|
|
139
|
-
} satisfies KernelDisplayOutput);
|
|
140
|
-
abortController.abort(new Error("external interrupt"));
|
|
141
|
-
observed.resolve(options.signal?.aborted ?? false);
|
|
142
|
-
await release.promise;
|
|
143
|
-
options.onDisplay({
|
|
144
|
-
type: "status",
|
|
145
|
-
event: { op: EVAL_TIMEOUT_RESUME_OP },
|
|
146
|
-
} satisfies KernelDisplayOutput);
|
|
147
|
-
return { status: "ok", cancelled: false, timedOut: false };
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const resultPromise = executeWithKernelBase({
|
|
152
|
-
kernel,
|
|
153
|
-
code: "completion('slow')",
|
|
154
|
-
options: { signal: abortController.signal },
|
|
155
|
-
runIdPrefix: "test",
|
|
156
|
-
errorLogLabel: "test",
|
|
157
|
-
cancelledErrorClass: TestCancelledError,
|
|
158
|
-
buildKernelEnvPatch: () => ({}),
|
|
159
|
-
formatKernelTimeoutAnnotation: () => "kernel timed out",
|
|
160
|
-
formatTimeoutAnnotation: () => "timed out",
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
await entered.promise;
|
|
164
|
-
triggerAbort.resolve();
|
|
165
|
-
expect(await observed.promise).toBe(true);
|
|
166
|
-
release.resolve();
|
|
167
|
-
const result = await resultPromise;
|
|
168
|
-
expect(result.cancelled).toBe(true);
|
|
169
|
-
expect(result.exitCode).toBeUndefined();
|
|
170
|
-
});
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import type { GoalModeState } from "../../goals/state";
|
|
3
|
-
import type { UsageStatistics } from "../../session/session-entries";
|
|
4
|
-
import type { ToolSession } from "../../tools";
|
|
5
|
-
import { runEvalBudget } from "../budget-bridge";
|
|
6
|
-
|
|
7
|
-
type TurnBudget = { total: number | null; spent: number; hard: boolean };
|
|
8
|
-
|
|
9
|
-
function makeSession(parts: { turn?: TurnBudget; goal?: GoalModeState; usage?: UsageStatistics }): ToolSession {
|
|
10
|
-
return {
|
|
11
|
-
getTurnBudget: parts.turn ? () => parts.turn as TurnBudget : undefined,
|
|
12
|
-
getGoalModeState: parts.goal ? () => parts.goal : undefined,
|
|
13
|
-
getUsageStatistics: parts.usage ? () => parts.usage as UsageStatistics : undefined,
|
|
14
|
-
} as unknown as ToolSession;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function goalState(extra: Partial<GoalModeState["goal"]>): GoalModeState {
|
|
18
|
-
return {
|
|
19
|
-
enabled: true,
|
|
20
|
-
mode: "active",
|
|
21
|
-
goal: { id: "g1", status: "active", tokensUsed: 0, timeUsedSeconds: 0, ...extra },
|
|
22
|
-
} as GoalModeState;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function usage(output: number): UsageStatistics {
|
|
26
|
-
return {
|
|
27
|
-
input: 0,
|
|
28
|
-
output,
|
|
29
|
-
cacheRead: 0,
|
|
30
|
-
cacheWrite: 0,
|
|
31
|
-
totalTokens: output,
|
|
32
|
-
orchestrationInput: 0,
|
|
33
|
-
orchestrationOutput: 0,
|
|
34
|
-
orchestrationCacheRead: 0,
|
|
35
|
-
premiumRequests: 0,
|
|
36
|
-
cost: 0,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe("runEvalBudget", () => {
|
|
41
|
-
it("prefers an active +Nk turn directive over Goal Mode", async () => {
|
|
42
|
-
const session = makeSession({
|
|
43
|
-
turn: { total: 200_000, spent: 5_000, hard: true },
|
|
44
|
-
goal: goalState({ tokenBudget: 100_000, tokensUsed: 4_200 }),
|
|
45
|
-
});
|
|
46
|
-
expect(await runEvalBudget({}, { session })).toEqual({ total: 200_000, spent: 5_000, hard: true });
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("reports an advisory turn budget as hard:false", async () => {
|
|
50
|
-
const session = makeSession({ turn: { total: 50_000, spent: 1_000, hard: false } });
|
|
51
|
-
expect(await runEvalBudget({}, { session })).toEqual({ total: 50_000, spent: 1_000, hard: false });
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("falls through to Goal Mode when no turn directive set a ceiling", async () => {
|
|
55
|
-
const session = makeSession({
|
|
56
|
-
turn: { total: null, spent: 7_777, hard: false },
|
|
57
|
-
goal: goalState({ tokenBudget: 100_000, tokensUsed: 4_200 }),
|
|
58
|
-
});
|
|
59
|
-
expect(await runEvalBudget({}, { session })).toEqual({ total: 100_000, spent: 4_200, hard: true });
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("treats a Goal Mode budget as hard, and a budgetless goal as no ceiling", async () => {
|
|
63
|
-
const withBudget = makeSession({ goal: goalState({ tokenBudget: 80_000, tokensUsed: 9_000 }) });
|
|
64
|
-
expect(await runEvalBudget({}, { session: withBudget })).toEqual({ total: 80_000, spent: 9_000, hard: true });
|
|
65
|
-
|
|
66
|
-
const noBudget = makeSession({ goal: goalState({ tokenBudget: undefined, tokensUsed: 1_234 }) });
|
|
67
|
-
expect(await runEvalBudget({}, { session: noBudget })).toEqual({ total: null, spent: 1_234, hard: false });
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("reports no ceiling but still surfaces spend", async () => {
|
|
71
|
-
const fromTurn = makeSession({ turn: { total: null, spent: 333, hard: false } });
|
|
72
|
-
expect(await runEvalBudget({}, { session: fromTurn })).toEqual({ total: null, spent: 333, hard: false });
|
|
73
|
-
|
|
74
|
-
const fromUsage = makeSession({ usage: usage(777) });
|
|
75
|
-
expect(await runEvalBudget({}, { session: fromUsage })).toEqual({ total: null, spent: 777, hard: false });
|
|
76
|
-
|
|
77
|
-
const empty = makeSession({});
|
|
78
|
-
expect(await runEvalBudget({}, { session: empty })).toEqual({ total: null, spent: 0, hard: false });
|
|
79
|
-
});
|
|
80
|
-
});
|
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
import { afterAll, afterEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
|
|
4
|
-
import * as ai from "@oh-my-pi/pi-ai";
|
|
5
|
-
import { Effort } from "@oh-my-pi/pi-ai";
|
|
6
|
-
import { TempDir } from "@oh-my-pi/pi-utils";
|
|
7
|
-
import { $ } from "bun";
|
|
8
|
-
import type { ModelRegistry } from "../../config/model-registry";
|
|
9
|
-
import { Settings } from "../../config/settings";
|
|
10
|
-
import type { ToolSession } from "../../tools";
|
|
11
|
-
import { ToolError } from "../../tools/tool-errors";
|
|
12
|
-
import { EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP } from "../bridge-timeout";
|
|
13
|
-
import { runEvalCompletion } from "../completion-bridge";
|
|
14
|
-
import { IdleTimeout } from "../idle-timeout";
|
|
15
|
-
import { disposeAllVmContexts } from "../js/context-manager";
|
|
16
|
-
import { executeJs } from "../js/executor";
|
|
17
|
-
import { disposeAllKernelSessions, type PythonResult } from "../py/executor";
|
|
18
|
-
|
|
19
|
-
function makeModel(provider: string, id: string, extra: Partial<Model<Api>> = {}): Model<Api> {
|
|
20
|
-
return {
|
|
21
|
-
id,
|
|
22
|
-
name: id,
|
|
23
|
-
api: "openai-responses",
|
|
24
|
-
provider,
|
|
25
|
-
baseUrl: "https://example.test/v1",
|
|
26
|
-
reasoning: false,
|
|
27
|
-
input: ["text"],
|
|
28
|
-
cost: { input: 1, output: 1, cacheRead: 0, cacheWrite: 1 },
|
|
29
|
-
contextWindow: 128000,
|
|
30
|
-
maxTokens: 4096,
|
|
31
|
-
...extra,
|
|
32
|
-
} as Model<Api>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const SMOL = makeModel("p", "smol");
|
|
36
|
-
const DEFAULT = makeModel("p", "default");
|
|
37
|
-
const SLOW = makeModel("p", "slow");
|
|
38
|
-
const REASONING_SLOW = makeModel("p", "slow", {
|
|
39
|
-
api: "anthropic-messages",
|
|
40
|
-
reasoning: true,
|
|
41
|
-
thinking: { efforts: [Effort.Low, Effort.Medium, Effort.High], mode: "anthropic-adaptive" },
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
interface SessionOptions {
|
|
45
|
-
available?: Model<Api>[];
|
|
46
|
-
apiKey?: string | null;
|
|
47
|
-
activeModel?: string;
|
|
48
|
-
roles?: Partial<Record<"smol" | "default" | "slow", string>>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function makeSession(opts: SessionOptions = {}): ToolSession {
|
|
52
|
-
const settings = Settings.isolated({ "async.enabled": false, "task.isolation.mode": "none" });
|
|
53
|
-
const roles = opts.roles ?? { smol: "p/smol", slow: "p/slow" };
|
|
54
|
-
for (const role in roles) {
|
|
55
|
-
const value = roles[role as keyof typeof roles];
|
|
56
|
-
if (value) settings.setModelRole(role, value);
|
|
57
|
-
}
|
|
58
|
-
const modelRegistry = {
|
|
59
|
-
getAvailable: () => opts.available ?? [SMOL, DEFAULT, SLOW],
|
|
60
|
-
getApiKey: async () => (opts.apiKey === undefined ? "test-key" : opts.apiKey),
|
|
61
|
-
resolver: () => async () => (opts.apiKey === undefined ? "test-key" : opts.apiKey),
|
|
62
|
-
} as unknown as ModelRegistry;
|
|
63
|
-
return {
|
|
64
|
-
settings,
|
|
65
|
-
modelRegistry,
|
|
66
|
-
getActiveModelString: () => opts.activeModel ?? "p/default",
|
|
67
|
-
} as unknown as ToolSession;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function assistant(opts: {
|
|
71
|
-
text?: string;
|
|
72
|
-
toolCall?: { name: string; arguments: Record<string, unknown> };
|
|
73
|
-
stopReason?: AssistantMessage["stopReason"];
|
|
74
|
-
errorMessage?: string;
|
|
75
|
-
}): AssistantMessage {
|
|
76
|
-
const content: AssistantMessage["content"] = [];
|
|
77
|
-
if (opts.text) content.push({ type: "text", text: opts.text });
|
|
78
|
-
if (opts.toolCall) {
|
|
79
|
-
content.push({ type: "toolCall", id: "tc-1", name: opts.toolCall.name, arguments: opts.toolCall.arguments });
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
role: "assistant",
|
|
83
|
-
content,
|
|
84
|
-
api: "openai-responses",
|
|
85
|
-
provider: "p",
|
|
86
|
-
model: "default",
|
|
87
|
-
usage: {
|
|
88
|
-
input: 0,
|
|
89
|
-
output: 0,
|
|
90
|
-
cacheRead: 0,
|
|
91
|
-
cacheWrite: 0,
|
|
92
|
-
totalTokens: 0,
|
|
93
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
94
|
-
},
|
|
95
|
-
stopReason: opts.stopReason ?? "stop",
|
|
96
|
-
errorMessage: opts.errorMessage,
|
|
97
|
-
timestamp: Date.now(),
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function runPythonCompletionInSubprocess(options: {
|
|
102
|
-
structured: boolean;
|
|
103
|
-
tempDir: TempDir;
|
|
104
|
-
}): Promise<PythonResult> {
|
|
105
|
-
const repoRoot = path.resolve(import.meta.dir, "../../../..");
|
|
106
|
-
const scriptPath = path.join(options.tempDir.path(), "run-python-completion.ts");
|
|
107
|
-
const resultPath = path.join(options.tempDir.path(), "python-completion-result.json");
|
|
108
|
-
const aiPath = path.resolve(import.meta.dir, "../../../../ai/src/index.ts");
|
|
109
|
-
const executorPath = path.resolve(import.meta.dir, "../py/executor.ts");
|
|
110
|
-
const settingsPath = path.resolve(import.meta.dir, "../../config/settings.ts");
|
|
111
|
-
const code = options.structured
|
|
112
|
-
? 'import json\nprint(json.dumps(completion("hi", schema={"type": "object"})))'
|
|
113
|
-
: 'print(completion("hi", model="smol"))';
|
|
114
|
-
const responseContent = options.structured
|
|
115
|
-
? '[{ type: "toolCall", id: "tc-1", name: "respond", arguments: { ok: true } }]'
|
|
116
|
-
: '[{ type: "text", text: "hello from python" }]';
|
|
117
|
-
await Bun.write(
|
|
118
|
-
scriptPath,
|
|
119
|
-
`
|
|
120
|
-
import { vi } from "bun:test";
|
|
121
|
-
import * as ai from ${JSON.stringify(aiPath)};
|
|
122
|
-
import { executePython } from ${JSON.stringify(executorPath)};
|
|
123
|
-
import { Settings } from ${JSON.stringify(settingsPath)};
|
|
124
|
-
|
|
125
|
-
const SMOL = {
|
|
126
|
-
id: "smol",
|
|
127
|
-
name: "smol",
|
|
128
|
-
api: "openai-responses",
|
|
129
|
-
provider: "p",
|
|
130
|
-
baseUrl: "https://example.test/v1",
|
|
131
|
-
reasoning: false,
|
|
132
|
-
input: ["text"],
|
|
133
|
-
cost: { input: 1, output: 1, cacheRead: 0, cacheWrite: 1 },
|
|
134
|
-
contextWindow: 128000,
|
|
135
|
-
maxTokens: 4096,
|
|
136
|
-
};
|
|
137
|
-
const settings = Settings.isolated({ "async.enabled": false, "task.isolation.mode": "none" });
|
|
138
|
-
settings.setModelRole("smol", "p/smol");
|
|
139
|
-
settings.setModelRole("slow", "p/slow");
|
|
140
|
-
const session = {
|
|
141
|
-
settings,
|
|
142
|
-
modelRegistry: {
|
|
143
|
-
getAvailable: () => [SMOL],
|
|
144
|
-
getApiKey: async () => "test-key",
|
|
145
|
-
resolver: () => async () => "test-key",
|
|
146
|
-
},
|
|
147
|
-
getActiveModelString: () => "p/smol",
|
|
148
|
-
};
|
|
149
|
-
vi.spyOn(ai, "completeSimple").mockResolvedValue({
|
|
150
|
-
role: "assistant",
|
|
151
|
-
api: "openai-responses",
|
|
152
|
-
provider: "p",
|
|
153
|
-
model: "smol",
|
|
154
|
-
stopReason: "stop",
|
|
155
|
-
content: ${responseContent},
|
|
156
|
-
});
|
|
157
|
-
const result = await executePython(${JSON.stringify(code)}, {
|
|
158
|
-
cwd: ${JSON.stringify(options.tempDir.path())},
|
|
159
|
-
sessionId: ${JSON.stringify(`py-completion:${options.structured ? "struct" : "plain"}`)},
|
|
160
|
-
sessionFile: ${JSON.stringify(path.join(options.tempDir.path(), "session.jsonl"))},
|
|
161
|
-
toolSession: session,
|
|
162
|
-
kernelMode: "per-call",
|
|
163
|
-
});
|
|
164
|
-
await Bun.write(${JSON.stringify(resultPath)}, JSON.stringify(result));
|
|
165
|
-
process.exit(0);
|
|
166
|
-
`,
|
|
167
|
-
);
|
|
168
|
-
const child = await $`bun ${scriptPath}`.cwd(repoRoot).quiet().nothrow();
|
|
169
|
-
const stdout = child.stdout.toString();
|
|
170
|
-
const stderr = child.stderr.toString();
|
|
171
|
-
if (child.exitCode !== 0)
|
|
172
|
-
throw new Error(stderr || stdout || `Python completion subprocess exited with ${child.exitCode}`);
|
|
173
|
-
return (await Bun.file(resultPath).json()) as PythonResult;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
describe("runEvalCompletion", () => {
|
|
177
|
-
afterEach(() => {
|
|
178
|
-
vi.restoreAllMocks();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("resolves each tier to its expected model", async () => {
|
|
182
|
-
const spy = vi.spyOn(ai, "completeSimple").mockResolvedValue(assistant({ text: "ok" }));
|
|
183
|
-
const session = makeSession();
|
|
184
|
-
|
|
185
|
-
await runEvalCompletion({ prompt: "q", model: "smol" }, { session });
|
|
186
|
-
await runEvalCompletion({ prompt: "q", model: "default" }, { session });
|
|
187
|
-
await runEvalCompletion({ prompt: "q", model: "slow" }, { session });
|
|
188
|
-
|
|
189
|
-
const resolved = spy.mock.calls.map(call => {
|
|
190
|
-
const model = call[0] as Model<Api>;
|
|
191
|
-
return `${model.provider}/${model.id}`;
|
|
192
|
-
});
|
|
193
|
-
expect(resolved).toEqual(["p/smol", "p/default", "p/slow"]);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("prefers the session active model for the default tier, falling back to @default", async () => {
|
|
197
|
-
const spy = vi.spyOn(ai, "completeSimple").mockResolvedValue(assistant({ text: "ok" }));
|
|
198
|
-
const session = makeSession({ available: [SMOL, DEFAULT, SLOW], activeModel: "p/slow" });
|
|
199
|
-
|
|
200
|
-
await runEvalCompletion({ prompt: "q", model: "default" }, { session });
|
|
201
|
-
|
|
202
|
-
const model = spy.mock.calls[0]?.[0] as Model<Api>;
|
|
203
|
-
expect(`${model.provider}/${model.id}`).toBe("p/slow");
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("returns the completion text in plain mode", async () => {
|
|
207
|
-
vi.spyOn(ai, "completeSimple").mockResolvedValue(assistant({ text: "the answer" }));
|
|
208
|
-
const result = await runEvalCompletion({ prompt: "q", model: "smol" }, { session: makeSession() });
|
|
209
|
-
expect(result.text).toBe("the answer");
|
|
210
|
-
expect(result.details).toEqual({ model: "p/smol", tier: "smol", structured: false });
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("supplies a non-empty systemPrompt when system is omitted (codex 'Instructions are required' guard)", async () => {
|
|
214
|
-
// The openai-codex Responses transformer drops `instructions` when no
|
|
215
|
-
// system prompt is provided, and the remote endpoint then 400s with
|
|
216
|
-
// "Instructions are required". runEvalCompletion must always carry a non-empty
|
|
217
|
-
// systemPrompt so `completion("…")` without a `system` argument works.
|
|
218
|
-
const spy = vi.spyOn(ai, "completeSimple").mockResolvedValue(assistant({ text: "ok" }));
|
|
219
|
-
await runEvalCompletion({ prompt: "q", model: "smol" }, { session: makeSession() });
|
|
220
|
-
const ctx = spy.mock.calls[0]?.[1] as { systemPrompt?: string[] };
|
|
221
|
-
expect(ctx.systemPrompt).toBeDefined();
|
|
222
|
-
expect(ctx.systemPrompt?.length).toBeGreaterThan(0);
|
|
223
|
-
expect(ctx.systemPrompt?.[0]).toMatch(/.+/);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("honors an explicit system prompt instead of overriding it", async () => {
|
|
227
|
-
const spy = vi.spyOn(ai, "completeSimple").mockResolvedValue(assistant({ text: "ok" }));
|
|
228
|
-
await runEvalCompletion({ prompt: "q", model: "smol", system: "Be terse." }, { session: makeSession() });
|
|
229
|
-
const ctx = spy.mock.calls[0]?.[1] as { systemPrompt?: string[] };
|
|
230
|
-
expect(ctx.systemPrompt).toEqual(["Be terse."]);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it("forces a respond tool call and returns its arguments in structured mode", async () => {
|
|
234
|
-
const spy = vi
|
|
235
|
-
.spyOn(ai, "completeSimple")
|
|
236
|
-
.mockResolvedValue(assistant({ toolCall: { name: "respond", arguments: { answer: 42 } } }));
|
|
237
|
-
const result = await runEvalCompletion(
|
|
238
|
-
{ prompt: "q", model: "smol", schema: { type: "object", properties: { answer: { type: "number" } } } },
|
|
239
|
-
{ session: makeSession() },
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
expect(JSON.parse(result.text)).toEqual({ answer: 42 });
|
|
243
|
-
expect(result.details.structured).toBe(true);
|
|
244
|
-
|
|
245
|
-
const ctx = spy.mock.calls[0]?.[1] as { tools?: Array<{ name: string }> };
|
|
246
|
-
const opts = spy.mock.calls[0]?.[2] as { toolChoice?: unknown };
|
|
247
|
-
expect(ctx.tools?.[0]?.name).toBe("respond");
|
|
248
|
-
expect(opts.toolChoice).toEqual({ type: "tool", name: "respond" });
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("falls back to JSON embedded in text when the model skips the respond tool", async () => {
|
|
252
|
-
vi.spyOn(ai, "completeSimple").mockResolvedValue(assistant({ text: 'here: {"answer": 7}' }));
|
|
253
|
-
const result = await runEvalCompletion(
|
|
254
|
-
{ prompt: "q", model: "smol", schema: { type: "object" } },
|
|
255
|
-
{ session: makeSession() },
|
|
256
|
-
);
|
|
257
|
-
expect(JSON.parse(result.text)).toEqual({ answer: 7 });
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it("requests reasoning only for the slow tier on a reasoning-capable model", async () => {
|
|
261
|
-
const spy = vi.spyOn(ai, "completeSimple").mockResolvedValue(assistant({ text: "ok" }));
|
|
262
|
-
const session = makeSession({ available: [SMOL, DEFAULT, REASONING_SLOW] });
|
|
263
|
-
|
|
264
|
-
await runEvalCompletion({ prompt: "q", model: "smol" }, { session });
|
|
265
|
-
await runEvalCompletion({ prompt: "q", model: "slow" }, { session });
|
|
266
|
-
|
|
267
|
-
const smolOpts = spy.mock.calls[0]?.[2] as { reasoning?: unknown };
|
|
268
|
-
const slowOpts = spy.mock.calls[1]?.[2] as { reasoning?: unknown };
|
|
269
|
-
expect(smolOpts.reasoning).toBeUndefined();
|
|
270
|
-
expect(slowOpts.reasoning).toBe(Effort.High);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("does not request reasoning for the slow tier on a non-reasoning model", async () => {
|
|
274
|
-
const spy = vi.spyOn(ai, "completeSimple").mockResolvedValue(assistant({ text: "ok" }));
|
|
275
|
-
// SLOW is reasoning:false — must not trip requireSupportedEffort downstream.
|
|
276
|
-
const result = await runEvalCompletion({ prompt: "q", model: "slow" }, { session: makeSession() });
|
|
277
|
-
expect(result.text).toBe("ok");
|
|
278
|
-
const opts = spy.mock.calls[0]?.[2] as { reasoning?: unknown };
|
|
279
|
-
expect(opts.reasoning).toBeUndefined();
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it("throws ToolError on invalid arguments", async () => {
|
|
283
|
-
await expect(runEvalCompletion({ prompt: "" }, { session: makeSession() })).rejects.toBeInstanceOf(ToolError);
|
|
284
|
-
await expect(
|
|
285
|
-
runEvalCompletion({ prompt: "q", model: "huge" }, { session: makeSession() }),
|
|
286
|
-
).rejects.toBeInstanceOf(ToolError);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("throws ToolError when no model resolves for the tier", async () => {
|
|
290
|
-
const session = makeSession({ available: [DEFAULT], roles: { smol: "missing/model" } });
|
|
291
|
-
await expect(runEvalCompletion({ prompt: "q", model: "smol" }, { session })).rejects.toBeInstanceOf(ToolError);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it("throws ToolError when the resolved model has no API key", async () => {
|
|
295
|
-
const session = makeSession({ apiKey: null });
|
|
296
|
-
await expect(runEvalCompletion({ prompt: "q", model: "smol" }, { session })).rejects.toBeInstanceOf(ToolError);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("maps error and aborted stop reasons to ToolError", async () => {
|
|
300
|
-
vi.spyOn(ai, "completeSimple").mockResolvedValueOnce(assistant({ stopReason: "error", errorMessage: "boom" }));
|
|
301
|
-
await expect(runEvalCompletion({ prompt: "q", model: "smol" }, { session: makeSession() })).rejects.toThrow(
|
|
302
|
-
"boom",
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
vi.spyOn(ai, "completeSimple").mockResolvedValueOnce(assistant({ stopReason: "aborted" }));
|
|
306
|
-
await expect(
|
|
307
|
-
runEvalCompletion({ prompt: "q", model: "smol" }, { session: makeSession() }),
|
|
308
|
-
).rejects.toBeInstanceOf(ToolError);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it("throws ToolError when plain mode produces no text", async () => {
|
|
312
|
-
vi.spyOn(ai, "completeSimple").mockResolvedValue(assistant({ text: "" }));
|
|
313
|
-
await expect(
|
|
314
|
-
runEvalCompletion({ prompt: "q", model: "smol" }, { session: makeSession() }),
|
|
315
|
-
).rejects.toBeInstanceOf(ToolError);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it("pauses the idle watchdog while a slow completion() request is in flight", async () => {
|
|
319
|
-
// A oneshot completion emits no status until it returns; delegated model
|
|
320
|
-
// time must be invisible to the eval timeout budget.
|
|
321
|
-
vi.spyOn(ai, "completeSimple").mockImplementation(async () => {
|
|
322
|
-
await Bun.sleep(200);
|
|
323
|
-
return assistant({ text: "the answer" });
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
const ops: string[] = [];
|
|
327
|
-
using idle = new IdleTimeout(60);
|
|
328
|
-
const result = await runEvalCompletion(
|
|
329
|
-
{ prompt: "q", model: "smol" },
|
|
330
|
-
{
|
|
331
|
-
session: makeSession(),
|
|
332
|
-
signal: idle.signal,
|
|
333
|
-
emitStatus: event => {
|
|
334
|
-
ops.push(event.op);
|
|
335
|
-
if (event.op === EVAL_TIMEOUT_PAUSE_OP) idle.pause();
|
|
336
|
-
if (event.op === EVAL_TIMEOUT_RESUME_OP) idle.resume();
|
|
337
|
-
},
|
|
338
|
-
},
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
expect(result.text).toBe("the answer");
|
|
342
|
-
expect(ops).toEqual([EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP, "completion"]);
|
|
343
|
-
expect(idle.signal.aborted).toBe(false);
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
describe("completion() through eval runtimes", () => {
|
|
348
|
-
afterEach(() => {
|
|
349
|
-
vi.restoreAllMocks();
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
afterAll(async () => {
|
|
353
|
-
await disposeAllVmContexts();
|
|
354
|
-
await disposeAllKernelSessions();
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("exposes completion() in the JavaScript runtime", async () => {
|
|
358
|
-
using tempDir = TempDir.createSync("@omp-eval-completion-js-");
|
|
359
|
-
const sessionFile = path.join(tempDir.path(), "session.jsonl");
|
|
360
|
-
const sessionId = `js-completion:${crypto.randomUUID()}`;
|
|
361
|
-
vi.spyOn(ai, "completeSimple").mockResolvedValue(assistant({ text: "hello from smol" }));
|
|
362
|
-
|
|
363
|
-
const result = await executeJs('return await completion("hi", { model: "smol" });', {
|
|
364
|
-
cwd: tempDir.path(),
|
|
365
|
-
sessionId,
|
|
366
|
-
session: makeSession(),
|
|
367
|
-
sessionFile,
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
expect(result.exitCode).toBe(0);
|
|
371
|
-
expect(result.output.trim()).toBe("hello from smol");
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
it("parses structured completion() output in the JavaScript runtime", async () => {
|
|
375
|
-
using tempDir = TempDir.createSync("@omp-eval-completion-js-struct-");
|
|
376
|
-
const sessionFile = path.join(tempDir.path(), "session.jsonl");
|
|
377
|
-
const sessionId = `js-completion-struct:${crypto.randomUUID()}`;
|
|
378
|
-
vi.spyOn(ai, "completeSimple").mockResolvedValue(
|
|
379
|
-
assistant({ toolCall: { name: "respond", arguments: { ok: true, n: 3 } } }),
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
const result = await executeJs(
|
|
383
|
-
'const r = await completion("hi", { schema: { type: "object" } }); return JSON.stringify(r);',
|
|
384
|
-
{ cwd: tempDir.path(), sessionId, session: makeSession(), sessionFile },
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
expect(result.exitCode).toBe(0);
|
|
388
|
-
expect(JSON.parse(result.output.trim())).toEqual({ ok: true, n: 3 });
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it("exposes completion() in the Python runtime", async () => {
|
|
392
|
-
const tempDir = TempDir.createSync("@omp-eval-completion-py-");
|
|
393
|
-
try {
|
|
394
|
-
const result = await runPythonCompletionInSubprocess({ structured: false, tempDir });
|
|
395
|
-
expect(result.exitCode).toBe(0);
|
|
396
|
-
expect(result.output.trim()).toBe("hello from python");
|
|
397
|
-
} finally {
|
|
398
|
-
tempDir.removeSync();
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("parses structured completion() output in the Python runtime", async () => {
|
|
403
|
-
const tempDir = TempDir.createSync("@omp-eval-completion-py-struct-");
|
|
404
|
-
try {
|
|
405
|
-
const result = await runPythonCompletionInSubprocess({ structured: true, tempDir });
|
|
406
|
-
expect(result.exitCode).toBe(0);
|
|
407
|
-
expect(JSON.parse(result.output.trim())).toEqual({ ok: true });
|
|
408
|
-
} finally {
|
|
409
|
-
tempDir.removeSync();
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
});
|