@oh-my-pi/pi-coding-agent 15.9.5 → 15.10.0

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 (192) hide show
  1. package/CHANGELOG.md +98 -1
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/gallery-cli.d.ts +43 -0
  4. package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
  5. package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
  6. package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
  7. package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
  8. package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
  9. package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
  10. package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
  11. package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
  12. package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
  13. package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
  14. package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
  15. package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
  16. package/dist/types/cli/gallery-screenshot.d.ts +35 -0
  17. package/dist/types/commands/gallery.d.ts +47 -0
  18. package/dist/types/config/keybindings.d.ts +10 -2
  19. package/dist/types/config/model-id-affixes.d.ts +2 -0
  20. package/dist/types/config/model-registry.d.ts +8 -1
  21. package/dist/types/config/settings-schema.d.ts +43 -7
  22. package/dist/types/edit/file-snapshot-store.d.ts +1 -1
  23. package/dist/types/eval/backend.d.ts +6 -6
  24. package/dist/types/eval/bridge-timeout.d.ts +27 -0
  25. package/dist/types/eval/idle-timeout.d.ts +16 -14
  26. package/dist/types/eval/js/executor.d.ts +3 -3
  27. package/dist/types/eval/py/executor.d.ts +2 -2
  28. package/dist/types/eval/py/spawn-options.d.ts +58 -0
  29. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  30. package/dist/types/lsp/types.d.ts +10 -0
  31. package/dist/types/main.d.ts +3 -2
  32. package/dist/types/memory-backend/index.d.ts +2 -1
  33. package/dist/types/memory-backend/resolve.d.ts +1 -1
  34. package/dist/types/memory-backend/types.d.ts +1 -1
  35. package/dist/types/modes/components/assistant-message.d.ts +5 -0
  36. package/dist/types/modes/components/copy-selector.d.ts +22 -0
  37. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  38. package/dist/types/modes/components/model-selector.d.ts +1 -0
  39. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  40. package/dist/types/modes/controllers/command-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  42. package/dist/types/modes/index.d.ts +5 -4
  43. package/dist/types/modes/interactive-mode.d.ts +2 -2
  44. package/dist/types/modes/setup-version.d.ts +11 -0
  45. package/dist/types/modes/setup-wizard/index.d.ts +2 -1
  46. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
  47. package/dist/types/modes/types.d.ts +2 -2
  48. package/dist/types/modes/utils/copy-targets.d.ts +53 -0
  49. package/dist/types/sdk.d.ts +1 -1
  50. package/dist/types/task/executor.d.ts +7 -0
  51. package/dist/types/telemetry-export.d.ts +1 -1
  52. package/dist/types/tools/eval-render.d.ts +1 -0
  53. package/dist/types/tools/fetch.d.ts +15 -7
  54. package/dist/types/tools/render-utils.d.ts +33 -0
  55. package/dist/types/tools/renderers.d.ts +16 -2
  56. package/dist/types/tools/search.d.ts +1 -1
  57. package/dist/types/tools/write.d.ts +2 -0
  58. package/dist/types/tui/code-cell.d.ts +6 -0
  59. package/dist/types/tui/output-block.d.ts +11 -0
  60. package/dist/types/web/scrapers/github.d.ts +22 -0
  61. package/dist/types/web/search/providers/perplexity.d.ts +8 -1
  62. package/dist/types/web/search/types.d.ts +1 -1
  63. package/package.json +9 -9
  64. package/scripts/dev-launch +42 -0
  65. package/scripts/dev-launch-preload.ts +19 -0
  66. package/src/autoresearch/dashboard.ts +11 -21
  67. package/src/cli/args.ts +2 -2
  68. package/src/cli/claude-trace-cli.ts +13 -1
  69. package/src/cli/gallery-cli.ts +223 -0
  70. package/src/cli/gallery-fixtures/agentic.ts +292 -0
  71. package/src/cli/gallery-fixtures/codeintel.ts +188 -0
  72. package/src/cli/gallery-fixtures/edit.ts +194 -0
  73. package/src/cli/gallery-fixtures/fs.ts +153 -0
  74. package/src/cli/gallery-fixtures/index.ts +40 -0
  75. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  76. package/src/cli/gallery-fixtures/memory.ts +81 -0
  77. package/src/cli/gallery-fixtures/misc.ts +221 -0
  78. package/src/cli/gallery-fixtures/search.ts +213 -0
  79. package/src/cli/gallery-fixtures/shell.ts +167 -0
  80. package/src/cli/gallery-fixtures/types.ts +41 -0
  81. package/src/cli/gallery-fixtures/web.ts +158 -0
  82. package/src/cli/gallery-screenshot.ts +279 -0
  83. package/src/cli-commands.ts +1 -0
  84. package/src/commands/gallery.ts +52 -0
  85. package/src/commands/launch.ts +1 -1
  86. package/src/config/keybindings.ts +68 -2
  87. package/src/config/model-equivalence.ts +35 -12
  88. package/src/config/model-id-affixes.ts +39 -22
  89. package/src/config/model-registry.ts +16 -16
  90. package/src/config/settings-schema.ts +29 -6
  91. package/src/config/settings.ts +11 -0
  92. package/src/dap/client.ts +14 -16
  93. package/src/debug/raw-sse.ts +18 -4
  94. package/src/edit/file-snapshot-store.ts +1 -1
  95. package/src/edit/index.ts +1 -1
  96. package/src/edit/renderer.ts +43 -55
  97. package/src/edit/streaming.ts +1 -1
  98. package/src/eval/__tests__/agent-bridge.test.ts +102 -58
  99. package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
  100. package/src/eval/__tests__/idle-timeout.test.ts +26 -12
  101. package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
  102. package/src/eval/__tests__/llm-bridge.test.ts +10 -10
  103. package/src/eval/agent-bridge.ts +38 -12
  104. package/src/eval/backend.ts +6 -6
  105. package/src/eval/bridge-timeout.ts +44 -0
  106. package/src/eval/idle-timeout.ts +33 -15
  107. package/src/eval/js/executor.ts +10 -10
  108. package/src/eval/llm-bridge.ts +4 -5
  109. package/src/eval/py/executor.ts +6 -6
  110. package/src/eval/py/kernel.ts +11 -1
  111. package/src/eval/py/spawn-options.ts +126 -0
  112. package/src/export/ttsr.ts +9 -0
  113. package/src/extensibility/extensions/runner.ts +3 -0
  114. package/src/extensibility/plugins/doctor.ts +0 -1
  115. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  116. package/src/goals/tools/goal-tool.ts +2 -2
  117. package/src/internal-urls/docs-index.generated.ts +7 -6
  118. package/src/lsp/client.ts +179 -52
  119. package/src/lsp/index.ts +38 -4
  120. package/src/lsp/render.ts +3 -3
  121. package/src/lsp/types.ts +10 -0
  122. package/src/main.ts +47 -52
  123. package/src/memory-backend/index.ts +13 -1
  124. package/src/memory-backend/resolve.ts +3 -5
  125. package/src/memory-backend/types.ts +1 -1
  126. package/src/modes/components/agent-dashboard.ts +13 -4
  127. package/src/modes/components/assistant-message.ts +22 -1
  128. package/src/modes/components/copy-selector.ts +249 -0
  129. package/src/modes/components/custom-editor.ts +10 -1
  130. package/src/modes/components/extensions/extension-list.ts +17 -8
  131. package/src/modes/components/history-search.ts +19 -11
  132. package/src/modes/components/model-selector.ts +125 -29
  133. package/src/modes/components/oauth-selector.ts +28 -12
  134. package/src/modes/components/session-observer-overlay.ts +13 -15
  135. package/src/modes/components/session-selector.ts +24 -13
  136. package/src/modes/components/status-line.ts +3 -5
  137. package/src/modes/components/tool-execution.ts +83 -24
  138. package/src/modes/components/tree-selector.ts +19 -7
  139. package/src/modes/components/user-message-selector.ts +25 -14
  140. package/src/modes/controllers/command-controller.ts +13 -118
  141. package/src/modes/controllers/event-controller.ts +26 -10
  142. package/src/modes/controllers/input-controller.ts +11 -3
  143. package/src/modes/controllers/selector-controller.ts +40 -3
  144. package/src/modes/index.ts +5 -4
  145. package/src/modes/interactive-mode.ts +21 -7
  146. package/src/modes/setup-version.ts +11 -0
  147. package/src/modes/setup-wizard/index.ts +3 -2
  148. package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
  149. package/src/modes/theme/theme.ts +46 -10
  150. package/src/modes/types.ts +2 -2
  151. package/src/modes/utils/context-usage.ts +10 -6
  152. package/src/modes/utils/copy-targets.ts +254 -0
  153. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  154. package/src/prompts/tools/ast-edit.md +1 -1
  155. package/src/prompts/tools/ast-grep.md +1 -1
  156. package/src/prompts/tools/read.md +1 -1
  157. package/src/prompts/tools/search.md +1 -1
  158. package/src/sdk.ts +21 -23
  159. package/src/session/agent-session.ts +13 -9
  160. package/src/slash-commands/builtin-registry.ts +4 -12
  161. package/src/slash-commands/helpers/usage-report.ts +2 -0
  162. package/src/task/executor.ts +20 -2
  163. package/src/task/render.ts +37 -11
  164. package/src/telemetry-export.ts +25 -7
  165. package/src/tools/bash.ts +18 -8
  166. package/src/tools/browser/render.ts +5 -4
  167. package/src/tools/debug.ts +3 -3
  168. package/src/tools/eval-backends.ts +6 -17
  169. package/src/tools/eval-render.ts +28 -10
  170. package/src/tools/eval.ts +19 -23
  171. package/src/tools/fetch.ts +99 -89
  172. package/src/tools/read.ts +7 -7
  173. package/src/tools/render-utils.ts +63 -3
  174. package/src/tools/renderers.ts +16 -1
  175. package/src/tools/report-tool-issue.ts +1 -1
  176. package/src/tools/search.ts +173 -81
  177. package/src/tools/ssh.ts +21 -8
  178. package/src/tools/todo.ts +20 -7
  179. package/src/tools/write.ts +39 -9
  180. package/src/tui/code-cell.ts +19 -4
  181. package/src/tui/output-block.ts +14 -0
  182. package/src/web/scrapers/github.ts +255 -3
  183. package/src/web/scrapers/youtube.ts +3 -2
  184. package/src/web/search/providers/perplexity.ts +199 -51
  185. package/src/web/search/render.ts +42 -57
  186. package/src/web/search/types.ts +5 -1
  187. package/dist/types/eval/heartbeat.d.ts +0 -45
  188. package/src/eval/__tests__/heartbeat.test.ts +0 -84
  189. package/src/eval/__tests__/shared-executors.test.ts +0 -609
  190. package/src/eval/heartbeat.ts +0 -74
  191. /package/dist/types/eval/__tests__/{heartbeat.test.d.ts → bridge-timeout.test.d.ts} +0 -0
  192. /package/dist/types/eval/__tests__/{shared-executors.test.d.ts → kernel-spawn.test.d.ts} +0 -0
