fix(codemode): stop leaking undefined into tool arguments (#37652)
This commit is contained in:
parent
0d68b0bb20
commit
33f1b269e9
8 changed files with 97 additions and 20 deletions
|
|
@ -44,7 +44,7 @@ export const normalizeError = (error: unknown): Diagnostic => {
|
|||
message = (value as { message: string }).message
|
||||
} else {
|
||||
try {
|
||||
message = JSON.stringify(copyOut(value)) ?? String(value)
|
||||
message = JSON.stringify(copyOut(value, "json")) ?? String(value)
|
||||
} catch {
|
||||
message = String(value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export const executeWithLimits = <const Provided extends Record<string, unknown>
|
|||
logs,
|
||||
)
|
||||
const value = yield* interpreter.run(program)
|
||||
const result = copyOut(copyIn(value, "Execution result"), true) as DataValue
|
||||
const result = copyOut(copyIn(value, "Execution result"), "nullify") as DataValue
|
||||
returned = { value: result, promises }
|
||||
const warnings = yield* promises.interrupt()
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ const formatConsoleTable = (value: unknown, columnsArgument: unknown): string =>
|
|||
const consoleTableColumns = (value: unknown): ReadonlyArray<string> | undefined => {
|
||||
if (value === undefined) return undefined
|
||||
if (containsRuntimeReference(value)) return undefined
|
||||
const columns = copyOut(copyIn(value, "console.table columns"), true)
|
||||
const columns = copyOut(copyIn(value, "console.table columns"), "nullify")
|
||||
return Array.isArray(columns) ? columns.map((column) => String(column)) : undefined
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const invokeJsonMethod = (name: string, args: Array<unknown>, node: AstNo
|
|||
}
|
||||
const space = args[2]
|
||||
const indent = typeof space === "number" || typeof space === "string" ? space : undefined
|
||||
return JSON.stringify(copyOut(copyIn(args[0], "JSON.stringify value")), null, indent)
|
||||
return JSON.stringify(copyOut(copyIn(args[0], "JSON.stringify value"), "json"), null, indent)
|
||||
}
|
||||
case "parse": {
|
||||
const text = args[0]
|
||||
|
|
|
|||
|
|
@ -118,8 +118,7 @@ export class ToolRuntimeError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
const isDefinition = <R>(value: Definition<R> | Tools<R>): value is Definition<R> =>
|
||||
isToolDefinition<R>(value)
|
||||
const isDefinition = <R>(value: Definition<R> | Tools<R>): value is Definition<R> => isToolDefinition<R>(value)
|
||||
|
||||
const runHost = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, ToolError, R> =>
|
||||
effect.pipe(
|
||||
|
|
@ -257,18 +256,31 @@ const copyBounded = (
|
|||
return copied
|
||||
}
|
||||
|
||||
export const copyOut = (value: unknown, undefinedAsNull = false): unknown => {
|
||||
if (value === undefined && undefinedAsNull) return null
|
||||
// "json" mirrors JSON.stringify (undefined object values drop, undefined array elements become
|
||||
// null, a bare undefined passes through): use it wherever data leaves as JSON, like tool
|
||||
// arguments and stringify-style formatting. "nullify" turns every undefined, including a bare
|
||||
// one, into null: use it for program results, where the consumer must never see undefined.
|
||||
export type CopyOutMode = "json" | "nullify"
|
||||
|
||||
export const copyOut = (value: unknown, mode: CopyOutMode): unknown => {
|
||||
if (value === undefined && mode === "nullify") return null
|
||||
if (typeof value === "number" && !Number.isFinite(value)) {
|
||||
return null
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
// Array.from densifies holes so sparse arrays normalize at the boundary like JSON does.
|
||||
return Array.from(value, (item) => copyOut(item, undefinedAsNull))
|
||||
return Array.from(value, (item) => {
|
||||
const copied = copyOut(item, mode)
|
||||
return copied === undefined && mode === "json" ? null : copied
|
||||
})
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === "object" && !(value instanceof ToolReference)) {
|
||||
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyOut(item, undefinedAsNull)]))
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.map(([key, item]) => [key, copyOut(item, mode)] as const)
|
||||
.filter(([, item]) => !(item === undefined && mode === "json")),
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
|
|
@ -696,13 +708,13 @@ export const make = <R>(
|
|||
invokeDefinition(
|
||||
"search",
|
||||
searchTool,
|
||||
args.map((arg) => copyOut(copyIn(arg, "Arguments for tool 'search'"))),
|
||||
args.map((arg) => copyOut(copyIn(arg, "Arguments for tool 'search'"), "json")),
|
||||
),
|
||||
),
|
||||
invoke: (path, args) =>
|
||||
Effect.gen(function* () {
|
||||
const name = canonicalSegments(path).join(".")
|
||||
const externalArgs = args.map((arg) => copyOut(copyIn(arg, `Arguments for tool '${name}'`)))
|
||||
const externalArgs = args.map((arg) => copyOut(copyIn(arg, `Arguments for tool '${name}'`), "json"))
|
||||
const tool = resolve(root, path)
|
||||
return yield* invokeDefinition(name, tool, externalArgs)
|
||||
}),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue