@yolk-sdk/codemode 0.1.0-canary.100

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/dist/catalog.d.mts +74 -0
  4. package/dist/catalog.d.mts.map +1 -0
  5. package/dist/catalog.mjs +164 -0
  6. package/dist/catalog.mjs.map +1 -0
  7. package/dist/classifier-tool.d.mts +78 -0
  8. package/dist/classifier-tool.d.mts.map +1 -0
  9. package/dist/classifier-tool.mjs +125 -0
  10. package/dist/classifier-tool.mjs.map +1 -0
  11. package/dist/executor.d.mts +108 -0
  12. package/dist/executor.d.mts.map +1 -0
  13. package/dist/executor.mjs +1 -0
  14. package/dist/index.d.mts +8 -0
  15. package/dist/index.mjs +7 -0
  16. package/dist/node.d.mts +35 -0
  17. package/dist/node.d.mts.map +1 -0
  18. package/dist/node.mjs +393 -0
  19. package/dist/node.mjs.map +1 -0
  20. package/dist/output.d.mts +42 -0
  21. package/dist/output.d.mts.map +1 -0
  22. package/dist/output.mjs +184 -0
  23. package/dist/output.mjs.map +1 -0
  24. package/dist/search.d.mts +23 -0
  25. package/dist/search.d.mts.map +1 -0
  26. package/dist/search.mjs +58 -0
  27. package/dist/search.mjs.map +1 -0
  28. package/dist/store.d.mts +77 -0
  29. package/dist/store.d.mts.map +1 -0
  30. package/dist/store.mjs +61 -0
  31. package/dist/store.mjs.map +1 -0
  32. package/dist/tool.d.mts +103 -0
  33. package/dist/tool.d.mts.map +1 -0
  34. package/dist/tool.mjs +392 -0
  35. package/dist/tool.mjs.map +1 -0
  36. package/package.json +63 -0
  37. package/src/catalog.ts +305 -0
  38. package/src/classifier-tool.ts +227 -0
  39. package/src/executor.ts +122 -0
  40. package/src/index.ts +87 -0
  41. package/src/node.ts +546 -0
  42. package/src/output.ts +287 -0
  43. package/src/search.ts +113 -0
  44. package/src/store.ts +135 -0
  45. package/src/tool.ts +766 -0