@@ -1,609 +0,0 @@
1
- import { afterAll, afterEach, describe, expect, it, vi } from "bun:test";
2
- import * as fs from "node:fs/promises";
3
- import * as path from "node:path";
4
- import type { AssistantMessage } from "@oh-my-pi/pi-ai";
5
- import { TempDir } from "@oh-my-pi/pi-utils";
6
- import type { ModelRegistry } from "../../config/model-registry";
7
- import { Settings } from "../../config/settings";
8
- import type { LoadExtensionsResult } from "../../extensibility/extensions/types";
9
- import type { CreateAgentSessionOptions, CreateAgentSessionResult } from "../../sdk";
10
- import * as sdkModule from "../../sdk";
11
- import type { AgentSession, AgentSessionEvent, PromptOptions } from "../../session/agent-session";
12
- import { TaskTool } from "../../task";
13
- import * as discoveryModule from "../../task/discovery";
14
- import type { AgentDefinition, TaskParams } from "../../task/types";
15
- import type { ToolSession } from "../../tools";
16
- import { EventBus } from "../../utils/event-bus";
17
- import { disposeAllVmContexts } from "../js/context-manager";
18
- import { executeJs } from "../js/executor";
19
- import { disposeAllKernelSessions, executePython } from "../py/executor";
20
-
21
- function createToolSession(cwd: string, sessionFile: string | null, evalSessionId?: string): ToolSession {
22
- const modelRegistry = {
23
- authStorage: undefined,
24
- refresh: async () => {},
25
- getAvailable: () => [],
26
- getApiKey: async () => null,
27
- } as unknown as ModelRegistry;
28
- return {
29
- cwd,
30
- hasUI: false,
31
- settings: Settings.isolated({
32
- "async.enabled": false,
33
- "task.isolation.mode": "none",
34
- }),
35
- getSessionFile: () => sessionFile,
36
- getSessionSpawns: () => "*",
37
- getEvalSessionId: evalSessionId ? () => evalSessionId : undefined,
38
- modelRegistry,
39
- } as unknown as ToolSession;
40
- }
41
-
42
- function createBridgeToolSession(resultText: string, calls: unknown[]): ToolSession {
43
- const readTool = {
44
- name: "read",
45
- label: "read",
46
- description: "read",
47
- parameters: { type: "object" },
48
- async execute(_id: string, args: unknown) {
49
- calls.push(args);
50
- return { content: [{ type: "text" as const, text: resultText }] };
51
- },
52
- };
53
- const tools = new Map<string, unknown>([["read", readTool]]);
54
- return { getToolByName: (name: string) => tools.get(name) } as unknown as ToolSession;
55
- }
56
-
57
- function assistantStopMessage(text: string): AssistantMessage {
58
- return {
59
- role: "assistant",
60
- content: text ? [{ type: "text", text }] : [],
61
- api: "openai-responses",
62
- provider: "openai",
63
- model: "mock",
64
- usage: {
65
- input: 0,
66
- output: 0,
67
- cacheRead: 0,
68
- cacheWrite: 0,
69
- totalTokens: 0,
70
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
71
- },
72
- stopReason: "stop",
73
- timestamp: Date.now(),
74
- };
75
- }
76
-
77
- function createYieldingSubagentSession(onPrompt: () => Promise<void>): AgentSession {
78
- const listeners: Array<(event: AgentSessionEvent) => void> = [];
79
- const state = { messages: [] as AssistantMessage[] };
80
- const emit = (event: AgentSessionEvent) => {
81
- for (const listener of listeners) listener(event);
82
- };
83
- return {
84
- state,
85
- agent: { state: { systemPrompt: ["test"] } },
86
- model: undefined,
87
- extensionRunner: undefined,
88
- sessionManager: {
89
- appendSessionInit: () => {},
90
- },
91
- getActiveToolNames: () => ["eval", "yield"],
92
- setActiveToolsByName: async () => {},
93
- subscribe: (listener: (event: AgentSessionEvent) => void) => {
94
- listeners.push(listener);
95
- return () => {
96
- const index = listeners.indexOf(listener);
97
- if (index >= 0) listeners.splice(index, 1);
98
- };
99
- },
100
- prompt: async (_text: string, _options?: PromptOptions) => {
101
- await onPrompt();
102
- state.messages.push(assistantStopMessage("done"));
103
- emit({
104
- type: "tool_execution_end",
105
- toolCallId: "yield-call",
106
- toolName: "yield",
107
- result: {
108
- content: [{ type: "text", text: "Result submitted." }],
109
- details: { status: "success", data: { ok: true } },
110
- },
111
- isError: false,
112
- });
113
- },
114
- waitForIdle: async () => {},
115
- getLastAssistantMessage: () => state.messages[state.messages.length - 1],
116
- abort: async () => {},
117
- dispose: async () => {},
118
- } as unknown as AgentSession;
119
- }
120
-
121
- const taskAgent: AgentDefinition = {
122
- name: "task",
123
- description: "Task agent",
124
- systemPrompt: "Read eval state and yield.",
125
- source: "bundled",
126
- tools: ["eval", "yield"],
127
- };
128
-
129
- const taskParams: TaskParams = {
130
- agent: "task",
131
- tasks: [{ id: "ReadEval", description: "Read eval state", assignment: "Read parent eval state." }],
132
- };
133
-
134
- describe("shared eval executors", () => {
135
- afterEach(() => {
136
- vi.restoreAllMocks();
137
- });
138
-
139
- afterAll(async () => {
140
- await disposeAllVmContexts();
141
- await disposeAllKernelSessions();
142
- });
143
-
144
- it("shares JavaScript state across executeJs calls with one session id", async () => {
145
- using tempDir = TempDir.createSync("@omp-eval-js-shared-");
146
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
147
- const sessionId = `js-shared:${crypto.randomUUID()}`;
148
- const session = createToolSession(tempDir.path(), sessionFile);
149
-
150
- await executeJs("globalThis.x = 41;", { sessionId, session, sessionFile });
151
- const result = await executeJs("return globalThis.x + 1;", { sessionId, session, sessionFile });
152
-
153
- expect(result.exitCode).toBe(0);
154
- expect(result.output.trim()).toBe("42");
155
- });
156
-
157
- it("treats idleTimeoutMs as an inactivity budget, not a fixed timer", async () => {
158
- using tempDir = TempDir.createSync("@omp-eval-js-idle-budget-");
159
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
160
- const sessionId = `js-idle-budget:${crypto.randomUUID()}`;
161
- const session = createToolSession(tempDir.path(), sessionFile);
162
-
163
- // With no wall-clock deadlineMs/timeoutMs and no aborting signal, a cell that
164
- // runs well past idleTimeoutMs must still complete: the backend must never
165
- // derive a competing fixed timer from the inactivity budget.
166
- const result = await executeJs("await Bun.sleep(120); return 'done';", {
167
- sessionId,
168
- session,
169
- sessionFile,
170
- idleTimeoutMs: 30,
171
- });
172
-
173
- expect(result.cancelled).toBe(false);
174
- expect(result.exitCode).toBe(0);
175
- expect(result.output.trim()).toBe("done");
176
- });
177
-
178
- it("shares Python state across executePython calls with one session id", async () => {
179
- using tempDir = TempDir.createSync("@omp-eval-py-shared-");
180
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
181
- const sessionId = `py-shared:${crypto.randomUUID()}`;
182
-
183
- await executePython("x = 41", { cwd: tempDir.path(), sessionId, sessionFile });
184
- const result = await executePython("print(x + 1)", { cwd: tempDir.path(), sessionId, sessionFile });
185
-
186
- expect(result.exitCode).toBe(0);
187
- expect(result.output.trim()).toBe("42");
188
- });
189
-
190
- it("deduplicates concurrent first JavaScript session acquisition", async () => {
191
- using tempDir = TempDir.createSync("@omp-eval-js-cold-start-");
192
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
193
- const sessionId = `js-cold-start:${crypto.randomUUID()}`;
194
- const session = createToolSession(tempDir.path(), sessionFile);
195
-
196
- const [first, second] = await Promise.all([
197
- executeJs(
198
- "globalThis.sharedMarker ??= crypto.randomUUID(); await Bun.sleep(50); return globalThis.sharedMarker;",
199
- {
200
- sessionId,
201
- session,
202
- sessionFile,
203
- },
204
- ),
205
- executeJs("globalThis.sharedMarker ??= crypto.randomUUID(); return globalThis.sharedMarker;", {
206
- sessionId,
207
- session,
208
- sessionFile,
209
- }),
210
- ]);
211
- const third = await executeJs("return globalThis.sharedMarker;", { sessionId, session, sessionFile });
212
-
213
- expect(first.exitCode).toBe(0);
214
- expect(second.exitCode).toBe(0);
215
- expect(third.exitCode).toBe(0);
216
- expect(first.output.trim()).toBe(second.output.trim());
217
- expect(third.output.trim()).toBe(first.output.trim());
218
- });
219
-
220
- it("deduplicates concurrent first Python session acquisition", async () => {
221
- using tempDir = TempDir.createSync("@omp-eval-py-cold-start-");
222
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
223
- const sessionId = `py-cold-start:${crypto.randomUUID()}`;
224
-
225
- const [first, second] = await Promise.all([
226
- executePython(
227
- `import asyncio, uuid
228
- shared_marker = globals().get("shared_marker") or str(uuid.uuid4())
229
- globals()["shared_marker"] = shared_marker
230
- await asyncio.sleep(0.05)
231
- print(shared_marker)`,
232
- { cwd: tempDir.path(), sessionId, sessionFile },
233
- ),
234
- executePython(
235
- `import uuid
236
- shared_marker = globals().get("shared_marker") or str(uuid.uuid4())
237
- globals()["shared_marker"] = shared_marker
238
- print(shared_marker)`,
239
- { cwd: tempDir.path(), sessionId, sessionFile },
240
- ),
241
- ]);
242
- const third = await executePython("print(shared_marker)", { cwd: tempDir.path(), sessionId, sessionFile });
243
-
244
- expect(first.exitCode).toBe(0);
245
- expect(second.exitCode).toBe(0);
246
- expect(third.exitCode).toBe(0);
247
- expect(first.output.trim()).toBe(second.output.trim());
248
- expect(third.output.trim()).toBe(first.output.trim());
249
- });
250
-
251
- it("splits retained Python kernels by cwd for one shared session id", async () => {
252
- using tempDir = TempDir.createSync("@omp-eval-py-cwd-");
253
- const dirA = path.join(tempDir.path(), "a");
254
- const dirB = path.join(tempDir.path(), "b");
255
- await fs.mkdir(dirA);
256
- await fs.mkdir(dirB);
257
- const realDirA = await fs.realpath(dirA);
258
- const realDirB = await fs.realpath(dirB);
259
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
260
- const sessionId = `py-cwd:${crypto.randomUUID()}`;
261
-
262
- const first = await executePython(
263
- `import os
264
- token = "from-a"
265
- print(os.getcwd())`,
266
- {
267
- cwd: dirA,
268
- sessionId,
269
- sessionFile,
270
- },
271
- );
272
- const second = await executePython(
273
- `import os
274
- print(os.getcwd())
275
- print("token" in globals())`,
276
- {
277
- cwd: dirB,
278
- sessionId,
279
- sessionFile,
280
- },
281
- );
282
- const third = await executePython("print(token)", { cwd: dirA, sessionId, sessionFile });
283
-
284
- expect(first.exitCode).toBe(0);
285
- expect(first.output.trim()).toBe(realDirA);
286
- expect(second.exitCode).toBe(0);
287
- expect(second.output.trim().split("\n")).toEqual([realDirB, "False"]);
288
- expect(third.exitCode).toBe(0);
289
- expect(third.output.trim()).toBe("from-a");
290
- });
291
-
292
- it("interrupts timed out synchronous Python cells before they mutate shared state", async () => {
293
- using tempDir = TempDir.createSync("@omp-eval-py-sync-timeout-");
294
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
295
- const sessionId = `py-sync-timeout:${crypto.randomUUID()}`;
296
-
297
- const timedOut = await executePython("import time\ntime.sleep(0.2)\nleaked_after_timeout = True", {
298
- cwd: tempDir.path(),
299
- sessionId,
300
- sessionFile,
301
- timeoutMs: 20,
302
- });
303
- await Bun.sleep(250);
304
- const probe = await executePython('print("leaked_after_timeout" in globals())', {
305
- cwd: tempDir.path(),
306
- sessionId,
307
- sessionFile,
308
- });
309
-
310
- expect(timedOut.cancelled).toBe(true);
311
- expect(probe.exitCode).toBe(0);
312
- expect(probe.output.trim()).toBe("False");
313
- });
314
-
315
- it("settles Python cells that raise SystemExit", async () => {
316
- using tempDir = TempDir.createSync("@omp-eval-py-system-exit-");
317
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
318
- const sessionId = `py-system-exit:${crypto.randomUUID()}`;
319
-
320
- const result = await executePython('raise SystemExit("bye")', {
321
- cwd: tempDir.path(),
322
- sessionId,
323
- sessionFile,
324
- timeoutMs: 500,
325
- });
326
-
327
- expect(result.exitCode).toBe(1);
328
- expect(result.output).toContain("SystemExit");
329
- expect(result.output).toContain("bye");
330
- });
331
-
332
- it("lets a subagent inherit parent JavaScript and Python eval state", async () => {
333
- using tempDir = TempDir.createSync("@omp-eval-subagent-");
334
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
335
- const evalSessionId = `session:${sessionFile}:cwd:${tempDir.path()}`;
336
- const parentSession = createToolSession(tempDir.path(), sessionFile, evalSessionId);
337
- let seenJs = "";
338
- let seenPy = "";
339
- let capturedOptions: CreateAgentSessionOptions | undefined;
340
-
341
- await executeJs('globalThis.parentSecret = "hello-js";', {
342
- sessionId: `js:${evalSessionId}`,
343
- session: parentSession,
344
- sessionFile,
345
- });
346
- await executePython('parent_secret = "hello-py"', {
347
- cwd: tempDir.path(),
348
- sessionId: `python:${evalSessionId}`,
349
- sessionFile,
350
- });
351
-
352
- vi.spyOn(discoveryModule, "discoverAgents").mockResolvedValue({ agents: [taskAgent], projectAgentsDir: null });
353
- vi.spyOn(sdkModule, "createAgentSession").mockImplementation(async (options = {}) => {
354
- capturedOptions = options;
355
- const inherited = options.parentEvalSessionId;
356
- if (!inherited) throw new Error("Missing parent eval session id");
357
- return {
358
- session: createYieldingSubagentSession(async () => {
359
- const jsResult = await executeJs("return globalThis.parentSecret;", {
360
- sessionId: `js:${inherited}`,
361
- session: parentSession,
362
- sessionFile,
363
- });
364
- const pyResult = await executePython("print(parent_secret)", {
365
- cwd: tempDir.path(),
366
- sessionId: `python:${inherited}`,
367
- sessionFile,
368
- });
369
- seenJs = jsResult.output.trim();
370
- seenPy = pyResult.output.trim();
371
- }),
372
- extensionsResult: {} as unknown as LoadExtensionsResult,
373
- setToolUIContext: () => {},
374
- eventBus: new EventBus(),
375
- } satisfies CreateAgentSessionResult;
376
- });
377
-
378
- const tool = await TaskTool.create(parentSession);
379
- await tool.execute("tool-call", taskParams);
380
-
381
- expect(capturedOptions?.parentEvalSessionId).toBe(evalSessionId);
382
- expect(seenJs).toBe("hello-js");
383
- expect(seenPy).toBe("hello-py");
384
- });
385
-
386
- it("routes interleaved JavaScript display output to the matching run", async () => {
387
- using tempDir = TempDir.createSync("@omp-eval-js-interleave-");
388
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
389
- const sessionId = `js-interleave:${crypto.randomUUID()}`;
390
- const session = createToolSession(tempDir.path(), sessionFile);
391
-
392
- const first = executeJs('await Bun.sleep(80); display({ label: "A" });', {
393
- sessionId,
394
- session,
395
- sessionFile,
396
- });
397
- await Bun.sleep(10);
398
- const second = executeJs('display({ label: "B" });', {
399
- sessionId,
400
- session,
401
- sessionFile,
402
- });
403
-
404
- const [firstResult, secondResult] = await Promise.all([first, second]);
405
- expect(firstResult.exitCode).toBe(0);
406
- expect(secondResult.exitCode).toBe(0);
407
- expect(firstResult.displayOutputs).toEqual([{ type: "json", data: { label: "A" } }]);
408
- expect(secondResult.displayOutputs).toEqual([{ type: "json", data: { label: "B" } }]);
409
- });
410
-
411
- it("routes interleaved Python display output to the matching run", async () => {
412
- using tempDir = TempDir.createSync("@omp-eval-py-interleave-");
413
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
414
- const sessionId = `py-interleave:${crypto.randomUUID()}`;
415
-
416
- const first = executePython(
417
- `import asyncio
418
- await asyncio.sleep(0.08)
419
- display({"label": "A"})`,
420
- {
421
- cwd: tempDir.path(),
422
- sessionId,
423
- sessionFile,
424
- },
425
- );
426
- await Bun.sleep(10);
427
- const second = executePython('display({"label": "B"})', {
428
- cwd: tempDir.path(),
429
- sessionId,
430
- sessionFile,
431
- });
432
-
433
- const [firstResult, secondResult] = await Promise.all([first, second]);
434
- expect(firstResult.exitCode).toBe(0);
435
- expect(secondResult.exitCode).toBe(0);
436
- expect(firstResult.displayOutputs).toEqual([{ type: "json", data: { label: "A" } }]);
437
- expect(secondResult.displayOutputs).toEqual([{ type: "json", data: { label: "B" } }]);
438
- });
439
- it("preserves module-level singleton state across re-imports of an unchanged file", async () => {
440
- using tempDir = TempDir.createSync("@omp-eval-js-mtime-");
441
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
442
- const sessionId = `js-mtime:${crypto.randomUUID()}`;
443
- const session = createToolSession(tempDir.path(), sessionFile);
444
- const modulePath = path.join(tempDir.path(), "singleton.ts");
445
- const moduleSpec = JSON.stringify(modulePath);
446
- await Bun.write(
447
- modulePath,
448
- "let value = 0;\nexport function set(v) { value = v; }\nexport function get() { return value; }\n",
449
- );
450
-
451
- const initResult = await executeJs(`const mod = await import(${moduleSpec}); mod.set(42); return mod.get();`, {
452
- sessionId,
453
- session,
454
- sessionFile,
455
- });
456
- expect(initResult.exitCode).toBe(0);
457
- expect(initResult.output.trim()).toBe("42");
458
-
459
- // Unchanged file: re-import must reuse the existing module namespace so the
460
- // counter is still 42. This is the regression — the previous unconditional
461
- // `delete require.cache[target]` reset singletons on every dynamic import.
462
- const reuseResult = await executeJs(`const mod = await import(${moduleSpec}); return mod.get();`, {
463
- sessionId,
464
- session,
465
- sessionFile,
466
- });
467
- expect(reuseResult.exitCode).toBe(0);
468
- expect(reuseResult.output.trim()).toBe("42");
469
-
470
- // Bump mtime by 5s to simulate an edit; the next import must evict the cache
471
- // and re-evaluate the file, dropping the counter back to its initializer.
472
- const future = new Date(Date.now() + 5_000);
473
- await fs.utimes(modulePath, future, future);
474
-
475
- const reloadResult = await executeJs(`const mod = await import(${moduleSpec}); return mod.get();`, {
476
- sessionId,
477
- session,
478
- sessionFile,
479
- });
480
- expect(reloadResult.exitCode).toBe(0);
481
- expect(reloadResult.output.trim()).toBe("0");
482
- });
483
-
484
- it("reloads a local re-export when a transitive dependency changes", async () => {
485
- using tempDir = TempDir.createSync("@omp-eval-js-transitive-");
486
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
487
- const sessionId = `js-transitive:${crypto.randomUUID()}`;
488
- const session = createToolSession(tempDir.path(), sessionFile);
489
- const leafPath = path.join(tempDir.path(), "leaf.ts");
490
- const entryPath = path.join(tempDir.path(), "entry.ts");
491
- const entrySpec = JSON.stringify(entryPath);
492
- await Bun.write(leafPath, "export const value = 1;\n");
493
- await Bun.write(entryPath, 'export { value } from "./leaf.ts";\n');
494
-
495
- const initial = await executeJs(`const mod = await import(${entrySpec}); return mod.value;`, {
496
- sessionId,
497
- session,
498
- sessionFile,
499
- });
500
- expect(initial.exitCode).toBe(0);
501
- expect(initial.output.trim()).toBe("1");
502
-
503
- await Bun.write(leafPath, "export const value = 2;\n");
504
- const future = new Date(Date.now() + 5_000);
505
- await fs.utimes(leafPath, future, future);
506
-
507
- const reloaded = await executeJs(`const mod = await import(${entrySpec}); return mod.value;`, {
508
- sessionId,
509
- session,
510
- sessionFile,
511
- });
512
- expect(reloaded.exitCode).toBe(0);
513
- expect(reloaded.output.trim()).toBe("2");
514
- });
515
-
516
- it("links a cyclic local module graph without crashing", async () => {
517
- // Regression: the loader used to link()+evaluate() each local module individually
518
- // inside the recursive linker callback. On any import cycle that re-entered Bun's
519
- // node:vm linker mid-instantiation and segfaulted the process (SIGTRAP,
520
- // getImportedModule on a null record) — e.g. `await import("…/edit/streaming.ts")`,
521
- // whose relative-import subtree is cyclic. The graph must now link in a single pass.
522
- using tempDir = TempDir.createSync("@omp-eval-js-cycle-");
523
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
524
- const sessionId = `js-cycle:${crypto.randomUUID()}`;
525
- const session = createToolSession(tempDir.path(), sessionFile);
526
- const alphaPath = path.join(tempDir.path(), "alpha.ts");
527
- const betaPath = path.join(tempDir.path(), "beta.ts");
528
- const alphaSpec = JSON.stringify(alphaPath);
529
- const betaSpec = JSON.stringify(betaPath);
530
- await Bun.write(
531
- alphaPath,
532
- 'import { betaName } from "./beta.ts";\nexport const alphaName = "alpha";\nexport function combined() { return alphaName + ":" + betaName; }\n',
533
- );
534
- await Bun.write(
535
- betaPath,
536
- 'import { alphaName } from "./alpha.ts";\nexport const betaName = "beta";\nexport function viaAlpha() { return alphaName; }\n',
537
- );
538
-
539
- const result = await executeJs(
540
- `const a = await import(${alphaSpec});\nconst b = await import(${betaSpec});\nreturn [a.combined(), b.viaAlpha()].join("|");`,
541
- { sessionId, session, sessionFile },
542
- );
543
-
544
- expect(result.exitCode).toBe(0);
545
- expect(result.output.trim()).toBe("alpha:beta|alpha");
546
- });
547
-
548
- it("loads TypeScript type-only imports in cells and local modules", async () => {
549
- using tempDir = TempDir.createSync("@omp-eval-js-type-imports-");
550
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
551
- const sessionId = `js-type-imports:${crypto.randomUUID()}`;
552
- const session = createToolSession(tempDir.path(), sessionFile);
553
- const typesPath = path.join(tempDir.path(), "types.ts");
554
- const valuesPath = path.join(tempDir.path(), "values.ts");
555
- const entryPath = path.join(tempDir.path(), "entry.ts");
556
- const typesSpec = JSON.stringify(typesPath);
557
- const entrySpec = JSON.stringify(entryPath);
558
- await Bun.write(typesPath, "export interface TypeOnly { value: number }\n");
559
- await Bun.write(valuesPath, "export interface InlineOnly { value: number }\nexport const imported = 41;\n");
560
- await Bun.write(
561
- entryPath,
562
- [
563
- 'import type { TypeOnly } from "./types.ts";',
564
- 'import { type InlineOnly, imported } from "./values.ts";',
565
- "export const typeOnly = 1;",
566
- "export const inlineType = imported;",
567
- "",
568
- ].join("\n"),
569
- );
570
-
571
- const result = await executeJs(
572
- `import type { TypeOnly } from ${typesSpec};\nconst mod = await import(${entrySpec});\nreturn mod.typeOnly + mod.inlineType;`,
573
- {
574
- sessionId,
575
- session,
576
- sessionFile,
577
- },
578
- );
579
-
580
- expect(result.exitCode).toBe(0);
581
- expect(result.output.trim()).toBe("42");
582
- });
583
-
584
- it("refreshes the Python tool proxy when bridge env appears after kernel warm-up", async () => {
585
- using tempDir = TempDir.createSync("@omp-eval-py-tool-proxy-");
586
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
587
- const sessionId = `py-tool-proxy:${crypto.randomUUID()}`;
588
- const bridgeCalls: unknown[] = [];
589
- const bridgeSession = createBridgeToolSession("bridge-ok", bridgeCalls);
590
-
591
- const withoutBridge = await executePython(
592
- 'try:\n print(tool.read({"path": "foo.txt"}))\nexcept Exception as exc:\n print(type(exc).__name__)\n print(str(exc))',
593
- { cwd: tempDir.path(), sessionId, sessionFile },
594
- );
595
- const withBridge = await executePython('print(tool.read({"path": "foo.txt"}))', {
596
- cwd: tempDir.path(),
597
- sessionId,
598
- sessionFile,
599
- toolSession: bridgeSession,
600
- });
601
-
602
- expect(withoutBridge.exitCode).toBe(0);
603
- expect(withoutBridge.output).toContain("RuntimeError");
604
- expect(withoutBridge.output).toContain("tool bridge is unavailable");
605
- expect(withBridge.exitCode).toBe(0);
606
- expect(withBridge.output.trim()).toBe("bridge-ok");
607
- expect(bridgeCalls).toEqual([{ path: "foo.txt", _i: "py prelude" }]);
608
- });
609
- });
@@ -1,74 +0,0 @@
1
- /**
2
- * Keepalive for in-flight host-side eval bridge calls.
3
- *
4
- * The eval watchdog ({@link ../tools/eval IdleTimeout}) caps a cell's `timeout`
5
- * as a wall-clock budget on the cell's *own* work, but pauses that budget while
6
- * a host-side `agent()`/`parallel()` (via `runSubprocess`) or `llm()` (a single
7
- * completion) call is in flight. Those calls are the only thing that re-arms the
8
- * watchdog — and they can run for long stretches with **no** status of their own
9
- * (a subagent's time-to-first-token on a reasoning model, a long quiet nested
10
- * tool, or the entire body of a oneshot `llm()` call). Without a keepalive the
11
- * watchdog would mistake that delegated work for the cell stalling and abort it
12
- * mid-flight, killing the subagent.
13
- *
14
- * {@link withBridgeHeartbeat} bridges that gap by emitting a synthetic
15
- * {@link EVAL_HEARTBEAT_OP} status event immediately when the call begins and
16
- * then on a fixed cadence until it settles. The event rides the same
17
- * `emitStatus → onStatus` channel both runtimes already forward, so it re-arms
18
- * the watchdog without any new plumbing. The heartbeat is the *sole* signal that
19
- * extends the budget: consumers MUST treat it as a pure keepalive — bump the
20
- * watchdog and drop it (never persist or render it) — see the executor display
21
- * sinks and the eval tool's `onStatus` handler. Every other status event
22
- * (compute helpers, `log()`/`phase()`, tool results) counts against the budget.
23
- */
24
- import type { JsStatusEvent } from "./js/shared/types";
25
-
26
- /**
27
- * Synthetic status op emitted purely to keep the eval idle watchdog alive while
28
- * a host-side bridge call is in flight. Carries no payload.
29
- */
30
- export const EVAL_HEARTBEAT_OP = "heartbeat";
31
-
32
- /**
33
- * Heartbeat cadence. Comfortably below the default 30s idle budget (and the
34
- * larger budgets long fanouts run under), so a working bridge call always bumps
35
- * the watchdog before it expires, while a genuine stall is still bounded once
36
- * the call settles and the heartbeat stops.
37
- */
38
- const HEARTBEAT_INTERVAL_MS = 5_000;
39
-
40
- let heartbeatIntervalMs = HEARTBEAT_INTERVAL_MS;
41
-
42
- /**
43
- * Test seam: override the heartbeat cadence so integration tests can exercise
44
- * the keepalive within a sub-second idle budget. Pass no value to restore the
45
- * production default.
46
- */
47
- export function setBridgeHeartbeatIntervalMs(ms?: number): void {
48
- heartbeatIntervalMs = ms === undefined ? HEARTBEAT_INTERVAL_MS : Math.max(1, Math.floor(ms));
49
- }
50
-
51
- /**
52
- * Run {@link operation}, pumping {@link EVAL_HEARTBEAT_OP} status events through
53
- * {@link emitStatus} — one immediately, then on a fixed cadence — until it
54
- * settles. The immediate beat pauses the watchdog the instant the call begins,
55
- * so a bridge call that starts close to the budget edge (after the cell already
56
- * spent most of it computing) is not aborted before the first interval tick. A
57
- * no-op wrapper when no `emitStatus` sink is wired (the heartbeat would reach
58
- * nobody).
59
- */
60
- export async function withBridgeHeartbeat<T>(
61
- emitStatus: ((event: JsStatusEvent) => void) | undefined,
62
- operation: () => Promise<T>,
63
- ): Promise<T> {
64
- if (!emitStatus) return operation();
65
- emitStatus({ op: EVAL_HEARTBEAT_OP });
66
- const timer = setInterval(() => emitStatus({ op: EVAL_HEARTBEAT_OP }), heartbeatIntervalMs);
67
- // Never keep the event loop alive for the heartbeat alone.
68
- timer.unref?.();
69
- try {
70
- return await operation();
71
- } finally {
72
- clearInterval(timer);
73
- }
74
- }