codemode.test.ts 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206
  1. import { describe, expect, test } from "bun:test"
  2. import { Cause, Effect, Schema } from "effect"
  3. import { CodeMode, Tool, toolError } from "../src/index.js"
  4. const run = (tool: Tool.Tool<never>) =>
  5. Effect.runPromise(CodeMode.make({ tools: { host: { call: tool } } }).execute("return await tools.host.call({})"))
  6. class UnsafeHostError extends Schema.TaggedErrorClass<UnsafeHostError>()("UnsafeHostError", {
  7. reason: Schema.String,
  8. }) {}
  9. describe("CodeMode host failure boundary", () => {
  10. test("preserves explicit safe tool failures", async () => {
  11. const result = await run(
  12. Tool.make({
  13. description: "Fail safely",
  14. input: Schema.Struct({}),
  15. output: Schema.String,
  16. execute: () => Effect.fail(toolError("Authorized request was refused")),
  17. }),
  18. )
  19. expect(result.ok ? undefined : result.error).toStrictEqual({
  20. kind: "ToolFailure",
  21. message: "Authorized request was refused",
  22. })
  23. })
  24. test("does not rewrite explicit safe tool failures", async () => {
  25. const result = await run(
  26. Tool.make({
  27. description: "Fail safely",
  28. input: Schema.Struct({}),
  29. output: Schema.String,
  30. execute: () => Effect.fail(toolError("File not found: /tmp/report.json")),
  31. }),
  32. )
  33. expect(result.ok ? undefined : result.error).toStrictEqual({
  34. kind: "ToolFailure",
  35. message: "File not found: /tmp/report.json",
  36. })
  37. })
  38. test("sanitizes unknown host failures and defects", async () => {
  39. for (const failure of [
  40. Effect.fail(new UnsafeHostError({ reason: "Authorization: Bearer typed-secret" })),
  41. Effect.die(new Error("postgres://user:defect-secret@example.invalid")),
  42. ]) {
  43. const result = await run(
  44. Tool.make({
  45. description: "Fail internally",
  46. input: Schema.Struct({}),
  47. output: Schema.String,
  48. execute: () => failure,
  49. }),
  50. )
  51. expect(result.ok ? undefined : result.error).toStrictEqual({
  52. kind: "ToolFailure",
  53. message: "Tool execution failed",
  54. })
  55. expect(JSON.stringify(result)).not.toMatch(/typed-secret|defect-secret|Authorization: Bearer/)
  56. }
  57. })
  58. test("sanitizes invalid host output", async () => {
  59. const secret = "invalid-output-secret"
  60. const result = await run(
  61. Tool.make({
  62. description: "Return invalid output",
  63. input: Schema.Struct({}),
  64. output: Schema.Struct({ safe: Schema.String }),
  65. execute: () => Effect.succeed({ safe: 1, secret } as unknown as { readonly safe: string }),
  66. }),
  67. )
  68. expect(result.ok ? undefined : result.error).toStrictEqual({
  69. kind: "InvalidToolOutput",
  70. message: "Invalid output from tool 'host.call'.",
  71. })
  72. expect(JSON.stringify(result)).not.toMatch(/invalid-output-secret/)
  73. })
  74. test("sanitizes host output that throws while being copied", async () => {
  75. const result = await run(
  76. Tool.make({
  77. description: "Return hostile output",
  78. input: Schema.Struct({}),
  79. output: Schema.Unknown,
  80. execute: () =>
  81. Effect.succeed(
  82. new Proxy(
  83. {},
  84. {
  85. ownKeys: () => {
  86. throw new Error("host-output-secret")
  87. },
  88. },
  89. ),
  90. ),
  91. }),
  92. )
  93. expect(result.ok ? undefined : result.error).toStrictEqual({
  94. kind: "InvalidToolOutput",
  95. message: "Invalid output from tool 'host.call'.",
  96. })
  97. expect(JSON.stringify(result)).not.toMatch(/host-output-secret/)
  98. })
  99. test("caught tool failures are Error values in-program", async () => {
  100. const result = await Effect.runPromise(
  101. CodeMode.make({
  102. tools: {
  103. host: {
  104. call: Tool.make({
  105. description: "Refuse",
  106. input: Schema.Struct({}),
  107. output: Schema.String,
  108. execute: () => Effect.fail(toolError("Refused")),
  109. }),
  110. },
  111. },
  112. }).execute(`
  113. try {
  114. await tools.host.call({})
  115. return "no"
  116. } catch (e) {
  117. return { isError: e instanceof Error, message: e.message }
  118. }
  119. `),
  120. )
  121. expect(result.ok).toBe(true)
  122. if (result.ok) expect(result.value).toStrictEqual({ isError: true, message: "Refused" })
  123. })
  124. test("propagates host interruption instead of returning a diagnostic", async () => {
  125. const exit = await Effect.runPromiseExit(
  126. CodeMode.make({
  127. tools: {
  128. host: {
  129. call: Tool.make({
  130. description: "Interrupt",
  131. input: Schema.Struct({}),
  132. output: Schema.String,
  133. execute: () => Effect.interrupt,
  134. }),
  135. },
  136. },
  137. }).execute("return await tools.host.call({})"),
  138. )
  139. expect(exit._tag).toBe("Failure")
  140. if (exit._tag === "Failure") {
  141. expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true)
  142. }
  143. })
  144. })
  145. describe("CodeMode tool-call observation", () => {
  146. test("reports the tools actually invoked with decoded input", async () => {
  147. const calls: Array<unknown> = []
  148. const lookup = Tool.make({
  149. description: "Look up a value",
  150. input: Schema.Struct({ query: Schema.String }),
  151. output: Schema.String,
  152. execute: ({ query }) => Effect.succeed(query),
  153. })
  154. const result = await Effect.runPromise(
  155. CodeMode.make({
  156. tools: { context: { lookup } },
  157. onToolCallStart: (call) => Effect.sync(() => calls.push(call)),
  158. }).execute(`
  159. if (false) await tools.context.lookup({ query: "not called" })
  160. return await tools.context.lookup({ query: "deployment failure" })
  161. `),
  162. )
  163. expect(result.ok).toBe(true)
  164. expect(calls).toStrictEqual([{ index: 0, name: "context.lookup", input: { query: "deployment failure" } }])
  165. })
  166. test("observes settled calls with outcome and duration", async () => {
  167. const events: Array<{ phase: string; index: number; name: string; outcome?: string; message?: string }> = []
  168. const lookup = Tool.make({
  169. description: "Look up a value",
  170. input: Schema.Struct({ query: Schema.String }),
  171. output: Schema.String,
  172. execute: ({ query }) =>
  173. query === "boom"
  174. ? Effect.fail(toolError("Lookup refused"))
  175. : query === "defect"
  176. ? Effect.die("broken")
  177. : Effect.succeed(query),
  178. })
  179. const runtime = CodeMode.make({
  180. tools: { context: { lookup } },
  181. onToolCallStart: (call) =>
  182. Effect.sync(() => {
  183. events.push({ phase: "start", index: call.index, name: call.name })
  184. }),
  185. onToolCallEnd: (call) =>
  186. Effect.sync(() => {
  187. expect(call.durationMs).toBeGreaterThanOrEqual(0)
  188. events.push({
  189. phase: "end",
  190. index: call.index,
  191. name: call.name,
  192. outcome: call.outcome,
  193. ...(call.message === undefined ? {} : { message: call.message }),
  194. })
  195. }),
  196. })
  197. const success = await Effect.runPromise(runtime.execute(`return await tools.context.lookup({ query: "ok" })`))
  198. expect(success.ok).toBe(true)
  199. const failure = await Effect.runPromise(runtime.execute(`return await tools.context.lookup({ query: "boom" })`))
  200. expect(failure.ok).toBe(false)
  201. const defect = await Effect.runPromise(runtime.execute(`return await tools.context.lookup({ query: "defect" })`))
  202. expect(defect.ok).toBe(false)
  203. expect(events).toStrictEqual([
  204. { phase: "start", index: 0, name: "context.lookup" },
  205. { phase: "end", index: 0, name: "context.lookup", outcome: "success" },
  206. { phase: "start", index: 0, name: "context.lookup" },
  207. { phase: "end", index: 0, name: "context.lookup", outcome: "failure", message: "Lookup refused" },
  208. { phase: "start", index: 0, name: "context.lookup" },
  209. { phase: "end", index: 0, name: "context.lookup", outcome: "failure", message: "Tool execution failed" },
  210. ])
  211. })
  212. test("observes interrupted calls", async () => {
  213. const events: Array<string> = []
  214. const call = Tool.make({
  215. description: "Interrupt",
  216. input: Schema.Struct({}),
  217. output: Schema.String,
  218. execute: () => Effect.interrupt,
  219. })
  220. const exit = await Effect.runPromiseExit(
  221. CodeMode.make({
  222. tools: { host: { call } },
  223. onToolCallStart: () => Effect.sync(() => events.push("start")),
  224. onToolCallEnd: (call) => Effect.sync(() => events.push(`end:${call.outcome}`)),
  225. }).execute("return await tools.host.call({})"),
  226. )
  227. expect(exit._tag).toBe("Failure")
  228. expect(events).toEqual(["start", "end:interrupted"])
  229. })
  230. test("observes running calls interrupted during completion", async () => {
  231. const events: Array<string> = []
  232. const call = Tool.make({
  233. description: "Pending",
  234. input: Schema.Struct({}),
  235. output: Schema.String,
  236. execute: () => Effect.never,
  237. })
  238. const result = await Effect.runPromise(
  239. CodeMode.make({
  240. tools: { host: { call } },
  241. onToolCallStart: () => Effect.sync(() => events.push("start")),
  242. onToolCallEnd: (call) => Effect.sync(() => events.push(`end:${call.outcome}`)),
  243. }).execute('tools.host.call({}); return "done"'),
  244. )
  245. expect(result).toMatchObject({ ok: true, value: "done" })
  246. expect(events).toEqual(["start", "end:interrupted"])
  247. })
  248. test("ends calls interrupted during start observation", async () => {
  249. const events: Array<string> = []
  250. const call = Tool.make({
  251. description: "Unused",
  252. input: Schema.Struct({}),
  253. output: Schema.String,
  254. execute: () => Effect.succeed("unused"),
  255. })
  256. const exit = await Effect.runPromiseExit(
  257. CodeMode.make({
  258. tools: { host: { call } },
  259. onToolCallStart: () => Effect.interrupt,
  260. onToolCallEnd: (call) => Effect.sync(() => events.push(call.outcome)),
  261. }).execute("return await tools.host.call({})"),
  262. )
  263. expect(exit._tag).toBe("Failure")
  264. expect(events).toEqual(["interrupted"])
  265. })
  266. test("observes calls interrupted by the execution timeout", async () => {
  267. const outcomes: Array<string> = []
  268. const call = Tool.make({
  269. description: "Pending",
  270. input: Schema.Struct({}),
  271. output: Schema.String,
  272. execute: () => Effect.never,
  273. })
  274. const result = await Effect.runPromise(
  275. CodeMode.make({
  276. tools: { host: { call } },
  277. limits: { timeoutMs: 10 },
  278. onToolCallEnd: (call) => Effect.sync(() => outcomes.push(call.outcome)),
  279. }).execute("return await tools.host.call({})"),
  280. )
  281. expect(result).toMatchObject({ ok: false, error: { kind: "TimeoutExceeded" } })
  282. expect(outcomes).toEqual(["interrupted"])
  283. })
  284. })
  285. describe("CodeMode console capture", () => {
  286. test("captures console output as bounded result logs", async () => {
  287. const result = await Effect.runPromise(
  288. CodeMode.execute({
  289. code: `
  290. const returned = console.log("Thread info:", { name: "Demo", count: 2 })
  291. console.warn("careful")
  292. return returned
  293. `,
  294. }),
  295. )
  296. expect(result).toStrictEqual({
  297. ok: true,
  298. value: null,
  299. logs: ['Thread info: {"name":"Demo","count":2}', "[warn] careful"],
  300. toolCalls: [],
  301. })
  302. expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
  303. })
  304. test("keeps logs captured before failures", async () => {
  305. const result = await Effect.runPromise(
  306. CodeMode.execute({
  307. code: `
  308. console.log("before failure")
  309. throw new Error("boom")
  310. `,
  311. }),
  312. )
  313. expect(result.ok ? undefined : result.logs).toStrictEqual(["before failure"])
  314. expect(result.ok ? undefined : result.error.message).toBe("Uncaught: boom")
  315. })
  316. test("prints NaN and Infinity literally instead of the JSON null", async () => {
  317. const result = await Effect.runPromise(
  318. CodeMode.execute({
  319. code: `
  320. console.log(NaN)
  321. console.log(Infinity, -Infinity)
  322. console.log({ ratio: NaN, bounds: [Infinity] })
  323. return null
  324. `,
  325. }),
  326. )
  327. expect(result.ok).toBe(true)
  328. expect(result.logs).toStrictEqual(["NaN", "Infinity -Infinity", '{"ratio":NaN,"bounds":[Infinity]}'])
  329. })
  330. test("renders CodeMode values nested inside logged containers", async () => {
  331. const result = await Effect.runPromise(
  332. CodeMode.execute({
  333. code: `
  334. console.log({ m: new Map([["a", 1]]), when: new Date(0), r: /ab/g, s: new Set([1, 2]) })
  335. console.log([new Date(0)])
  336. return null
  337. `,
  338. }),
  339. )
  340. expect(result.ok).toBe(true)
  341. expect(result.logs).toStrictEqual([
  342. '{"m":Map(1) [["a",1]],"when":1970-01-01T00:00:00.000Z,"r":/ab/g,"s":Set(2) [1,2]}',
  343. "[1970-01-01T00:00:00.000Z]",
  344. ])
  345. })
  346. test("console formatting is total: cycles and opaque references render as markers", async () => {
  347. const result = await Effect.runPromise(
  348. CodeMode.execute({
  349. code: `
  350. const m = new Map()
  351. m.set("self", m)
  352. console.log({ box: m })
  353. console.log({ fn: (x) => x, ok: 1 })
  354. return null
  355. `,
  356. }),
  357. )
  358. expect(result.ok).toBe(true)
  359. expect(result.logs).toStrictEqual(['{"box":Map(1) [["self",[Circular]]]}', '{"fn":[opaque reference],"ok":1}'])
  360. })
  361. test("console.table renders CodeMode value cells", async () => {
  362. const result = await Effect.runPromise(
  363. CodeMode.execute({
  364. code: `
  365. console.table([{ when: new Date(0), n: NaN }])
  366. return null
  367. `,
  368. }),
  369. )
  370. expect(result.ok).toBe(true)
  371. expect(result.logs).toStrictEqual(["(index)\twhen\tn\n0\t1970-01-01T00:00:00.000Z\tNaN"])
  372. })
  373. test("captures console.dir and console.table output", async () => {
  374. const result = await Effect.runPromise(
  375. CodeMode.execute({
  376. code: `
  377. console.dir({ nested: { ok: true } })
  378. console.table([
  379. { name: "Kit", count: 1, hidden: "x" },
  380. { name: "Olive", count: 2, hidden: "y" }
  381. ], ["name", "count"])
  382. return "done"
  383. `,
  384. }),
  385. )
  386. expect(result).toStrictEqual({
  387. ok: true,
  388. value: "done",
  389. logs: ['{"nested":{"ok":true}}', "(index)\tname\tcount\n0\tKit\t1\n1\tOlive\t2"],
  390. toolCalls: [],
  391. })
  392. })
  393. })
  394. describe("CodeMode output budget", () => {
  395. test("absent maxOutputBytes means no truncation at all", async () => {
  396. const result = await Effect.runPromise(
  397. CodeMode.execute({
  398. code: `console.log("z".repeat(50_000)); return "x".repeat(100_000)`,
  399. }),
  400. )
  401. expect(result.ok).toBe(true)
  402. if (!result.ok) return
  403. expect(result.truncated).toBeUndefined()
  404. expect(result.value).toBe("x".repeat(100_000))
  405. expect(result.logs).toStrictEqual(["z".repeat(50_000)])
  406. })
  407. test("truncates an oversized result value with a marker instead of failing", async () => {
  408. const limits: CodeMode.ExecutionLimits = { maxOutputBytes: 40 }
  409. const result = await Effect.runPromise(
  410. CodeMode.execute({
  411. code: `return { data: "${"x".repeat(200)}" }`,
  412. limits,
  413. }),
  414. )
  415. expect(result.ok).toBe(true)
  416. if (!result.ok) return
  417. expect(result.truncated).toBe(true)
  418. expect(typeof result.value).toBe("string")
  419. expect(result.value).toMatch(
  420. /^\{"data":"x+ \[result truncated: \d+ bytes exceeds the 40-byte output limit; return a smaller value\]$/,
  421. )
  422. expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
  423. })
  424. test("keeps leading logs within the remaining budget and marks the cut", async () => {
  425. const limits: CodeMode.ExecutionLimits = { maxOutputBytes: 40 }
  426. const result = await Effect.runPromise(
  427. CodeMode.execute({
  428. code: `
  429. console.log("first line")
  430. console.log("${"y".repeat(200)}")
  431. return "ok"
  432. `,
  433. limits,
  434. }),
  435. )
  436. expect(result.ok).toBe(true)
  437. if (!result.ok) return
  438. expect(result.value).toBe("ok")
  439. expect(result.truncated).toBe(true)
  440. expect(result.logs).toStrictEqual(["first line", "[logs truncated: showing 1 of 2 lines]"])
  441. })
  442. test("does not mark results within the budget", async () => {
  443. const result = await Effect.runPromise(
  444. CodeMode.execute({
  445. code: `
  446. console.log("fits")
  447. return { fits: true }
  448. `,
  449. }),
  450. )
  451. expect(result).toStrictEqual({
  452. ok: true,
  453. value: { fits: true },
  454. logs: ["fits"],
  455. toolCalls: [],
  456. })
  457. })
  458. })
  459. describe("CodeMode schema flexibility", () => {
  460. test("accepts render-only JSON Schema input and omitted output", async () => {
  461. const observed: Array<unknown> = []
  462. const call = Tool.make({
  463. description: "Call an adapter-described tool",
  464. input: {
  465. type: "object",
  466. properties: { id: { type: "string" }, count: { type: "number" } },
  467. required: ["id"],
  468. },
  469. execute: (input) =>
  470. Effect.sync(() => {
  471. observed.push(input)
  472. return { echoed: input }
  473. }),
  474. })
  475. const runtime = CodeMode.make({ tools: { adapter: { call } } })
  476. expect(runtime.catalog()).toStrictEqual([
  477. {
  478. path: "adapter.call",
  479. description: "Call an adapter-described tool",
  480. signature: "tools.adapter.call(input: {\n id: string,\n count?: number,\n}): Promise<void>",
  481. },
  482. ])
  483. // JSON Schema is render-only: mistyped input passes through unvalidated.
  484. const result = await Effect.runPromise(runtime.execute(`return await tools.adapter.call({ id: 42 })`))
  485. expect(result.ok).toBe(true)
  486. if (result.ok) expect(result.value).toBeNull()
  487. expect(observed).toStrictEqual([{ id: 42 }])
  488. })
  489. test("outbound tool arguments follow JSON serialization semantics", async () => {
  490. const observed: Array<unknown> = []
  491. const call = Tool.make({
  492. description: "Observe raw input",
  493. input: { type: "object" },
  494. execute: (input) =>
  495. Effect.sync(() => {
  496. observed.push(input)
  497. return "ok"
  498. }),
  499. })
  500. const runtime = CodeMode.make({ tools: { adapter: { call } } })
  501. const result = await Effect.runPromise(
  502. runtime.execute(
  503. `return await tools.adapter.call({ q: undefined, limit: 0 / 0, rate: 1 / 0, items: [1, undefined, 2], holes: [1, , 3] })`,
  504. ),
  505. )
  506. expect(result.ok).toBe(true)
  507. const received = observed[0] as Record<string, unknown>
  508. expect(received).toStrictEqual({ limit: null, rate: null, items: [1, null, 2], holes: [1, null, 3] })
  509. // The undefined-valued property is dropped like JSON.stringify, not delivered as undefined.
  510. expect(Object.hasOwn(received, "q")).toBe(false)
  511. })
  512. test("dropping undefined values lets optionalKey schemas accept conditional arguments", async () => {
  513. const observed: Array<unknown> = []
  514. const find = Tool.make({
  515. description: "Find things",
  516. input: Schema.Struct({ query: Schema.optionalKey(Schema.String), limit: Schema.optionalKey(Schema.Number) }),
  517. execute: (input) =>
  518. Effect.sync(() => {
  519. observed.push(input)
  520. return "ok"
  521. }),
  522. })
  523. const runtime = CodeMode.make({ tools: { things: { find } } })
  524. // The `cond ? value : undefined` idiom: optionalKey rejects a present undefined, so the
  525. // JSON boundary must drop the key before the schema decodes.
  526. const result = await Effect.runPromise(
  527. runtime.execute(`return await tools.things.find({ query: undefined, limit: 5 })`),
  528. )
  529. expect(result.ok).toBe(true)
  530. expect(observed).toStrictEqual([{ limit: 5 }])
  531. const search = await Effect.runPromise(runtime.execute(`return (await search({ query: undefined })).items.length`))
  532. expect(search.ok).toBe(true)
  533. })
  534. test("renders JSON Schema outputs and $defs references", async () => {
  535. const lookup = Tool.make({
  536. description: "Look up a user",
  537. input: { type: "object", properties: { login: { type: "string" } }, required: ["login"] },
  538. output: {
  539. $ref: "#/$defs/User",
  540. $defs: {
  541. User: {
  542. type: "object",
  543. properties: { login: { type: "string" }, id: { type: "number" } },
  544. required: ["login", "id"],
  545. },
  546. },
  547. },
  548. execute: () => Effect.succeed({ login: "kit", id: 7 }),
  549. })
  550. const runtime = CodeMode.make({ tools: { users: { lookup } } })
  551. expect(runtime.catalog()).toStrictEqual([
  552. {
  553. path: "users.lookup",
  554. description: "Look up a user",
  555. signature: "tools.users.lookup(input: {\n login: string,\n}): Promise<{\n login: string,\n id: number,\n}>",
  556. },
  557. ])
  558. const result = await Effect.runPromise(runtime.execute(`return await tools.users.lookup({ login: "kit" })`))
  559. expect(result.ok).toBe(true)
  560. if (result.ok) expect(result.value).toStrictEqual({ login: "kit", id: 7 })
  561. })
  562. test("Effect Schema output without an input transform renders void when omitted", async () => {
  563. const ping = Tool.make({
  564. description: "Ping",
  565. input: Schema.Struct({ host: Schema.String }),
  566. execute: () => Effect.succeed("pong"),
  567. })
  568. const runtime = CodeMode.make({ tools: { net: { ping } } })
  569. expect(runtime.catalog()[0]?.signature).toBe("tools.net.ping(input: {\n host: string,\n}): Promise<void>")
  570. const result = await Effect.runPromise(runtime.execute(`return await tools.net.ping({ host: "example.test" })`))
  571. expect(result.ok).toBe(true)
  572. if (result.ok) expect(result.value).toBeNull()
  573. })
  574. })
  575. describe("CodeMode public contract", () => {
  576. const lookup = Tool.make({
  577. description: "Look up an order by ID",
  578. input: Schema.Struct({ id: Schema.String }),
  579. output: Schema.Struct({ id: Schema.String, status: Schema.String }),
  580. execute: ({ id }) => Effect.succeed({ id, status: "open" }),
  581. })
  582. const tools = { orders: { lookup } }
  583. const source = `return await tools.orders.lookup({ id: "order_42" })`
  584. test("keeps one-shot and reusable execution equivalent", async () => {
  585. const runtime = CodeMode.make({ tools })
  586. const [oneShot, reusable] = await Promise.all([
  587. Effect.runPromise(CodeMode.execute({ tools, code: source })),
  588. Effect.runPromise(runtime.execute(source)),
  589. ])
  590. expect(reusable).toStrictEqual(oneShot)
  591. const input: CodeMode.Input = { code: source }
  592. expect(Schema.decodeUnknownSync(CodeMode.Input)(input)).toStrictEqual(input)
  593. expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(reusable)))).toStrictEqual(reusable)
  594. })
  595. test("a reused execution Effect starts from a clean slate", async () => {
  596. const echo = Tool.make({
  597. description: "echo",
  598. input: Schema.Struct({}),
  599. output: Schema.Number,
  600. execute: () => Effect.succeed(1),
  601. })
  602. const effect = CodeMode.execute({
  603. tools: { host: { echo } },
  604. code: `console.log("hi"); return await tools.host.echo({})`,
  605. limits: { maxToolCalls: 1 },
  606. })
  607. const first = await Effect.runPromise(effect)
  608. const second = await Effect.runPromise(effect)
  609. // Per-execution state (tool-call budget and audit list, logs, timeout bookkeeping) must
  610. // bind at run time, so the second run neither exhausts the budget nor leaks run 1's logs.
  611. expect(first).toStrictEqual(second)
  612. expect(second).toStrictEqual({ ok: true, value: 1, logs: ["hi"], toolCalls: [{ name: "host.echo" }] })
  613. })
  614. test("describes the catalog and keeps the search built-in registered", async () => {
  615. const runtime = CodeMode.make({ tools })
  616. expect(runtime.catalog()).toStrictEqual([
  617. {
  618. path: "orders.lookup",
  619. description: "Look up an order by ID",
  620. signature: "tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>",
  621. },
  622. ])
  623. const result = await Effect.runPromise(runtime.execute(`return search({ query: "order" })`))
  624. expect(result.ok).toBe(true)
  625. if (result.ok) {
  626. expect(result.value).toStrictEqual({
  627. items: [
  628. {
  629. path: "tools.orders.lookup",
  630. description: "Look up an order by ID",
  631. signature:
  632. "tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>",
  633. },
  634. ],
  635. remaining: 0,
  636. next: null,
  637. })
  638. }
  639. })
  640. test("renders equivalent catalogs identically regardless of tool insertion order", () => {
  641. const alpha = Tool.make({
  642. description: "Alpha tool",
  643. input: Schema.Struct({}),
  644. output: Schema.Void,
  645. execute: () => Effect.void,
  646. })
  647. const zeta = Tool.make({
  648. description: "Zeta tool",
  649. input: Schema.Struct({}),
  650. output: Schema.Void,
  651. execute: () => Effect.void,
  652. })
  653. const first = CodeMode.make({ tools: { zeta: { zeta, alpha }, alpha: { zeta, alpha } } })
  654. const second = CodeMode.make({ tools: { alpha: { alpha, zeta }, zeta: { alpha, zeta } } })
  655. expect(first.catalog()).toStrictEqual(second.catalog())
  656. expect(first.catalog().map((tool) => tool.path)).toEqual(["alpha.alpha", "alpha.zeta", "zeta.alpha", "zeta.zeta"])
  657. })
  658. test("renders bracket notation for tool names that are not JavaScript identifiers", async () => {
  659. const resolveLibrary = Tool.make({
  660. description: "Resolve a library ID",
  661. input: Schema.Struct({ libraryName: Schema.String }),
  662. output: Schema.String,
  663. execute: ({ libraryName }) => Effect.succeed(`/resolved/${libraryName}`),
  664. })
  665. const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } })
  666. expect(runtime.catalog()).toStrictEqual([
  667. {
  668. path: "context7.resolve-library-id",
  669. description: "Resolve a library ID",
  670. signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise<string>',
  671. },
  672. ])
  673. const search = await Effect.runPromise(runtime.execute(`return search({ query: "resolve library id" })`))
  674. expect(search.ok).toBe(true)
  675. if (search.ok) {
  676. expect(search.value).toStrictEqual({
  677. items: [
  678. {
  679. path: 'tools.context7["resolve-library-id"]',
  680. description: "Resolve a library ID",
  681. signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise<string>',
  682. },
  683. ],
  684. remaining: 0,
  685. next: null,
  686. })
  687. }
  688. const call = await Effect.runPromise(
  689. runtime.execute(`return await tools.context7["resolve-library-id"]({ libraryName: "TypeScript" })`),
  690. )
  691. expect(call.ok).toBe(true)
  692. if (call.ok) expect(call.value).toBe("/resolved/TypeScript")
  693. const exact = await Effect.runPromise(
  694. runtime.execute(`return search({ query: 'tools.context7["resolve-library-id"]' })`),
  695. )
  696. expect(exact.ok).toBe(true)
  697. if (exact.ok) expect(exact.value).toMatchObject({ remaining: 0, next: null })
  698. })
  699. test("uses one ranked search returning complete tools for large catalogs", async () => {
  700. const upload = Tool.make({
  701. description: "Upload one readable local file to the current Discord thread",
  702. input: Schema.Struct({ path: Schema.String }),
  703. output: Schema.Struct({ sent: Schema.Boolean }),
  704. execute: () => Effect.succeed({ sent: true }),
  705. })
  706. const generate = Tool.make({
  707. description: "Generate an image and upload it to the current Discord thread",
  708. input: Schema.Struct({ prompt: Schema.String }),
  709. output: Schema.Struct({ sent: Schema.Boolean }),
  710. execute: () => Effect.succeed({ sent: true }),
  711. })
  712. const runtime = CodeMode.make({
  713. tools: { thread: { uploadFile: upload, generateImage: generate }, orders: { lookup } },
  714. })
  715. const result = await Effect.runPromise(
  716. runtime.execute(`
  717. return search({
  718. query: "send message attachment upload file to current Discord thread",
  719. limit: 2
  720. })
  721. `),
  722. )
  723. expect(result.ok).toBe(true)
  724. if (!result.ok) return
  725. expect(result.value).toStrictEqual({
  726. items: [
  727. {
  728. path: "tools.thread.uploadFile",
  729. description: "Upload one readable local file to the current Discord thread",
  730. signature: "tools.thread.uploadFile(input: {\n path: string,\n}): Promise<{\n sent: boolean,\n}>",
  731. },
  732. {
  733. path: "tools.thread.generateImage",
  734. description: "Generate an image and upload it to the current Discord thread",
  735. signature: "tools.thread.generateImage(input: {\n prompt: string,\n}): Promise<{\n sent: boolean,\n}>",
  736. },
  737. ],
  738. remaining: 0,
  739. next: null,
  740. })
  741. expect(result.toolCalls).toStrictEqual([{ name: "search" }])
  742. const variants = await Effect.runPromise(
  743. runtime.execute(`
  744. return [
  745. search({ query: "file" }),
  746. search({ query: "image" })
  747. ]
  748. `),
  749. )
  750. expect(variants.ok).toBe(true)
  751. if (variants.ok) {
  752. expect((variants.value as Array<{ items: Array<{ path: string }> }>)[0]?.items[0]?.path).toBe(
  753. "tools.thread.uploadFile",
  754. )
  755. expect((variants.value as Array<{ items: Array<{ path: string }> }>)[1]?.items[0]?.path).toBe(
  756. "tools.thread.generateImage",
  757. )
  758. }
  759. })
  760. test("search is a counted tool call: it burns maxToolCalls and fires the hooks", async () => {
  761. const started: Array<string> = []
  762. const ended: Array<string> = []
  763. const limited = CodeMode.make({
  764. tools,
  765. limits: { maxToolCalls: 1 },
  766. onToolCallStart: (call) => Effect.sync(() => void started.push(call.name)),
  767. onToolCallEnd: (call) => Effect.sync(() => void ended.push(`${call.name}:${call.outcome}`)),
  768. })
  769. const result = await Effect.runPromise(limited.execute(`search({}); return search({})`))
  770. expect(result.ok).toBe(false)
  771. if (!result.ok) expect(result.error.kind).toBe("ToolCallLimitExceeded")
  772. expect(started).toEqual(["search"])
  773. expect(ended).toEqual(["search:success"])
  774. })
  775. test("search is an opaque, shadowable global like other built-ins", async () => {
  776. const runtime = CodeMode.make({ tools })
  777. expect(await Effect.runPromise(runtime.execute(`return typeof search`))).toMatchObject({ value: "function" })
  778. // A program-level declaration shadows the global, as JS module scope does.
  779. const shadowed = await Effect.runPromise(runtime.execute(`const search = () => "local"; return search()`))
  780. expect(shadowed.ok).toBe(true)
  781. if (shadowed.ok) expect(shadowed.value).toBe("local")
  782. // The reference itself cannot cross the data boundary.
  783. const escaped = await Effect.runPromise(runtime.execute(`return { search }`))
  784. expect(escaped.ok).toBe(false)
  785. if (!escaped.ok) expect(escaped.error.kind).toBe("InvalidDataValue")
  786. })
  787. test("search defaults to 10 results and resolves exact tool paths", async () => {
  788. const tool = (index: number) =>
  789. Tool.make({
  790. description: `Numbered tool ${index}`,
  791. input: Schema.Struct({ id: Schema.String }),
  792. output: Schema.String,
  793. execute: () => Effect.succeed("ok"),
  794. })
  795. const runtime = CodeMode.make({
  796. tools: {
  797. many: Object.fromEntries(Array.from({ length: 14 }, (_, index) => [`tool${index}`, tool(index)])),
  798. },
  799. })
  800. const browse = await Effect.runPromise(runtime.execute(`return search({})`))
  801. expect(browse.ok).toBe(true)
  802. if (browse.ok) {
  803. const value = browse.value as {
  804. items: Array<{ path: string }>
  805. remaining: number
  806. next: { offset: number } | null
  807. }
  808. expect(value.items).toHaveLength(10)
  809. expect(value.remaining).toBe(4)
  810. expect(value.next).toStrictEqual({ offset: 10 })
  811. }
  812. for (const query of ["many.tool13", "tools.many.tool13"]) {
  813. const exact = await Effect.runPromise(runtime.execute(`return search({ query: ${JSON.stringify(query)} })`))
  814. expect(exact.ok).toBe(true)
  815. if (exact.ok) {
  816. expect(exact.value).toStrictEqual({
  817. items: [
  818. {
  819. path: "tools.many.tool13",
  820. description: "Numbered tool 13",
  821. signature: "tools.many.tool13(input: {\n id: string,\n}): Promise<string>",
  822. },
  823. ],
  824. remaining: 0,
  825. next: null,
  826. })
  827. }
  828. }
  829. })
  830. test("scopes search to one namespace and browses it alphabetically", async () => {
  831. const simple = (description: string) =>
  832. Tool.make({
  833. description,
  834. input: Schema.Struct({ id: Schema.String }),
  835. output: Schema.String,
  836. execute: () => Effect.succeed("ok"),
  837. })
  838. const runtime = CodeMode.make({
  839. tools: {
  840. github: { list_issues: simple("List issues"), create_issue: simple("Create an issue") },
  841. linear: { list_issues: simple("List Linear issues") },
  842. },
  843. })
  844. // Empty query + namespace browses just that namespace, alphabetical by path.
  845. const browse = await Effect.runPromise(runtime.execute(`return search({ query: "", namespace: "github" })`))
  846. expect(browse.ok).toBe(true)
  847. if (browse.ok) {
  848. const value = browse.value as { items: Array<{ path: string }>; remaining: number }
  849. expect(value.remaining).toBe(0)
  850. expect(value.items.map((item) => item.path)).toStrictEqual([
  851. "tools.github.create_issue",
  852. "tools.github.list_issues",
  853. ])
  854. }
  855. // A query + namespace ranks within that namespace only.
  856. const scoped = await Effect.runPromise(runtime.execute(`return search({ query: "issues", namespace: "linear" })`))
  857. expect(scoped.ok).toBe(true)
  858. if (scoped.ok) {
  859. const value = scoped.value as { items: Array<{ path: string }>; remaining: number }
  860. expect(value.remaining).toBe(0)
  861. expect(value.items[0]?.path).toBe("tools.linear.list_issues")
  862. }
  863. const invalid = await Effect.runPromise(runtime.execute(`return search({ query: "issues", namespace: 7 })`))
  864. expect(invalid.ok).toBe(false)
  865. if (!invalid.ok) expect(invalid.error.kind).toBe("InvalidToolInput")
  866. })
  867. test("matches input parameter names and partial-word substrings", async () => {
  868. const upload = Tool.make({
  869. description: "Send a document to the workspace",
  870. input: {
  871. type: "object",
  872. properties: { attachment: { type: "string", description: "Local path of the payload to send" } },
  873. required: ["attachment"],
  874. },
  875. execute: () => Effect.succeed("ok"),
  876. })
  877. const other = Tool.make({
  878. description: "Rename the workspace",
  879. input: Schema.Struct({ name: Schema.String }),
  880. output: Schema.String,
  881. execute: () => Effect.succeed("ok"),
  882. })
  883. const runtime = CodeMode.make({ tools: { files: { upload, other } } })
  884. // "attachment" appears in neither path nor description - only in the input schema's
  885. // property names, which the searchable text includes.
  886. const byParameter = await Effect.runPromise(runtime.execute(`return search({ query: "attachment" })`))
  887. expect(byParameter.ok).toBe(true)
  888. if (byParameter.ok) {
  889. const value = byParameter.value as { items: Array<{ path: string }>; remaining: number }
  890. expect(value.remaining).toBe(0)
  891. expect(value.items[0]?.path).toBe("tools.files.upload")
  892. }
  893. // Substring matching: a partial word ("docum") still hits the description.
  894. const bySubstring = await Effect.runPromise(runtime.execute(`return search({ query: "docum" })`))
  895. expect(bySubstring.ok).toBe(true)
  896. if (bySubstring.ok) {
  897. const value = bySubstring.value as { items: Array<{ path: string }>; remaining: number }
  898. expect(value.remaining).toBe(0)
  899. expect(value.items[0]?.path).toBe("tools.files.upload")
  900. }
  901. })
  902. test("a plural query term matches singular-only tool text", async () => {
  903. const simple = (description: string) =>
  904. Tool.make({
  905. description,
  906. input: Schema.Struct({ id: Schema.String }),
  907. output: Schema.String,
  908. execute: () => Effect.succeed("ok"),
  909. })
  910. const runtime = CodeMode.make({
  911. tools: {
  912. // Neither path nor description contains "issues" - only the singular "issue".
  913. tracker: { fetch_all: simple("Fetch every open issue in the project") },
  914. github: { list_issues: simple("List issues") },
  915. misc: { rename: simple("Rename the workspace") },
  916. },
  917. })
  918. // "issues" still finds the singular-only tool (term OR singular(term) per field)...
  919. const plural = await Effect.runPromise(runtime.execute(`return search({ query: "issues", namespace: "tracker" })`))
  920. expect(plural.ok).toBe(true)
  921. if (plural.ok) {
  922. const value = plural.value as { items: Array<{ path: string }>; remaining: number }
  923. expect(value.remaining).toBe(0)
  924. expect(value.items[0]?.path).toBe("tools.tracker.fetch_all")
  925. }
  926. // ...while a true "issues" path match still outranks the singular-only description match.
  927. const ranked = await Effect.runPromise(runtime.execute(`return search({ query: "issues" })`))
  928. expect(ranked.ok).toBe(true)
  929. if (ranked.ok) {
  930. const value = ranked.value as { items: Array<{ path: string }>; remaining: number }
  931. expect(value.remaining).toBe(0)
  932. expect(value.items.map((item) => item.path)).toStrictEqual([
  933. "tools.github.list_issues",
  934. "tools.tracker.fetch_all",
  935. ])
  936. }
  937. })
  938. test("empty query lists everything alphabetically by path", async () => {
  939. const simple = (description: string) =>
  940. Tool.make({
  941. description,
  942. input: Schema.Struct({}),
  943. output: Schema.String,
  944. execute: () => Effect.succeed("ok"),
  945. })
  946. // Deliberately declared out of alphabetical order.
  947. const runtime = CodeMode.make({
  948. tools: {
  949. zeta: { last: simple("Last") },
  950. alpha: { beta: simple("Middle"), aardvark: simple("First") },
  951. },
  952. })
  953. const browse = await Effect.runPromise(runtime.execute(`return search({})`))
  954. expect(browse.ok).toBe(true)
  955. if (browse.ok) {
  956. const value = browse.value as { items: Array<{ path: string }>; remaining: number; next: unknown }
  957. expect(value.items.map((item) => item.path)).toStrictEqual([
  958. "tools.alpha.aardvark",
  959. "tools.alpha.beta",
  960. "tools.zeta.last",
  961. ])
  962. expect(value.remaining).toBe(0)
  963. expect(value.next).toBeNull()
  964. }
  965. const middle = await Effect.runPromise(runtime.execute(`return search({ limit: 1, offset: 1 })`))
  966. expect(middle.ok).toBe(true)
  967. if (middle.ok) {
  968. expect(middle.value).toMatchObject({
  969. items: [{ path: "tools.alpha.beta" }],
  970. remaining: 1,
  971. next: { offset: 2 },
  972. })
  973. }
  974. const exhausted = await Effect.runPromise(runtime.execute(`return search({ limit: 1, offset: 3 })`))
  975. expect(exhausted.ok).toBe(true)
  976. if (exhausted.ok) expect(exhausted.value).toStrictEqual({ items: [], remaining: 0, next: null })
  977. })
  978. test("decodes tool input and output before exposing either side", async () => {
  979. const observed: Array<unknown> = []
  980. const transformed = Tool.make({
  981. description: "Double a number",
  982. input: Schema.Struct({ value: Schema.NumberFromString }),
  983. output: Schema.NumberFromString,
  984. execute: ({ value }) =>
  985. Effect.sync(() => {
  986. observed.push(value)
  987. return String(value * 2)
  988. }),
  989. })
  990. const runtime = CodeMode.make({
  991. tools: { math: { double: transformed } },
  992. onToolCallStart: (call) => Effect.sync(() => observed.push(call.input)),
  993. })
  994. const success = await Effect.runPromise(runtime.execute(`return await tools.math.double({ value: "21" })`))
  995. expect(success).toStrictEqual({ ok: true, value: 42, toolCalls: [{ name: "math.double" }] })
  996. expect(observed).toStrictEqual([{ value: 21 }, 21])
  997. const invalid = await Effect.runPromise(runtime.execute(`return await tools.math.double({ value: 21 })`))
  998. expect(invalid.ok).toBe(false)
  999. if (invalid.ok) return
  1000. expect(invalid.error.kind).toBe("InvalidToolInput")
  1001. expect(observed).toStrictEqual([{ value: 21 }, 21])
  1002. })
  1003. test("returns JSON-safe data and normalizes undefined to null", async () => {
  1004. const result = await Effect.runPromise(
  1005. CodeMode.execute({
  1006. code: `return { top: undefined, nested: [1, undefined] }`,
  1007. }),
  1008. )
  1009. expect(result).toStrictEqual({
  1010. ok: true,
  1011. value: { top: null, nested: [1, null] },
  1012. toolCalls: [],
  1013. })
  1014. expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
  1015. })
  1016. test("returns the final top-level expression when return is omitted", async () => {
  1017. const result = await Effect.runPromise(CodeMode.execute({ code: `1; 2` }))
  1018. expect(result).toStrictEqual({ ok: true, value: 2, toolCalls: [] })
  1019. })
  1020. test("does not implicitly return expressions nested in control flow", async () => {
  1021. const result = await Effect.runPromise(CodeMode.execute({ code: `if (true) { 2 }` }))
  1022. expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] })
  1023. })
  1024. test("returns null when the final top-level statement is not an expression", async () => {
  1025. const result = await Effect.runPromise(CodeMode.execute({ code: `1; const value = 2` }))
  1026. expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] })
  1027. })
  1028. test("rejects invalid configuration and search limits", async () => {
  1029. expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: 0 } })).toThrow(RangeError)
  1030. expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow(
  1031. RangeError,
  1032. )
  1033. expect(() => CodeMode.execute({ code: "return 1", limits: { maxToolCalls: -1 } })).toThrow(RangeError)
  1034. expect(() => CodeMode.execute({ code: "return 1", limits: { maxOutputBytes: -1 } })).toThrow(RangeError)
  1035. const result = await Effect.runPromise(
  1036. CodeMode.make({ tools }).execute(`return search({ query: "order", limit: 0.5 })`),
  1037. )
  1038. expect(result.ok).toBe(false)
  1039. if (result.ok) return
  1040. expect(result.error.kind).toBe("InvalidToolInput")
  1041. for (const offset of [-1, 0.5, Number.MAX_SAFE_INTEGER + 1, "1"]) {
  1042. const invalidOffset = await Effect.runPromise(
  1043. CodeMode.make({ tools }).execute(`return search({ query: "order", offset: ${JSON.stringify(offset)} })`),
  1044. )
  1045. expect(invalidOffset.ok).toBe(false)
  1046. if (!invalidOffset.ok) expect(invalidOffset.error.kind).toBe("InvalidToolInput")
  1047. }
  1048. })
  1049. test("enforces the tool-call limit as a diagnostic", async () => {
  1050. const result = await Effect.runPromise(CodeMode.execute({ tools, code: source, limits: { maxToolCalls: 0 } }))
  1051. expect(result.ok).toBe(false)
  1052. if (!result.ok) expect(result.error.kind).toBe("ToolCallLimitExceeded")
  1053. })
  1054. test("timeoutMs and maxToolCalls have no defaults: absent means unlimited", async () => {
  1055. // 150 tool calls would have exceeded the old default cap of 100; with no limits
  1056. // provided, there is no cap and no timeout - budgets are host policy.
  1057. const counter = Tool.make({
  1058. description: "Count invocations",
  1059. input: Schema.Struct({}),
  1060. output: Schema.Number,
  1061. execute: () => Effect.succeed(1),
  1062. })
  1063. const result = await Effect.runPromise(
  1064. CodeMode.execute({
  1065. tools: { host: { count: counter } },
  1066. code: `
  1067. let total = 0
  1068. for (let i = 0; i < 150; i += 1) total += await tools.host.count({})
  1069. return total
  1070. `,
  1071. }),
  1072. )
  1073. expect(result).toMatchObject({ ok: true, value: 150 })
  1074. if (result.ok) expect(result.toolCalls.length).toBe(150)
  1075. })
  1076. test("the timeout interrupts a busy loop without any operation budget", async () => {
  1077. // Regression: timeout interruption must not depend on interpreter-side work accounting.
  1078. // The Effect fiber runtime auto-yields between interpreter steps, so a pure `while
  1079. // (true) {}` loop is interrupted by `timeoutMs` alone.
  1080. const startedAt = Date.now()
  1081. const result = await Effect.runPromise(CodeMode.execute({ code: "while (true) {}", limits: { timeoutMs: 200 } }))
  1082. const elapsedMs = Date.now() - startedAt
  1083. expect(result.ok).toBe(false)
  1084. if (!result.ok) {
  1085. expect(result.error.kind).toBe("TimeoutExceeded")
  1086. expect(result.error.message).toContain("timed out after 200ms")
  1087. }
  1088. expect(elapsedMs).toBeLessThan(3_000)
  1089. })
  1090. })