@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.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/catalog.d.mts +74 -0
- package/dist/catalog.d.mts.map +1 -0
- package/dist/catalog.mjs +164 -0
- package/dist/catalog.mjs.map +1 -0
- package/dist/classifier-tool.d.mts +78 -0
- package/dist/classifier-tool.d.mts.map +1 -0
- package/dist/classifier-tool.mjs +125 -0
- package/dist/classifier-tool.mjs.map +1 -0
- package/dist/executor.d.mts +108 -0
- package/dist/executor.d.mts.map +1 -0
- package/dist/executor.mjs +1 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +7 -0
- package/dist/node.d.mts +35 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +393 -0
- package/dist/node.mjs.map +1 -0
- package/dist/output.d.mts +42 -0
- package/dist/output.d.mts.map +1 -0
- package/dist/output.mjs +184 -0
- package/dist/output.mjs.map +1 -0
- package/dist/search.d.mts +23 -0
- package/dist/search.d.mts.map +1 -0
- package/dist/search.mjs +58 -0
- package/dist/search.mjs.map +1 -0
- package/dist/store.d.mts +77 -0
- package/dist/store.d.mts.map +1 -0
- package/dist/store.mjs +61 -0
- package/dist/store.mjs.map +1 -0
- package/dist/tool.d.mts +103 -0
- package/dist/tool.d.mts.map +1 -0
- package/dist/tool.mjs +392 -0
- package/dist/tool.mjs.map +1 -0
- package/package.json +63 -0
- package/src/catalog.ts +305 -0
- package/src/classifier-tool.ts +227 -0
- package/src/executor.ts +122 -0
- package/src/index.ts +87 -0
- package/src/node.ts +546 -0
- package/src/output.ts +287 -0
- package/src/search.ts +113 -0
- package/src/store.ts +135 -0
- 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
|
+
}
|