Merge branch 'codex/canonical-tool-output' into codex/code-mode-typed-results
# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/services.md # examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md # examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md # examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md # examples/acp-agent/tests/snapshots/code-mode-workspace-context/system-prompt.expected.md # packages/core/tools/README.md
This commit is contained in:
@@ -350,6 +350,25 @@ export class ToolOutputError extends HarnessError {
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert one projector exception into the canonical invalid-output failure. */
|
||||
function projectionError(toolName: string, projector: 'render' | 'presentationMeta', error: unknown): ToolOutputError {
|
||||
return new ToolOutputError(toolName, [`output.${projector} failed: ${errorMessage(error)}`])
|
||||
}
|
||||
|
||||
/** Snapshot one projector result before later durable-result materialization. */
|
||||
function snapshotProjection<T>(toolName: string, projector: 'render' | 'presentationMeta', candidate: T): T {
|
||||
try {
|
||||
const detached = snapshotJsonValue(candidate)
|
||||
if (detached === undefined) {
|
||||
throw new ToolOutputError(toolName, [`output.${projector} returned non-lossless JSON`])
|
||||
}
|
||||
return detached
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ToolOutputError) throw error
|
||||
throw projectionError(toolName, projector, error)
|
||||
}
|
||||
}
|
||||
|
||||
/** Successful canonical tool execution, including its Native/model projection. */
|
||||
export interface ToolExecutionSuccess {
|
||||
readonly isError: false
|
||||
@@ -1167,10 +1186,23 @@ export class ToolRegistry extends Service {
|
||||
const violations = validateJsonSchemaValue(tool.output.schema, detached, 'value')
|
||||
if (violations.length > 0) throw new ToolOutputError(tool.name, violations)
|
||||
const value = deepFreeze(detached as JsonValue)
|
||||
const content = tool.output.render(exec.arguments, value)
|
||||
const meta = exec.parent === undefined && tool.output.presentationMeta !== undefined
|
||||
? tool.output.presentationMeta(exec.arguments, value)
|
||||
: undefined
|
||||
let rendered: ContentBlock[]
|
||||
try {
|
||||
rendered = tool.output.render(exec.arguments, value)
|
||||
} catch (error: unknown) {
|
||||
throw projectionError(tool.name, 'render', error)
|
||||
}
|
||||
const content = snapshotProjection(tool.name, 'render', rendered)
|
||||
let meta: JsonValue | undefined
|
||||
if (exec.parent === undefined && tool.output.presentationMeta !== undefined) {
|
||||
let projected: JsonValue
|
||||
try {
|
||||
projected = tool.output.presentationMeta(exec.arguments, value)
|
||||
} catch (error: unknown) {
|
||||
throw projectionError(tool.name, 'presentationMeta', error)
|
||||
}
|
||||
meta = snapshotProjection(tool.name, 'presentationMeta', projected)
|
||||
}
|
||||
return this.markCanonical(this.materializeFinalResult({
|
||||
isError: false,
|
||||
value,
|
||||
|
||||
@@ -235,13 +235,21 @@ function checkSchemaNode(node: unknown, path: string, violations: string[], seen
|
||||
case 'boolean':
|
||||
case 'null': {
|
||||
const allowed = node.enum
|
||||
const enumValid = Array.isArray(allowed)
|
||||
&& allowed.length > 0
|
||||
&& allowed.every(entry => scalarMatches(schemaType, entry))
|
||||
if (Object.hasOwn(node, 'enum')) {
|
||||
if (!Array.isArray(allowed) || allowed.length === 0 || !allowed.every(entry => scalarMatches(schemaType, entry))) {
|
||||
if (!enumValid) {
|
||||
violations.push(`${path}.enum must be a non-empty array of ${schemaType} values`)
|
||||
}
|
||||
}
|
||||
if (Object.hasOwn(node, 'const') && !scalarMatches(schemaType, node.const)) {
|
||||
violations.push(`${path}.const must be a ${schemaType} value`)
|
||||
const constValid = scalarMatches(schemaType, node.const)
|
||||
if (Object.hasOwn(node, 'const')) {
|
||||
if (!constValid) {
|
||||
violations.push(`${path}.const must be a ${schemaType} value`)
|
||||
} else if (enumValid && !allowed.includes(node.const as JsonSchemaScalar)) {
|
||||
violations.push(`${path}.const must be one of ${path}.enum when both are declared`)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -300,8 +308,20 @@ function propertyPath(path: string, key: string): string {
|
||||
return path === '' ? key : `${path}.${key}`
|
||||
}
|
||||
|
||||
/** Collect value violations for one trusted schema node. */
|
||||
/** Contain hostile getters/proxies so validation remains total for arbitrary values. */
|
||||
function checkValue(node: JsonSchemaNode, value: unknown, path: string): string[] {
|
||||
if (node.type !== undefined && !(SCHEMA_TYPES as readonly unknown[]).includes(node.type)) {
|
||||
return checkValueUnchecked(node, value, path)
|
||||
}
|
||||
try {
|
||||
return checkValueUnchecked(node, value, path)
|
||||
} catch {
|
||||
return [`"${diagnosticPath(path)}" must be a lossless JSON value`]
|
||||
}
|
||||
}
|
||||
|
||||
/** Collect value violations for one trusted schema node after the exception boundary. */
|
||||
function checkValueUnchecked(node: JsonSchemaNode, value: unknown, path: string): string[] {
|
||||
if (node.oneOf !== undefined) {
|
||||
const matches = node.oneOf.filter(branch => checkValue(branch, value, path).length === 0).length
|
||||
return matches === 1 ? [] : [`"${diagnosticPath(path)}" must match exactly one oneOf branch (matched ${matches})`]
|
||||
|
||||
@@ -203,7 +203,12 @@ function compilePropertyMap(
|
||||
if (Object.hasOwn(property, 'required') && property.required !== true) {
|
||||
authorError(`${path}.${key}.required must be true when present`)
|
||||
}
|
||||
properties[key] = compileValueSchema(property, `${path}.${key}`, seen, true)
|
||||
Object.defineProperty(properties, key, {
|
||||
value: compileValueSchema(property, `${path}.${key}`, seen, true),
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
})
|
||||
if (property.required === true) required.push(key)
|
||||
}
|
||||
return required.length > 0 ? { properties, required } : { properties }
|
||||
|
||||
Reference in New Issue
Block a user