package/src/tool.ts ADDED
@@ -0,0 +1,766 @@
1
+ import {
2
+ Cause,
3
+ Clock,
4
+ Data,
5
+ Duration,
6
+ Effect,
7
+ Exit,
8
+ Fiber,
9
+ FiberSet,
10
+ Option,
11
+ Predicate,
12
+ Scope
13
+ } from 'effect'
14
+ import * as Schema from 'effect/Schema'
15
+ import { ToolError } from '@yolk-sdk/agent/loop'
16
+ import {
17
+ boundNestedToolCallArgs,
18
+ makeNestedToolCallRecorder,
19
+ nestedToolCallMaxErrorChars,
20
+ nestedToolCallRecordLimit,
21
+ nestedToolCallResultFields,
22
+ recordNestedToolCall,
23
+ truncateCodePoints,
24
+ contentPartText,
25
+ ToolCall,
26
+ ToolResult,
27
+ type AgentUsage,
28
+ type Content,
29
+ type NestedToolCallInput,
30
+ type NestedToolCallStatus
31
+ } from '@yolk-sdk/agent/protocol'
32
+ import {
33
+ makeTool,
34
+ type NestedToolExecutor,
35
+ type ToolExecutionInput,
36
+ type ToolLedgerAbandonedInput,
37
+ type ToolLedgerEntry,
38
+ type ToolRegistration
39
+ } from '@yolk-sdk/agent/tools'
40
+ import {
41
+ codeModeCatalog,
42
+ defaultCodeModeInlineBudget,
43
+ describeCodeModeTool,
44
+ findCodeModeTool,
45
+ renderCodeModeDescription,
46
+ type CodeModeCatalogTool
47
+ } from './catalog.ts'
48
+ import type {
49
+ CodeModeExecutionResult,
50
+ CodeModeExecutor,
51
+ CodeModeExecutorGlobal,
52
+ CodeModeExecutorTool,
53
+ CodeModeStore
54
+ } from './executor.ts'
55
+ import {
56
+ boundCodeModeSegments,
57
+ codeModeResultSegments,
58
+ codeModeSegmentsContent,
59
+ defaultCodeModeMaxImageBytes,
60
+ defaultCodeModeMaxImages
61
+ } from './output.ts'
62
+ import { searchCodeModeTools } from './search.ts'
63
+ import type {
64
+ CodeModeInterruptedCall,
65
+ CodeModeInterruptedCalls,
66
+ CodeModeInterruptedCallStatus,
67
+ CodeModeStructuredContent
68
+ } from './store.ts'
69
+
70
+ /** Default name of the code mode tool. */
71
+ export const codeModeToolName = 'codemode'
72
+
73
+ /** Host-owned limits of one script. */
74
+ export type CodeModeLimits = {
75
+ /** Overall deadline including nested calls. Default 120000 (clamped by `deadline`). */
76
+ readonly timeoutMs?: number
77
+ /** VM heap cap. Default 64 MiB. */
78
+ readonly memoryLimitBytes?: number
79
+ /** Nested tool calls per script; further calls reject with an Error. Default 256. The
80
+ * `nestedCalls` record keeps up to this many calls, at most `nestedToolCallMaxRecordedCalls`
81
+ * (4096) whatever the limit (argument byte budgets still apply).
82
+ */
83
+ readonly maxNestedCalls?: number
84
+ /** Model-visible result characters, cut head and tail with an omission marker. Default 40000. */
85
+ readonly maxOutputChars?: number
86
+ /** Images kept in the result, in order; later images are dropped with a note. Default 8. */
87
+ readonly maxImages?: number
88
+ /** Base64 characters of images kept in the result in total; an image that would exceed it is
89
+ * dropped with a note. Default 4 MiB.
90
+ */
91
+ readonly maxImageBytes?: number
92
+ }
93
+
94
+ export const defaultCodeModeLimits = {
95
+ timeoutMs: 120_000,
96
+ memoryLimitBytes: 64 * 1024 * 1024,
97
+ maxNestedCalls: 256,
98
+ maxOutputChars: 40_000,
99
+ maxImages: defaultCodeModeMaxImages,
100
+ maxImageBytes: defaultCodeModeMaxImageBytes
101
+ } as const satisfies Required<CodeModeLimits>
102
+
103
+ /** Kept between a host deadline and the script deadline, for result handling. */
104
+ export const codeModeDeadlineMarginMs = 5_000
105
+
106
+ /** A deadline never clamps a script below this timeout. */
107
+ export const codeModeMinimumTimeoutMs = 1_000
108
+
109
+ export type MakeCodeModeToolOptions<Context> = {
110
+ readonly executor: CodeModeExecutor
111
+ /** Default `codemode`. */
112
+ readonly name?: string
113
+ /** Estimated tokens (four characters each) for listing nested tools. Default 3000. */
114
+ readonly inlineBudget?: number
115
+ readonly limits?: CodeModeLimits
116
+ /**
117
+ * Epoch milliseconds by which the tool call must end, for example the remaining function budget
118
+ * of a Workflow step. The script timeout is clamped to it minus 5 s, but never below 1 s.
119
+ */
120
+ readonly deadline?: (context: Context) => number | undefined
121
+ /**
122
+ * The store scripts read with `load(key)`, usually rebuilt from the transcript with
123
+ * `codeModeStoreFromToolResults`. Without it every script starts with an empty store.
124
+ */
125
+ readonly loadStore?: (context: Context) => Effect.Effect<CodeModeStore, ToolError>
126
+ /**
127
+ * Runs before each nested call executes, with the nested call (id `<toolCallId>/<seq>`) and the
128
+ * host context. A failure rejects that call in the script with the message and records it as an
129
+ * `error` without executing it. Host decorators around the `ToolExecutor` (outside
130
+ * `ResolvedToolSet.execute`) never see nested calls: put per-call run-authority checks here or in
131
+ * registration-level wrappers.
132
+ */
133
+ readonly beforeNestedCall?: (input: {
134
+ readonly call: ToolCall
135
+ readonly context: Context
136
+ }) => Effect.Effect<void, string>
137
+ /**
138
+ * Runs after each nested call that `beforeNestedCall` admitted, with its outcome: `success` (a
139
+ * result without `isError`), `failure` (an error result or an unexpected failure), or
140
+ * `interrupted` (cancelled, for example still running when the script ended). `durationMs`
141
+ * covers the execution; `result` is present when the call produced one. Calls rejected by
142
+ * `beforeNestedCall` or the call limit never ran and are not reported. The script waits for the
143
+ * hook before it sees the call settle. A failing hook (a failure, a defect, or a synchronous
144
+ * throw) is logged and never changes the call's result.
145
+ */
146
+ readonly afterNestedCall?: (input: CodeModeAfterNestedCallInput<Context>) => Effect.Effect<void>
147
+ }
148
+
149
+ /** Outcome of a nested call for `afterNestedCall`. */
150
+ export type CodeModeNestedCallOutcome = 'success' | 'failure' | 'interrupted'
151
+
152
+ export type CodeModeAfterNestedCallInput<Context> = {
153
+ readonly call: ToolCall
154
+ readonly outcome: CodeModeNestedCallOutcome
155
+ readonly durationMs: number
156
+ readonly context: Context
157
+ readonly result?: ToolResult
158
+ }
159
+
160
+ const CodeModeParams = Schema.Struct({
161
+ code: Schema.String.annotate({
162
+ description:
163
+ 'Raw JavaScript: the body of an async function. Top-level await and return work. Not a function declaration and not a module.'
164
+ })
165
+ })
166
+
167
+ type CodeModeParams = typeof CodeModeParams.Type
168
+
169
+ class CodeModeExecutorRejected extends Data.TaggedError('CodeModeExecutorRejected')<{
170
+ readonly message: string
171
+ }> {}
172
+
173
+ const errorMessage = (error: unknown) =>
174
+ error instanceof Error ? error.message : Predicate.isString(error) ? error : 'unknown error'
175
+
176
+ const nestedText = (content: Content) =>
177
+ Predicate.isString(content)
178
+ ? content
179
+ : content
180
+ .map(contentPartText)
181
+ .filter(text => text.length > 0)
182
+ .join('\n')
183
+
184
+ type NestedResolution =
185
+ | { readonly ok: true; readonly value: unknown }
186
+ | { readonly ok: false; readonly message: string }
187
+
188
+ /** What a nested call resolves to: `structuredContent` for tools with an output schema (when
189
+ * present), text content otherwise; error results reject with their text. A tool that declares an
190
+ * output schema but returns no `structuredContent` resolves to its text: that is a tool bug, kept
191
+ * readable rather than turned into a failure.
192
+ */
193
+ const resolveNestedResult = (tool: CodeModeCatalogTool, result: ToolResult): NestedResolution => {
194
+ const text = nestedText(result.content)
195
+
196
+ if (result.isError === true) {
197
+ return { ok: false, message: text.length > 0 ? text : `Tool ${tool.name} failed.` }
198
+ }
199
+
200
+ return tool.structured && result.structuredContent !== undefined
201
+ ? { ok: true, value: result.structuredContent }
202
+ : { ok: true, value: text }
203
+ }
204
+
205
+ type CallOutcome = {
206
+ status: NestedToolCallStatus
207
+ durationMs: number
208
+ error?: string
209
+ usage?: AgentUsage
210
+ }
211
+
212
+ type CallRecord = {
213
+ readonly name: string
214
+ readonly args: unknown
215
+ readonly id: string
216
+ outcome?: CallOutcome
217
+ }
218
+
219
+ const recordedOutcome = (exit: Exit.Exit<ToolResult>, durationMs: number): CallOutcome => {
220
+ if (Exit.isFailure(exit)) {
221
+ return Cause.hasInterruptsOnly(exit.cause)
222
+ ? { status: 'cancelled', durationMs }
223
+ : { status: 'error', durationMs, error: 'The tool call failed unexpectedly.' }
224
+ }
225
+
226
+ const result = exit.value
227
+
228
+ const outcome: CallOutcome =
229
+ result.isError === true
230
+ ? { status: 'error', durationMs, error: nestedText(result.content) }
231
+ : { status: 'ok', durationMs }
232
+
233
+ if (result.usage !== undefined) {
234
+ outcome.usage = result.usage
235
+ }
236
+
237
+ return outcome
238
+ }
239
+
240
+ const hookOutcome = (exit: Exit.Exit<ToolResult>): CodeModeNestedCallOutcome =>
241
+ Exit.isSuccess(exit)
242
+ ? exit.value.isError === true
243
+ ? 'failure'
244
+ : 'success'
245
+ : Cause.hasInterruptsOnly(exit.cause)
246
+ ? 'interrupted'
247
+ : 'failure'
248
+
249
+ /** What `describeNamespace(name)` resolves to. */
250
+ type CodeModeNamespaceDescription = {
251
+ readonly name: string
252
+ description?: string
253
+ readonly tools: ReadonlyArray<{ readonly name: string; readonly description: string }>
254
+ }
255
+
256
+ const SearchOptions = Schema.Struct({
257
+ limit: Schema.optionalKey(Schema.Finite),
258
+ namespace: Schema.optionalKey(Schema.String)
259
+ })
260
+
261
+ const decodeSearchOptions = Schema.decodeUnknownOption(SearchOptions)
262
+
263
+ /** `searchTools`, `describeTool`, and `describeNamespace` over every nested tool. */
264
+ const discoveryGlobals = (
265
+ catalog: ReadonlyArray<CodeModeCatalogTool>
266
+ ): ReadonlyArray<CodeModeExecutorGlobal> => [
267
+ {
268
+ name: 'searchTools',
269
+ spread: true,
270
+ execute: args => {
271
+ const [query, options] = Array.isArray(args) ? args : []
272
+
273
+ return Promise.resolve(
274
+ searchCodeModeTools(
275
+ catalog,
276
+ Predicate.isString(query) ? query : '',
277
+ Option.getOrElse(decodeSearchOptions(options ?? {}), () => ({}))
278
+ )
279
+ )
280
+ }
281
+ },
282
+ {
283
+ name: 'describeTool',
284
+ execute: name => {
285
+ const tool = Predicate.isString(name) ? findCodeModeTool(catalog, name) : undefined
286
+
287
+ return Promise.resolve(tool === undefined ? undefined : describeCodeModeTool(tool))
288
+ }
289
+ },
290
+ {
291
+ name: 'describeNamespace',
292
+ execute: name => {
293
+ const tools = catalog.filter(tool => tool.namespace === name)
294
+
295
+ if (!Predicate.isString(name) || tools.length === 0) return Promise.resolve(undefined)
296
+
297
+ const description = tools.find(
298
+ tool => tool.namespaceDescription !== undefined
299
+ )?.namespaceDescription
300
+
301
+ const namespace: CodeModeNamespaceDescription = {
302
+ name,
303
+ tools: tools.map(tool => ({ name: tool.identifier, description: tool.description }))
304
+ }
305
+
306
+ if (description !== undefined) namespace.description = description
307
+
308
+ return Promise.resolve(namespace)
309
+ }
310
+ }
311
+ ]
312
+
313
+ const hasStoreWrites = (result: CodeModeExecutionResult) =>
314
+ result.storeWrites !== undefined &&
315
+ (Object.keys(result.storeWrites.set).length > 0 || result.storeWrites.delete.length > 0)
316
+
317
+ const timeoutFor = (limit: number, deadline: number | undefined, now: number) =>
318
+ deadline === undefined
319
+ ? limit
320
+ : Math.max(codeModeMinimumTimeoutMs, Math.min(limit, deadline - now - codeModeDeadlineMarginMs))
321
+
322
+ /**
323
+ * Waits at most `codeModeDeadlineMarginMs` for an effect (interrupting nested fibers) to finish; it
324
+ * keeps running detached when a nested call does not respond to interruption in time.
325
+ */
326
+ const boundedWait = (effect: Effect.Effect<void>) =>
327
+ Effect.forkDetach(effect).pipe(
328
+ Effect.flatMap(fiber =>
329
+ Fiber.await(fiber).pipe(
330
+ Effect.timeoutOption(Duration.millis(codeModeDeadlineMarginMs)),
331
+ Effect.interruptible
332
+ )
333
+ ),
334
+ Effect.asVoid
335
+ )
336
+
337
+ type RunInput<Context> = {
338
+ readonly options: MakeCodeModeToolOptions<Context>
339
+ readonly limits: Required<CodeModeLimits>
340
+ readonly call: ToolCall
341
+ readonly context: Context
342
+ readonly code: string
343
+ readonly nested: NestedToolExecutor
344
+ }
345
+
346
+ const runScript = <Context>(input: RunInput<Context>): Effect.Effect<ToolResult, ToolError> =>
347
+ Effect.gen(function* () {
348
+ const { call, limits, nested, options } = input
349
+ const store = options.loadStore === undefined ? {} : yield* options.loadStore(input.context)
350
+ const startedAt = yield* Clock.currentTimeMillis
351
+ const timeoutMs = timeoutFor(limits.timeoutMs, options.deadline?.(input.context), startedAt)
352
+ const catalog = codeModeCatalog(nested.tools)
353
+
354
+ return yield* Effect.acquireUseRelease(
355
+ Scope.make(),
356
+ scope =>
357
+ Effect.gen(function* () {
358
+ // Nested calls run as fibers of this tool call (same services, interrupted with it).
359
+ const fibers = yield* FiberSet.make<ToolResult>().pipe(Scope.provide(scope))
360
+ const runFork = yield* FiberSet.runtime(fibers)<never>()
361
+ const records: Array<CallRecord> = []
362
+
363
+ const callTool =
364
+ (tool: CodeModeCatalogTool): CodeModeExecutorTool['execute'] =>
365
+ (args, { signal }) => {
366
+ if (signal.aborted) {
367
+ return Promise.reject(new Error(`${tool.callLabel} was cancelled.`))
368
+ }
369
+
370
+ if (records.length >= limits.maxNestedCalls) {
371
+ return Promise.reject(
372
+ new Error(
373
+ `${tool.callLabel}: Nested call limit reached: a script may make at most ${limits.maxNestedCalls} tool calls. Batch the work or return partial results.`
374
+ )
375
+ )
376
+ }
377
+
378
+ const params = args === undefined ? {} : args
379
+
380
+ const record: CallRecord = {
381
+ id: `${call.id}/${records.length + 1}`,
382
+ name: tool.name,
383
+ args: params
384
+ }
385
+
386
+ records.push(record)
387
+
388
+ const nestedCall = ToolCall.make({ id: record.id, name: tool.name, params })
389
+ const afterNestedCall = options.afterNestedCall
390
+
391
+ const run =
392
+ afterNestedCall === undefined
393
+ ? nested.execute(nestedCall)
394
+ : Effect.gen(function* () {
395
+ const started = yield* Clock.currentTimeMillis
396
+
397
+ // Host observability never changes the call: a hook failure, defects and
398
+ // synchronous throws included, is only logged.
399
+ const afterNestedCallSafely = (
400
+ hookInput: CodeModeAfterNestedCallInput<Context>
401
+ ) =>
402
+ Effect.suspend(() => afterNestedCall(hookInput)).pipe(
403
+ Effect.exit,
404
+ Effect.flatMap(hookExit =>
405
+ Exit.isSuccess(hookExit)
406
+ ? Effect.void
407
+ : Effect.logWarning(
408
+ `Code mode afterNestedCall failed for ${nestedCall.id}; ignored: ${Cause.pretty(hookExit.cause)}`
409
+ )
410
+ )
411
+ )
412
+
413
+ return yield* nested.execute(nestedCall).pipe(
414
+ Effect.onExit(exit =>
415
+ Effect.flatMap(Clock.currentTimeMillis, finished =>
416
+ afterNestedCallSafely(
417
+ Exit.isSuccess(exit)
418
+ ? {
419
+ call: nestedCall,
420
+ outcome: hookOutcome(exit),
421
+ durationMs: finished - started,
422
+ context: input.context,
423
+ result: exit.value
424
+ }
425
+ : {
426
+ call: nestedCall,
427
+ outcome: hookOutcome(exit),
428
+ durationMs: finished - started,
429
+ context: input.context
430
+ }
431
+ )
432
+ )
433
+ )
434
+ )
435
+ })
436
+
437
+ const admitted =
438
+ options.beforeNestedCall === undefined
439
+ ? run
440
+ : options.beforeNestedCall({ call: nestedCall, context: input.context }).pipe(
441
+ Effect.matchEffect({
442
+ onFailure: message =>
443
+ Effect.succeed(
444
+ ToolResult.make({
445
+ toolCallId: nestedCall.id,
446
+ content: message,
447
+ isError: true
448
+ })
449
+ ),
450
+ onSuccess: () => run
451
+ })
452
+ )
453
+
454
+ const execution = Effect.gen(function* () {
455
+ const started = yield* Clock.currentTimeMillis
456
+
457
+ return yield* admitted.pipe(
458
+ Effect.onExit(exit =>
459
+ Effect.map(Clock.currentTimeMillis, finished => {
460
+ record.outcome = recordedOutcome(exit, finished - started)
461
+ })
462
+ )
463
+ )
464
+ })
465
+
466
+ return new Promise((resolve, reject) => {
467
+ runFork(execution, { signal }).addObserver(exit => {
468
+ if (Exit.isFailure(exit)) {
469
+ reject(
470
+ new Error(
471
+ Cause.hasInterruptsOnly(exit.cause)
472
+ ? `${tool.callLabel} was cancelled.`
473
+ : `${tool.callLabel} failed unexpectedly.`
474
+ )
475
+ )
476
+
477
+ return
478
+ }
479
+
480
+ const resolution = resolveNestedResult(tool, exit.value)
481
+
482
+ if (resolution.ok) {
483
+ resolve(resolution.value)
484
+ } else {
485
+ // The script sees which call failed; the nested-call record keeps the raw text.
486
+ reject(new Error(`${tool.callLabel}: ${resolution.message}`))
487
+ }
488
+ })
489
+ })
490
+ }
491
+
492
+ const tools: ReadonlyArray<CodeModeExecutorTool> = catalog.map(tool => ({
493
+ name: tool.name,
494
+ description: tool.description,
495
+ inputSchema: tool.inputSchema,
496
+ outputSchema: tool.outputSchema,
497
+ callLabel: tool.callLabel,
498
+ execute: callTool(tool)
499
+ }))
500
+
501
+ const result = yield* Effect.tryPromise({
502
+ try: signal =>
503
+ options.executor.execute(input.code, {
504
+ tools,
505
+ globals: discoveryGlobals(catalog),
506
+ timeoutMs,
507
+ memoryLimitBytes: limits.memoryLimitBytes,
508
+ store,
509
+ signal
510
+ }),
511
+ catch: error => new CodeModeExecutorRejected({ message: errorMessage(error) })
512
+ }).pipe(
513
+ Effect.catch(error =>
514
+ Effect.succeed<CodeModeExecutionResult>({
515
+ ok: false,
516
+ error: {
517
+ kind: 'sandbox',
518
+ message: `The code mode executor failed: ${error.message}`
519
+ },
520
+ output: []
521
+ })
522
+ ),
523
+ // Backstop for executors that miss their own deadline: interrupting aborts the signal.
524
+ Effect.timeoutOption(Duration.millis(timeoutMs + codeModeDeadlineMarginMs)),
525
+ Effect.map(
526
+ Option.getOrElse((): CodeModeExecutionResult => ({
527
+ ok: false,
528
+ error: {
529
+ kind: 'timeout',
530
+ message: `Execution timed out after ${timeoutMs} ms (the executor did not stop in time)`
531
+ },
532
+ output: []
533
+ }))
534
+ )
535
+ )
536
+
537
+ // Calls still running when the script ended are cancelled and recorded as such.
538
+ yield* boundedWait(FiberSet.clear(fibers))
539
+
540
+ const finishedAt = yield* Clock.currentTimeMillis
541
+
542
+ const recorded = records.map((record): NestedToolCallInput => ({
543
+ id: record.id,
544
+ name: record.name,
545
+ args: record.args,
546
+ // A call interrupted before it started has no outcome.
547
+ ...(record.outcome ?? { status: 'cancelled' })
548
+ }))
549
+
550
+ const recorder = recorded.reduce(
551
+ recordNestedToolCall,
552
+ makeNestedToolCallRecorder({ maxCalls: limits.maxNestedCalls })
553
+ )
554
+
555
+ const { nestedCalls, usage } = nestedToolCallResultFields(recorder)
556
+
557
+ const content = codeModeSegmentsContent(
558
+ codeModeResultSegments({
559
+ result,
560
+ wallTimeMs: finishedAt - startedAt,
561
+ calls: recorded,
562
+ maxChars: limits.maxOutputChars,
563
+ maxImages: limits.maxImages,
564
+ maxImageBytes: limits.maxImageBytes
565
+ })
566
+ )
567
+
568
+ const structuredContent: CodeModeStructuredContent = {
569
+ codemode:
570
+ result.ok && result.storeWrites !== undefined && hasStoreWrites(result)
571
+ ? { ok: true, storeWrites: result.storeWrites }
572
+ : { ok: result.ok }
573
+ }
574
+
575
+ type ResultFields = {
576
+ toolCallId: string
577
+ content: Content
578
+ isError?: boolean
579
+ structuredContent: CodeModeStructuredContent
580
+ nestedCalls: typeof nestedCalls
581
+ usage?: AgentUsage
582
+ }
583
+
584
+ const fields: ResultFields = {
585
+ toolCallId: call.id,
586
+ content,
587
+ structuredContent,
588
+ nestedCalls
589
+ }
590
+
591
+ if (!result.ok) {
592
+ fields.isError = true
593
+ }
594
+
595
+ if (usage !== undefined) {
596
+ fields.usage = usage
597
+ }
598
+
599
+ return ToolResult.make(fields)
600
+ }),
601
+ (scope, exit) => boundedWait(Scope.close(scope, exit))
602
+ )
603
+ })
604
+
605
+ const argsPreviewChars = 200
606
+
607
+ const interruptedCallStatus = (entry: ToolLedgerEntry): CodeModeInterruptedCallStatus => {
608
+ const outcome = entry.outcome
609
+
610
+ if (outcome === undefined) return 'unknown'
611
+
612
+ if (Predicate.isTagged(outcome, 'Failed')) return 'failed'
613
+
614
+ return outcome.result.isError === true ? 'failed' : 'applied'
615
+ }
616
+
617
+ const nestedEntryState = (entry: ToolLedgerEntry) => {
618
+ const outcome = entry.outcome
619
+
620
+ if (outcome === undefined) {
621
+ return 'unknown: it started but never recorded a result, so it may have been applied'
622
+ }
623
+
624
+ if (Predicate.isTagged(outcome, 'Failed'))
625
+ return `completed with an error: ${truncateCodePoints(outcome.error.message, nestedToolCallMaxErrorChars)}`
626
+
627
+ return outcome.result.isError === true ? 'completed with an error result' : 'applied'
628
+ }
629
+
630
+ /** The bounded, wire-safe `interruptedCalls` of an abandoned execution's nested ledger entries:
631
+ * the `nestedCalls` record bounds (`maxCalls`, 8 KiB of arguments per call, 32 KiB in total).
632
+ */
633
+ const interruptedCalls = (
634
+ nested: ReadonlyArray<ToolLedgerEntry>,
635
+ maxCalls: number
636
+ ): CodeModeInterruptedCalls => {
637
+ const counts = { applied: 0, failed: 0, unknown: 0 }
638
+ const calls: Array<CodeModeInterruptedCall> = []
639
+ let argsBytes = 0
640
+ let complete = true
641
+
642
+ for (const entry of nested) {
643
+ const status = interruptedCallStatus(entry)
644
+
645
+ counts[status]++
646
+
647
+ if (calls.length >= maxCalls) {
648
+ complete = false
649
+ continue
650
+ }
651
+
652
+ const { args, bytes, truncated } = boundNestedToolCallArgs(entry.args, argsBytes)
653
+
654
+ // `argsTruncated`: the ledger already cut the arguments when the call was claimed.
655
+ if (truncated || entry.argsTruncated) complete = false
656
+
657
+ argsBytes += bytes
658
+ calls.push({ key: entry.key, toolName: entry.toolName, args, status })
659
+ }
660
+
661
+ return { calls, complete, counts }
662
+ }
663
+
664
+ /**
665
+ * The result of a code mode call whose earlier execution was abandoned (claimed, lease expired,
666
+ * no result recorded). The script never runs again: the result lists that execution's ledgered
667
+ * nested calls as applied, failed, or unknown, and says they were not undone. The text is bounded
668
+ * by `maxOutputChars`; `structuredContent.codemode.interruptedCalls` carries the same entries for
669
+ * hosts, bounded like `nestedCalls`. When the ledger cannot list them, the result stays
670
+ * interrupted, says they may have been applied, and sets `interruptedCallsUnavailable` instead.
671
+ */
672
+ const abandonedScriptResult =
673
+ (limits: Required<CodeModeLimits>) =>
674
+ ({ call, nested }: ToolLedgerAbandonedInput): ToolResult => {
675
+ const maxChars = limits.maxOutputChars
676
+
677
+ const listing =
678
+ nested === undefined
679
+ ? 'Its ledgered nested tool calls could not be listed (the tool ledger is unavailable), so any of them may already have been applied.'
680
+ : nested.length === 0
681
+ ? 'No ledgered nested tool calls were recorded for that execution.'
682
+ : [
683
+ `Ledgered nested tool calls of that execution (they were not undone):`,
684
+ ...nested.map(
685
+ entry =>
686
+ `- ${entry.key} ${entry.toolName} ${truncateCodePoints(entry.args, argsPreviewChars)}: ${nestedEntryState(entry)}`
687
+ )
688
+ ].join('\n')
689
+
690
+ const text = [
691
+ `Script interrupted: an earlier execution of this ${call.name} call (${call.id}) started but never recorded a result. The script was not run again.`,
692
+ listing,
693
+ 'Calls the ledger policy skips (by default read-only calls) are not listed. Verify the state these calls affect before repeating any work.'
694
+ ].join('\n\n')
695
+
696
+ const structuredContent: CodeModeStructuredContent = {
697
+ codemode:
698
+ nested === undefined
699
+ ? { ok: false, interrupted: true, interruptedCallsUnavailable: true }
700
+ : {
701
+ ok: false,
702
+ interrupted: true,
703
+ interruptedCalls: interruptedCalls(
704
+ nested,
705
+ nestedToolCallRecordLimit(limits.maxNestedCalls)
706
+ )
707
+ }
708
+ }
709
+
710
+ return ToolResult.make({
711
+ toolCallId: call.id,
712
+ content: codeModeSegmentsContent(boundCodeModeSegments([{ type: 'text', text }], maxChars)),
713
+ isError: true,
714
+ structuredContent
715
+ })
716
+ }
717
+
718
+ /**
719
+ * The code mode tool: one registration (default name `codemode`, input `{ code }`) whose scripts
720
+ * call the other code-mode-callable tools of the same `resolveTools` resolution. Nested calls run
721
+ * through the resolved execute path with the same host context (input decoding, enablement,
722
+ * registration wrappers), get ids `<toolCallId>/<seq>`, and are recorded on the result's
723
+ * `nestedCalls`. The resolved description lists the callable tools (see
724
+ * `renderCodeModeDescription`).
725
+ *
726
+ * Access is `write`: scripts can call any write tool the resolution exposes to code mode; each
727
+ * nested call keeps its own access metadata and host wrappers.
728
+ *
729
+ * With a tool ledger (`resolveTools(..., { ledger })`) the code mode call itself is ledgered, so a
730
+ * re-executed call never runs its script again: a completed call returns its stored result, an
731
+ * in-flight one is awaited, and an abandoned one returns an interrupted result listing its
732
+ * ledgered nested calls. Nested write calls are ledgered under `<toolCallId>/<seq>`.
733
+ */
734
+ export const makeCodeModeTool = <Context>(
735
+ options: MakeCodeModeToolOptions<Context>
736
+ ): ToolRegistration<Context> => {
737
+ const name = options.name ?? codeModeToolName
738
+ const limits: Required<CodeModeLimits> = { ...defaultCodeModeLimits, ...options.limits }
739
+ const inlineBudget = options.inlineBudget ?? defaultCodeModeInlineBudget
740
+ const store = options.loadStore !== undefined
741
+
742
+ return makeTool<Context, typeof CodeModeParams>({
743
+ name,
744
+ description: renderCodeModeDescription({ tools: [], inlineBudget, store }),
745
+ parameters: CodeModeParams,
746
+ access: 'write',
747
+ nestedToolAccess: true,
748
+ describe: ({ tools }) => renderCodeModeDescription({ tools, inlineBudget, store }),
749
+ abandonedResult: abandonedScriptResult(limits),
750
+ execute: ({
751
+ call,
752
+ context,
753
+ params,
754
+ nested
755
+ }: ToolExecutionInput<Context> & { readonly params: CodeModeParams }) =>
756
+ nested === undefined
757
+ ? Effect.fail(
758
+ new ToolError({
759
+ tool: name,
760
+ cause: 'unavailable',
761
+ message: `${name} requires nested tool access; resolve it with resolveTools.`
762
+ })
763
+ )
764
+ : runScript({ options, limits, call, context, code: params.code, nested })
765
+ })
766
+ }