feat(compose): add agent input and output schemas - #623
Conversation
|
PR Title: feat(compose): add agent input and output schemas Commit: 本次变更为 agent-compose 的 agent 定义新增可选的 input_schema / output_schema(JSON Schema)能力:支持内联(YAML/JSON)或复用 scheduler.script 的 source descriptor(file/http/git)。核心实现是 pkg/compose 新增的 JSONSchemaSource/JSONSchema 类型与 normalizeJSONSchemaSource 解析/归一化逻辑,并贯通到 NormalizeOptions.ResolveSchemaURLs、proto AgentSpec.input_schema_json/output_schema_json、ProjectSpecToProto/ProjectSpecYAMLShape 映射、canonical JSON/YAML 输出,以及 projects records 的 managed agent config 持久化。整体设计与现有 script source 机制保持一致,未解析 source 的持久化保护(ValidateResolvedScriptURLs)、布尔/对象 schema、proto 往返哈希均有测试覆盖。主要风险集中在两处:(1) schema 规范化经 float64 中转导致大整数精度静默丢失(涉及 canonicalJSONSchemaDocument、JSONSchema.UnmarshalJSON/yamlValue、agentJSONSchemaYAMLValue,文件源 schema 在 normalize 时即被破坏);(2) 通过文件引用规避 provider 关键字冲突的 schema 经 proto→YAML 导出后会再次与 source descriptor 形状混淆,无法往返解析。 |
|
补充说明一下本 PR 增加的能力及其影响。 增加的内容Agent 定义新增两个彼此独立、完全可选的字段:
两个字段都接受标准 JSON Schema object 或 boolean schema,并支持两种配置方式:
已有的 配置示例下面的例子内联声明输入 Schema,并从项目内文件加载输出 Schema: agents:
researcher:
description: 调研指定主题并返回带引用的结论。
input_schema:
type: object
additionalProperties: false
required: [query]
properties:
query:
type: string
description: 需要调研的主题或问题。
max_findings:
type: integer
minimum: 1
maximum: 10
default: 3
description: 最多返回多少条结论。
output_schema:
provider: file
path: ./schemas/research-result.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["summary", "findings"],
"properties": {
"summary": {
"type": "string",
"description": "对调研问题的简要回答。"
},
"findings": {
"type": "array",
"description": "有依据的调研结论。",
"items": {
"type": "object",
"required": ["claim", "source"],
"properties": {
"claim": {
"type": "string",
"description": "事实性结论。"
},
"source": {
"type": "string",
"description": "支持该结论的来源。"
}
}
}
}
}
}执行 一个完整的公开示例也已放在: https://github.com/kingfs/agent-compose-sample/tree/master/agents/json-schema-agent 兼容性与影响
如果后续需要把声明升级为强制运行时契约,建议另行设计:输入侧明确只校验 |
|
PR Title: feat(compose): add agent input and output schemas Commit: 本次变更旨在修复 JSON Schema 在序列化往返时大整数精度静默丢失的问题(对应历史 finding 4aba060c)。核心改动:(1) pkg/compose/json_schema_source.go 中 yamlValue() 与 canonicalJSONSchemaDocument() 改用 json.Decoder.UseNumber(),新增 jsonNumbersForYAML() 将 json.Number 转为带显式 !!int/!!float 标签的 yaml.Node;(2) pkg/agentcompose/api/project_yaml_shape.go 中 agentJSONSchemaYAMLValue() 同步改用 UseNumber(),新增 jsonNumbersForProjectYAML() 实现相同逻辑;(3) 将 source 判定从 mappingHasKey(value,"provider") 收窄为 mappingHasSourceProvider()(provider 值须为 file/http/git),避免 JSON Schema 内含 provider 扩展关键字时被误判为外部 source。整体上,int64/uint64 范围内的大整数字面量现在可以稳定往返,历史精度丢失缺陷已解决,新增测试(大整数保留、provider 关键字往返、proto→YAML→Parse 的 hash 保持)覆盖了主要路径。发现的遗留问题:(1) 对小数/指数形式字面量(如 0.50、1e-3、高精度小数)存在新的不对称——canonical JSON 经 UseNumber 保留原始字面量,但 YAML 导出→导入经 yaml.v3 必然解析为 float64 后再 json.Marshal 归一化,导致精度被截断、表示被改写、项目 hash 不稳定(低严重度,数据完整性);(2) json.Unmarshal 改为 Decoder.Decode 后,顶层值之后的尾随非法字符被静默忽略,校验被放宽(低严重度,功能正确性)。 |
|
PR Title: feat(compose): add agent input and output schemas Commit: 本次改动为纯文档/注释澄清,共 3 个文件(+5/-4):pkg/compose/json_schema_source.go 的 JSONSchemaSource 注释、英文与中文的 agent-compose-yaml-manual 文档。改动将 source descriptor 的判定语义从"包含 provider 键的 mapping"细化为"顶层 provider 值为 file/http/git 的 mapping;其他 mapping 与 boolean 视为内联 schema"。 核查结论:
总体评估:无代码行为变化、无回归风险、无安全问题,文档与实现一致,未发现需要上报的缺陷。 |
|
PR Title: feat(compose): add agent input and output schemas Commit: 本变更(pkg/compose/json_schema_source.go 及对应测试)包含两项修复:1) canonicalJSONSchemaDocument 在解码首个 JSON 值后追加第二次 Decode,拒绝顶层值之后的尾随数据(对尾随空白仍放行),恢复与 json.Unmarshal 一致的严格解析语义,修复了历史问题 d273a93d(校验放宽);2) 新增 canonicalizeSchemaNumbers,把含 .eE 的 json.Number 统一转为 float64 后再 json.Marshal,使 canonical JSON 与 YAML 导出→导入的 float64 归一化行为一致,修复历史问题 5cbd4ea3(往返不稳定、项目 hash 变化)。同时删除了未使用的 mappingHasKey。 评估:尾随数据检查正确且完整(能拒绝第二 JSON 值、非法 token、null,且不影响尾随空白)。数值归一化方向能恢复往返稳定,但以牺牲精度为代价——来自纯 JSON 来源(file/http/git 外部 source、ParseCanonicalJSON)的高精度小数/指数数字(如 0.12345678901234567890123)现在会被静默截断到 float64 精度,而 base 中原样保留字面量,属于静默的数据语义改变。新增测试仅覆盖尾随数据拒绝,未覆盖数值归一化及 YAML 往返稳定性,存在回归测试缺口。 |
|
PR Title: feat(compose): add agent input and output schemas Commit: 本次变更移除了 canonicalJSONSchemaDocument 中对 canonicalizeSchemaNumbers 的调用(该函数会把含 .eE 的 json.Number 经 Float64() 转成 float64),使纯 JSON 来源(file/http/git 外部 schema 解析与 ParseCanonicalJSON 读回)的 schema 数字字面量如 0.12345678901234567890123、1e-3 得以原样保留,不再被静默截断或改写为 float64 最短表示。这正好修复了先前已确认的历史精度丢失问题。同时新增 TestAgentJSONSchemaPreservesJSONNumberLiterals,直接通过 UnmarshalJSON 验证字面量保留。整体评估:改动范围小、方向正确——json.Marshal 对 json.Number 会按字面量原样输出,因此 JSON 路径行为符合预期,且该测试在移除规范化后可通过、在旧逻辑下会失败,是有效的回归测试。需要注意的缺口:内联 YAML 入口(UnmarshalYAML)仍通过 value.Decode 把 YAML 标量解码为 float64,高精度数字字面量依旧丢失,新测试也未覆盖该入口,导致“保留数字字面量”的行为在不同入口间不一致(低严重度,已在发现中说明)。 |
|
PR Title: feat(compose): add agent input and output schemas Commit: 本次改动对 pkg/compose/json_schema_source.go 的 JSON Schema 处理做了加固与增强:引入 github.com/santhosh-tekuri/jsonschema/v6,用 jsonschema.UnmarshalJSON 替换原先基于 json.Decoder 的解析,并通过 compiler.Compile 在保存快照前对 schema 做真实编译校验(拒绝 type: unknown、非法正则等);新增 rejectJSONSchemaResourceLoader 拒绝所有外部 $ref 资源加载,确保应用已存储快照时不会隐式访问文件系统或网络;新增 jsonSchemaYAMLValue 将内联 YAML schema 转为 JSON 时保留数字字面量(如 0.12345678901234567890123、1e-3)。测试新增了内联 YAML 数字保留、非法关键字拒绝、外部 $ref 拒绝三组用例,文档同步更新。整体方向正确,安全加固到位;但需注意两点回归风险:canonicalJSONSchemaDocument 删除了原有的尾部多余数据(io.EOF)校验,可能使现有测试 TestAgentJSONSchemaRejectsTrailingJSON 失效并静默接受含尾部垃圾的来源内容;jsonSchemaYAMLValue 强制 mapping key 必须为 !!str,导致内联 YAML 中使用 << 合并键(锚点复用)的写法不再被支持。 |
| } | ||
|
|
||
| func canonicalJSONSchemaDocument(data []byte) ([]byte, error) { | ||
| value, err := jsonschema.UnmarshalJSON(bytes.NewReader(data)) |
There was a problem hiding this comment.
canonicalJSONSchemaDocument 移除了尾部多余数据校验,可能使现有测试失败并静默接受畸形 schema 内容
旧实现使用 json.Decoder 解码后用 decoder.Decode(&struct{}{}) != io.EOF 显式拒绝尾部多余数据(如 {"type":"object"} trailing)。本次改动将解析替换为 jsonschema.UnmarshalJSON,并删除了该 EOF 检查(同时移除了 io 导入)。santhosh-tekuri/jsonschema/v6 的 UnmarshalJSON 仅对输入做单次 json.Decoder.Decode,不会检查首个 JSON 值之后的尾部内容,因此含尾部垃圾数据的 schema 会被静默截断接受,且现有的 TestAgentJSONSchemaRejectsTrailingJSON 用例将失败(该用例本次未修改)。
Problem code:
Changed code at pkg/compose/json_schema_source.go:120
Recommendation:
在调用 jsonschema.UnmarshalJSON 后重新加入尾部数据校验(保留原先的双 Decode + io.EOF 检查,或等价地读取剩余字节确认仅为空白),以避免静默接受畸形来源内容并保持现有测试通过。
Suggested diff:
--- a/pkg/compose/json_schema_source.go
+++ b/pkg/compose/json_schema_source.go
@@
import (
"bytes"
"context"
"encoding/json"
"fmt"
+ "io"
"agent-compose/pkg/sources"
@@
func canonicalJSONSchemaDocument(data []byte) ([]byte, error) {
value, err := jsonschema.UnmarshalJSON(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("invalid JSON Schema: %w", err)
}
+ decoder := json.NewDecoder(bytes.NewReader(data))
+ decoder.UseNumber()
+ if err := decoder.Decode(&struct{}{}); err != io.EOF {
+ return nil, fmt.Errorf("invalid JSON Schema: unexpected trailing data")
+ }
switch value.(type) {
Summary
input_schemaandoutput_schemafields to agent definitions while preserving existing behavior when they are omitted.Testing
go test ./pkg/compose ./pkg/projects ./pkg/agentcompose/apitask linttask buildtask docs:buildconfig --jsonverification with an inline input schema and file-backed output schema.upcreated the project and a second identicalupreturned unchanged with the same revision.task testcompleted protobuf, deployment, installer, and script checks, then hit the existing unrelatedTestEnsurePromptAttachLLMFacadeEnvClaudeUsesControllerStorefailure. The same focused test fails on unmodifiedorigin/main.Checklist