@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.
Files changed (244) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
  3. package/dist/cli.js +3342 -3274
  4. package/dist/types/capability/index.d.ts +1 -1
  5. package/dist/types/capability/types.d.ts +23 -1
  6. package/dist/types/config/settings-schema.d.ts +39 -2
  7. package/dist/types/cursor.d.ts +2 -1
  8. package/dist/types/extensibility/extensions/runner.d.ts +4 -0
  9. package/dist/types/extensibility/shared-events.d.ts +18 -1
  10. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  11. package/dist/types/internal-urls/parse.d.ts +12 -0
  12. package/dist/types/internal-urls/router.d.ts +6 -0
  13. package/dist/types/internal-urls/types.d.ts +6 -0
  14. package/dist/types/lsp/config.d.ts +1 -0
  15. package/dist/types/lsp/types.d.ts +2 -0
  16. package/dist/types/mcp/manager.d.ts +5 -0
  17. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +5 -0
  19. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  20. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  21. package/dist/types/sdk.d.ts +3 -1
  22. package/dist/types/session/agent-session-types.d.ts +8 -6
  23. package/dist/types/session/agent-session.d.ts +28 -1
  24. package/dist/types/session/model-controls.d.ts +4 -1
  25. package/dist/types/session/session-advisors.d.ts +7 -1
  26. package/dist/types/session/session-tools.d.ts +44 -6
  27. package/dist/types/session/streaming-output.d.ts +7 -2
  28. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  29. package/dist/types/session/turn-recovery.d.ts +5 -3
  30. package/dist/types/task/index.d.ts +1 -1
  31. package/dist/types/task/types.d.ts +3 -11
  32. package/dist/types/thinking.d.ts +21 -2
  33. package/dist/types/tools/index.d.ts +8 -3
  34. package/dist/types/tools/output-meta.d.ts +5 -0
  35. package/dist/types/tools/read.d.ts +9 -1
  36. package/dist/types/tools/xdev.d.ts +53 -67
  37. package/dist/types/utils/cpuprofile.d.ts +51 -0
  38. package/dist/types/utils/inspect-image-mode.d.ts +29 -0
  39. package/dist/types/utils/profile-tree.d.ts +47 -0
  40. package/dist/types/utils/sample-profile.d.ts +67 -0
  41. package/dist/types/utils/title-generator.d.ts +17 -16
  42. package/package.json +12 -12
  43. package/src/capability/index.ts +43 -12
  44. package/src/capability/mcp.ts +21 -0
  45. package/src/capability/types.ts +20 -1
  46. package/src/cli/read-cli.ts +44 -2
  47. package/src/config/settings-schema.ts +42 -2
  48. package/src/config/settings.ts +35 -0
  49. package/src/cursor.ts +4 -3
  50. package/src/discovery/builtin-rules/index.ts +2 -0
  51. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  52. package/src/eval/py/runner.py +16 -2
  53. package/src/extensibility/extensions/runner.ts +117 -5
  54. package/src/extensibility/extensions/types.ts +0 -1
  55. package/src/extensibility/extensions/wrapper.ts +74 -42
  56. package/src/extensibility/hooks/tool-wrapper.ts +11 -4
  57. package/src/extensibility/hooks/types.ts +0 -1
  58. package/src/extensibility/shared-events.ts +18 -1
  59. package/src/internal-urls/mcp-protocol.ts +17 -3
  60. package/src/internal-urls/parse.ts +31 -0
  61. package/src/internal-urls/router.ts +24 -4
  62. package/src/internal-urls/types.ts +6 -0
  63. package/src/live/transport.ts +2 -2
  64. package/src/lsp/client.ts +2 -2
  65. package/src/lsp/config.ts +4 -0
  66. package/src/lsp/types.ts +2 -0
  67. package/src/mcp/config.ts +26 -14
  68. package/src/mcp/manager.ts +26 -9
  69. package/src/mcp/tool-bridge.ts +52 -1
  70. package/src/memories/index.ts +25 -6
  71. package/src/modes/components/custom-editor.ts +39 -16
  72. package/src/modes/components/status-line/segments.ts +3 -1
  73. package/src/modes/components/tips.txt +2 -1
  74. package/src/modes/components/tool-execution.ts +8 -7
  75. package/src/modes/controllers/command-controller.ts +10 -10
  76. package/src/modes/controllers/extension-ui-controller.ts +7 -7
  77. package/src/modes/controllers/selector-controller.ts +7 -2
  78. package/src/modes/rpc/rpc-mode.ts +60 -47
  79. package/src/prompts/steering/user-interjection.md +2 -5
  80. package/src/prompts/tools/task.md +4 -2
  81. package/src/sdk.ts +64 -58
  82. package/src/session/agent-session-types.ts +8 -5
  83. package/src/session/agent-session.ts +144 -11
  84. package/src/session/model-controls.ts +48 -12
  85. package/src/session/session-advisors.ts +30 -14
  86. package/src/session/session-listing.ts +66 -4
  87. package/src/session/session-tools.ts +162 -54
  88. package/src/session/streaming-output.ts +18 -6
  89. package/src/session/tool-choice-queue.ts +19 -4
  90. package/src/session/turn-recovery.ts +25 -6
  91. package/src/slash-commands/builtin-registry.ts +69 -0
  92. package/src/task/executor.ts +11 -3
  93. package/src/task/index.ts +43 -32
  94. package/src/task/types.ts +12 -17
  95. package/src/thinking.ts +68 -5
  96. package/src/tools/bash.ts +16 -9
  97. package/src/tools/index.ts +36 -16
  98. package/src/tools/output-meta.ts +20 -0
  99. package/src/tools/read.ts +89 -15
  100. package/src/tools/write.ts +16 -7
  101. package/src/tools/xdev.ts +198 -210
  102. package/src/utils/cpuprofile.ts +235 -0
  103. package/src/utils/inspect-image-mode.ts +39 -0
  104. package/src/utils/profile-tree.ts +111 -0
  105. package/src/utils/sample-profile.ts +437 -0
  106. package/src/utils/title-generator.ts +88 -34
  107. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  108. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  109. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  110. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  111. package/dist/types/cli/update-cli.test.d.ts +0 -1
  112. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  113. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  114. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  115. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  116. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  117. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  118. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  119. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  120. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  121. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  122. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  123. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  124. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  125. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  126. package/dist/types/hindsight/client.test.d.ts +0 -1
  127. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  128. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  129. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  130. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  131. package/dist/types/launch/protocol.test.d.ts +0 -1
  132. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  133. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  134. package/dist/types/live/protocol.test.d.ts +0 -1
  135. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  136. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  137. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  138. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  139. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  140. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  141. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  142. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  143. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  144. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  145. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  146. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  147. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  148. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  149. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  150. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  151. package/dist/types/modes/print-mode.test.d.ts +0 -1
  152. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  153. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  154. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  155. package/dist/types/modes/warp-events.test.d.ts +0 -1
  156. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  157. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  158. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  159. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  160. package/dist/types/session/blob-store.test.d.ts +0 -1
  161. package/dist/types/session/messages.test.d.ts +0 -1
  162. package/dist/types/session/session-context.test.d.ts +0 -1
  163. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  164. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  165. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  166. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  167. package/dist/types/system-prompt.test.d.ts +0 -1
  168. package/dist/types/task/render.test.d.ts +0 -1
  169. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  170. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  171. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  172. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  173. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  174. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  175. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  176. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  177. package/src/advisor/__tests__/config.test.ts +0 -349
  178. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  179. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  180. package/src/cli/update-cli.test.ts +0 -28
  181. package/src/config/__tests__/model-registry.test.ts +0 -182
  182. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  183. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  184. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  185. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  186. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  187. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  188. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  189. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  190. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  191. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  192. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  193. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  194. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  195. package/src/hindsight/client.test.ts +0 -75
  196. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  197. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  198. package/src/launch/broker-list-order.test.ts +0 -89
  199. package/src/launch/broker-output-snapshot.test.ts +0 -126
  200. package/src/launch/protocol.test.ts +0 -59
  201. package/src/launch/spawn-options.test.ts +0 -31
  202. package/src/launch/terminal-output.test.ts +0 -107
  203. package/src/live/protocol.test.ts +0 -140
  204. package/src/mcp/config-writer.test.ts +0 -43
  205. package/src/mcp/smithery-auth.test.ts +0 -29
  206. package/src/mcp/smithery-registry.test.ts +0 -51
  207. package/src/mcp/transports/stdio.test.ts +0 -427
  208. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  209. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  210. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  211. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  212. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  213. package/src/modes/components/custom-editor.test.ts +0 -510
  214. package/src/modes/components/login-dialog.test.ts +0 -56
  215. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  216. package/src/modes/components/status-line/component.test.ts +0 -84
  217. package/src/modes/components/tool-execution.test.ts +0 -162
  218. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  219. package/src/modes/noninteractive-dispose.test.ts +0 -73
  220. package/src/modes/print-mode.test.ts +0 -71
  221. package/src/modes/session-teardown.test.ts +0 -219
  222. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  223. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  224. package/src/modes/warp-events.test.ts +0 -794
  225. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  226. package/src/plan-mode/model-transition.test.ts +0 -60
  227. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  228. package/src/session/agent-session-error-log.test.ts +0 -59
  229. package/src/session/blob-store.test.ts +0 -56
  230. package/src/session/messages.test.ts +0 -282
  231. package/src/session/session-context.test.ts +0 -384
  232. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  233. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  234. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  235. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  236. package/src/system-prompt.test.ts +0 -236
  237. package/src/task/render.test.ts +0 -290
  238. package/src/task/spawn-policy.test.ts +0 -62
  239. package/src/tools/__tests__/eval-description.test.ts +0 -18
  240. package/src/tools/__tests__/glob.test.ts +0 -37
  241. package/src/tools/__tests__/json-tree.test.ts +0 -35
  242. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  243. package/src/tools/hub/launch-compat.test.ts +0 -40
  244. 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
- });