@yolk-sdk/codemode 0.1.0-canary.124 → 0.1.0-canary.126
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/README.md +14 -8
- package/dist/catalog.d.mts.map +1 -1
- package/dist/catalog.mjs +55 -8
- package/dist/catalog.mjs.map +1 -1
- package/dist/executor.d.mts +3 -2
- package/dist/executor.d.mts.map +1 -1
- package/dist/node.d.mts.map +1 -1
- package/dist/node.mjs +48 -6
- package/dist/node.mjs.map +1 -1
- package/dist/tool.d.mts.map +1 -1
- package/dist/tool.mjs +38 -34
- package/dist/tool.mjs.map +1 -1
- package/package.json +2 -2
- package/src/catalog.ts +102 -8
- package/src/executor.ts +3 -2
- package/src/node.ts +66 -8
- package/src/tool.ts +46 -26
package/src/node.ts
CHANGED
|
@@ -121,7 +121,7 @@ const maxArgumentDepth = 64
|
|
|
121
121
|
* proxy (close-match suggestions). Not a security boundary (`globalThis.tools` is unguarded):
|
|
122
122
|
* arguments are still decoded by the host.
|
|
123
123
|
*/
|
|
124
|
-
const toolsGuardSource = `(function (tools, labels, unbound) {
|
|
124
|
+
const toolsGuardSource = `(function (tools, labels, unbound, rejectionPrefix) {
|
|
125
125
|
// Captured before the script runs, so a script that changes built-ins cannot change the checks.
|
|
126
126
|
const { apply, getPrototypeOf, setPrototypeOf } = Reflect
|
|
127
127
|
const isFinite = Number.isFinite
|
|
@@ -136,6 +136,10 @@ const toolsGuardSource = `(function (tools, labels, unbound) {
|
|
|
136
136
|
const ObjectProto = Object.prototype
|
|
137
137
|
const { trunc, min } = Math
|
|
138
138
|
const stringify = JSON.stringify
|
|
139
|
+
const parse = JSON.parse
|
|
140
|
+
const ErrorCtor = Error
|
|
141
|
+
const startsWith = String.prototype.startsWith
|
|
142
|
+
const sliceString = String.prototype.slice
|
|
139
143
|
const plainJson = 'pass plain JSON (objects, arrays, strings, finite numbers, booleans, null)'
|
|
140
144
|
const maxValues = ${maxArgumentValues}
|
|
141
145
|
const maxDepth = ${maxArgumentDepth}
|
|
@@ -236,6 +240,24 @@ const toolsGuardSource = `(function (tools, labels, unbound) {
|
|
|
236
240
|
apply(setDelete, walk.ancestors, [value])
|
|
237
241
|
return copy
|
|
238
242
|
}
|
|
243
|
+
// The host sends every rejection of a tool call as the prefix and the JSON of { message, code? }
|
|
244
|
+
// (pi only carries a message): rebuild the Error with its code.
|
|
245
|
+
const restored = error => {
|
|
246
|
+
const message = error instanceof ErrorCtor ? error.message : undefined
|
|
247
|
+
if (typeof message !== 'string' || !apply(startsWith, message, [rejectionPrefix])) return error
|
|
248
|
+
let payload
|
|
249
|
+
try {
|
|
250
|
+
payload = parse(apply(sliceString, message, [rejectionPrefix.length]))
|
|
251
|
+
} catch {
|
|
252
|
+
return error
|
|
253
|
+
}
|
|
254
|
+
if (payload === null || typeof payload !== 'object') return error
|
|
255
|
+
const rebuilt = new ErrorCtor(typeof payload.message === 'string' ? payload.message : '')
|
|
256
|
+
if (typeof payload.code === 'string') {
|
|
257
|
+
defineProperty(rebuilt, 'code', { value: payload.code, writable: true, configurable: true })
|
|
258
|
+
}
|
|
259
|
+
return rebuilt
|
|
260
|
+
}
|
|
239
261
|
const wrap = (label, call) => async (...args) => {
|
|
240
262
|
let json
|
|
241
263
|
try {
|
|
@@ -244,7 +266,11 @@ const toolsGuardSource = `(function (tools, labels, unbound) {
|
|
|
244
266
|
if (error instanceof Rejected) throw new TypeErrorCtor(label + ': argument ' + error.message)
|
|
245
267
|
throw error
|
|
246
268
|
}
|
|
247
|
-
|
|
269
|
+
try {
|
|
270
|
+
return await call(json)
|
|
271
|
+
} catch (error) {
|
|
272
|
+
throw restored(error)
|
|
273
|
+
}
|
|
248
274
|
}
|
|
249
275
|
// Each tool's function sits at tools[name] unless an earlier tool holds that key, in which case
|
|
250
276
|
// that earlier function is already wrapped.
|
|
@@ -295,7 +321,7 @@ const guardSuffix = (
|
|
|
295
321
|
|
|
296
322
|
const rejected = (unbound ?? []).map(entry => [entry.key, entry.message])
|
|
297
323
|
|
|
298
|
-
return `\n})(${toolsGuardSource}(tools, ${jsonSource(pairs)}, ${jsonSource(rejected)}), console)`
|
|
324
|
+
return `\n})(${toolsGuardSource}(tools, ${jsonSource(pairs)}, ${jsonSource(rejected)}, ${jsonSource(rejectionPrefix)}), console)`
|
|
299
325
|
}
|
|
300
326
|
|
|
301
327
|
const lineTerminators = /\r\n?|[\n\u2028\u2029]/g
|
|
@@ -373,12 +399,37 @@ const stripTypes = (code: string): Stripped => {
|
|
|
373
399
|
}
|
|
374
400
|
}
|
|
375
401
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
402
|
+
/**
|
|
403
|
+
* pi passes only the message of a rejected tool call into the VM. Every tool rejection crosses as
|
|
404
|
+
* this prefix and the JSON of `{ message, code? }`, which the tools guard turns back into an
|
|
405
|
+
* `Error` with its `code`. Calls through the unguarded `globalThis.tools` see the encoded message.
|
|
406
|
+
*/
|
|
407
|
+
const rejectionPrefix = '\u0000yolk-tool-rejection:'
|
|
408
|
+
|
|
409
|
+
type EncodedRejection = { message: string; code?: string }
|
|
410
|
+
|
|
411
|
+
const RejectionCode = Schema.Struct({ code: Schema.String })
|
|
412
|
+
|
|
413
|
+
const encodedRejection = (error: unknown) => {
|
|
414
|
+
const payload: EncodedRejection = {
|
|
415
|
+
message: error instanceof Error ? error.message : String(error)
|
|
380
416
|
}
|
|
381
417
|
|
|
418
|
+
const code = Option.getOrUndefined(
|
|
419
|
+
Option.map(Schema.decodeUnknownOption(RejectionCode)(error), found => found.code)
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
if (code !== undefined) payload.code = code
|
|
423
|
+
|
|
424
|
+
return new Error(`${rejectionPrefix}${JSON.stringify(payload)}`)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const piCallable = (
|
|
428
|
+
tool: CodeModeExecutorTool | CodeModeExecutorGlobal,
|
|
429
|
+
execute: CodemodeTool['execute']
|
|
430
|
+
): CodemodeTool => {
|
|
431
|
+
const converted: CodemodeTool = { name: tool.name, execute }
|
|
432
|
+
|
|
382
433
|
if (tool.description !== undefined) converted.description = tool.description
|
|
383
434
|
|
|
384
435
|
if (tool.inputSchema !== undefined) converted.inputSchema = tool.inputSchema
|
|
@@ -388,8 +439,15 @@ const piTool = (tool: CodeModeExecutorTool): CodemodeTool => {
|
|
|
388
439
|
return converted
|
|
389
440
|
}
|
|
390
441
|
|
|
442
|
+
const piTool = (tool: CodeModeExecutorTool): CodemodeTool =>
|
|
443
|
+
piCallable(tool, (args, context) =>
|
|
444
|
+
tool.execute(args, context).catch((error: unknown) => {
|
|
445
|
+
throw encodedRejection(error)
|
|
446
|
+
})
|
|
447
|
+
)
|
|
448
|
+
|
|
391
449
|
const piGlobal = (global: CodeModeExecutorGlobal): CodemodeTool => {
|
|
392
|
-
const converted =
|
|
450
|
+
const converted = piCallable(global, (args, context) => global.execute(args, context))
|
|
393
451
|
|
|
394
452
|
if (global.spread !== undefined) converted.spread = global.spread
|
|
395
453
|
|
package/src/tool.ts
CHANGED
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
nestedToolCallResultFields,
|
|
23
23
|
recordNestedToolCall,
|
|
24
24
|
truncateCodePoints,
|
|
25
|
-
contentPartText,
|
|
26
25
|
ToolCall,
|
|
27
26
|
ToolResult,
|
|
28
27
|
type AgentUsage,
|
|
@@ -207,47 +206,61 @@ class CodeModeExecutorRejected extends Data.TaggedError('CodeModeExecutorRejecte
|
|
|
207
206
|
const errorMessage = (error: unknown) =>
|
|
208
207
|
error instanceof Error ? error.message : Predicate.isString(error) ? error : 'unknown error'
|
|
209
208
|
|
|
209
|
+
// The text a script sees of a result: its text parts only. File, image, document and audio parts
|
|
210
|
+
// (and the labels providers render for them) never enter scripts (ADR 0013).
|
|
210
211
|
const nestedText = (content: Content) =>
|
|
211
212
|
Predicate.isString(content)
|
|
212
213
|
? content
|
|
213
214
|
: content
|
|
214
|
-
.
|
|
215
|
+
.flatMap(part => (Predicate.isTagged(part, 'Text') ? [part.text] : []))
|
|
215
216
|
.filter(text => text.length > 0)
|
|
216
217
|
.join('\n')
|
|
217
218
|
|
|
218
219
|
type NestedResolution =
|
|
219
220
|
| { readonly ok: true; readonly value: unknown }
|
|
220
|
-
| { readonly ok: false; readonly message: string }
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
221
|
+
| { readonly ok: false; readonly message: string; readonly code?: string }
|
|
222
|
+
|
|
223
|
+
// The `code` of an error result: a model-visible error's (`ModelVisibleToolError.code`) or any
|
|
224
|
+
// structured error with a string `code` (a connector's `ProviderFailure`).
|
|
225
|
+
const decodeErrorCode = Schema.decodeUnknownOption(Schema.Struct({ code: Schema.String }))
|
|
226
|
+
|
|
227
|
+
const errorCode = (result: ToolResult) =>
|
|
228
|
+
Option.getOrUndefined(
|
|
229
|
+
Option.map(decodeErrorCode(result.structuredContent), envelope => envelope.code)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
/** A typed tool's successful result without its value: the script never falls back to text. */
|
|
233
|
+
const isInvalidTypedResult = (tool: CodeModeCatalogTool, result: ToolResult) =>
|
|
234
|
+
tool.structured && result.isError !== true && result.structuredContent === undefined
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* What a nested call resolves to (ADR 0013). A typed tool (one with an output schema) resolves to
|
|
238
|
+
* its value as declared, file fields as `FileRef` or `FileUnavailable`; a typed result without a
|
|
239
|
+
* value rejects ("returned an invalid result"). An untyped tool resolves to its text parts joined
|
|
240
|
+
* with newlines. Error results reject with their text and the `code` of their structured error
|
|
241
|
+
* (a model-visible error's code, a connector failure's code). Files never reach scripts as labels.
|
|
228
242
|
*/
|
|
229
243
|
const resolveNestedResult = (tool: CodeModeCatalogTool, result: ToolResult): NestedResolution => {
|
|
230
|
-
const text = nestedText(result.content)
|
|
231
|
-
|
|
232
244
|
if (result.isError === true) {
|
|
233
|
-
|
|
245
|
+
const text = nestedText(result.content)
|
|
246
|
+
const message = text.length > 0 ? text : `Tool ${tool.name} failed.`
|
|
247
|
+
const code = errorCode(result)
|
|
248
|
+
|
|
249
|
+
return code === undefined ? { ok: false, message } : { ok: false, message, code }
|
|
234
250
|
}
|
|
235
251
|
|
|
236
|
-
|
|
237
|
-
tool.structured && result.structuredContent !== undefined ? result.structuredContent : text
|
|
252
|
+
if (!tool.structured) return { ok: true, value: nestedText(result.content) }
|
|
238
253
|
|
|
239
|
-
|
|
254
|
+
return result.structuredContent === undefined
|
|
255
|
+
? { ok: false, message: 'returned an invalid result' }
|
|
256
|
+
: { ok: true, value: result.structuredContent }
|
|
257
|
+
}
|
|
240
258
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
mimeType: file.mimeType,
|
|
245
|
-
size: file.size
|
|
246
|
-
}))
|
|
259
|
+
/** The script-visible rejection of a nested call, with the tool's error `code` when it has one. */
|
|
260
|
+
const nestedCallError = (label: string, resolution: { message: string; code?: string }) => {
|
|
261
|
+
const error = new Error(`${label}: ${resolution.message}`)
|
|
247
262
|
|
|
248
|
-
return
|
|
249
|
-
? { ok: true, value: { ...value, files } }
|
|
250
|
-
: { ok: true, value: { text, files } }
|
|
263
|
+
return resolution.code === undefined ? error : Object.assign(error, { code: resolution.code })
|
|
251
264
|
}
|
|
252
265
|
|
|
253
266
|
type CallOutcome = {
|
|
@@ -642,6 +655,13 @@ const runScript = <Context>(input: RunInput<Context>): Effect.Effect<ToolResult,
|
|
|
642
655
|
Effect.map(Clock.currentTimeMillis, finished => {
|
|
643
656
|
record.outcome = recordedOutcome(exit, finished - started)
|
|
644
657
|
})
|
|
658
|
+
),
|
|
659
|
+
Effect.tap(result =>
|
|
660
|
+
isInvalidTypedResult(tool, result)
|
|
661
|
+
? Effect.logWarning(
|
|
662
|
+
`Code mode: ${tool.name} declares an output schema but returned no value (call ${nestedCall.id}).`
|
|
663
|
+
)
|
|
664
|
+
: Effect.void
|
|
645
665
|
)
|
|
646
666
|
)
|
|
647
667
|
})
|
|
@@ -666,7 +686,7 @@ const runScript = <Context>(input: RunInput<Context>): Effect.Effect<ToolResult,
|
|
|
666
686
|
resolve(resolution.value)
|
|
667
687
|
} else {
|
|
668
688
|
// The script sees which call failed; the nested-call record keeps the raw text.
|
|
669
|
-
reject(
|
|
689
|
+
reject(nestedCallError(tool.callLabel, resolution))
|
|
670
690
|
}
|
|
671
691
|
})
|
|
672
692
|
})
|