抽取画布 Agent 公共执行框架
Project CI / Repository checks (push) Successful in 1m3s
Project CI / Frontend tests (push) Successful in 1m59s
Project CI / Native shell tests (push) Successful in 2m27s
Project CI / Backend tests (push) Successful in 3m5s

新增业务中立的 function-calling harness 与公共提示词协议

统一画布 Agent 工具路由约束和待确认控制语义

保留失败前输出并事务化收口 memory、deadline 与取消状态

迁移画布 Agent 和 API 编排并同步架构文档与项目记忆
This commit is contained in:
2026-07-28 21:34:34 +08:00
parent 271d5d7bd3
commit d7f0c45164
36 changed files with 2310 additions and 720 deletions
@@ -4250,8 +4250,8 @@
- 背景:VectorEngine Apifox `api-349239079` 暴露 OpenAI-compatible `POST /v1/chat/completions`;创意 Agent 和通用 LLM 代理需要统一到 VectorEngine 文本服务,并将默认文本模型切换为 `gpt-5.4-mini`
- 决策:创意 Agent 的 `CREATIVE_AGENT_GPT5_MODEL` 固定为 `gpt-5.4-mini`,协议切到 Chat Completions,不再携带旧 APIMart `official_fallback` 字段;画布 Agent 侧边栏聊天规划请求也复用该模型和 Chat Completions 协议,不再显式使用 `gpt-4o` / Responses。通用 `/api/llm/chat/completions` 代理使用 `GENARRATIVE_LLM_PROVIDER=openai-compatible``GENARRATIVE_LLM_BASE_URL=https://api.vectorengine.cn/v1``GENARRATIVE_LLM_MODEL=gpt-5.4-mini`。未单独配置 `GENARRATIVE_LLM_API_KEY` 时,api-server 可复用 `VECTOR_ENGINE_API_KEY`;前端 LLM 客户端必须兼容 OpenAI `choices`、api-server raw `{content}` 和项目 envelope `{ok,data:{content}}` 三种非流式响应,以及 OpenAI SSE delta 和 api-server `event: delta` 两种流式响应。
- 决策补充:画布 Agent 的 planning prompt 必须自动注入上一条已完成生成结果的 `latestGeneratedImage`,来源为上一轮 generation 的 `toolName` / `resourceId` / `objectKey` 等轻量元数据。用户用「这张」「刚才那个」「上一张」「把衣服换成……」等方式指代上一张图或继续编辑时,规划默认调用 `edit_image`引用该结果;不能因为本轮没有手动附件而退回 `generate_image`
- 决策补充:画布 Agent 侧边栏的“规范图 / 视觉规范图 / 风格规范图 / 素材规范展板”是 Agent 规划 prompt 和 function-calling 工具选择约束,不是侧边栏 UI 说明文案。此类请求默认走 `generate_image`,prompt 必须要求规范展板包含统一视角、线条粗细、色卡、材质、阴影、圆角、状态层级、尺寸标注等视觉规范元素;角色规范图若是规范展板也走 `generate_image`,只有实际角色立绘才走 `generate_character`,多个图标素材 / 图集才走 `generate_icon_spritesheet`
- 决策补充:画布 Agent 的 planning prompt 必须自动注入上一条已完成生成结果的 `latestGeneratedImage`,来源为上一轮 generation 的 `toolName` / `imageId` / `resourceId` / `objectKey` 等轻量元数据。用户用「这张」「刚才那个」「上一张」「把衣服换成……」等方式指代上一张图或继续编辑时,规划默认调用 `edit-image``latestGeneratedImage.imageId` 传入 `edit-image.object_image_id`;不得生成工具 schema 中不存在的 `source_image_id`,也不能因为本轮没有手动附件而退回 `generate-image`
- 决策补充:画布 Agent 侧边栏的“规范图 / 视觉规范图 / 风格规范图 / 素材规范展板”是 Agent 规划 prompt 和 function-calling 工具选择约束,不是侧边栏 UI 说明文案。此类请求默认走 `generate-image`,prompt 必须要求规范展板包含统一视角、线条粗细、色卡、材质、阴影、圆角、状态层级、尺寸标注等视觉规范元素;角色规范图若是规范展板也走 `generate-image`,只有实际角色立绘才走 `generate-character`,多个图标素材 / 图集才走 `generate-icon-spritesheet`
- 影响范围:`server-rs/crates/platform-agent``server-rs/crates/api-server/src/config.rs``src/services/llmClient.ts``.env.example``deploy/env/api-server.env.example``scripts/test-ve-llm.mjs`
- 验证方式:`npm run test -- src/services/llmClient.test.ts``cargo test -p api-server --manifest-path server-rs/Cargo.toml from_env_reads_non_public_models_and_urls app_state_builds_creative_agent_gpt5_client_from_vector_engine_settings llm_chat_completions editor_agent_llm_request_uses_vector_engine_chat_model``cargo test -p platform-agent --manifest-path server-rs/Cargo.toml``npm run check:encoding``git diff --check`
@@ -4510,3 +4510,11 @@
- 对账边界:保留管理员显式手动对账。owner 始终可用;member 必须单独持有 `profile-wallet-consumption-reconcile` 独立操作权限,任意 Tab 都不隐式授予。`POST /admin/api/profile/users/reconcile-consumption` 经二次确认后调用 runtime service identity 受限 procedure,扫描该用户全部权威流水、比较并校准投影,记录管理员与对账时间。
- 展示边界:现有共享“用户详情”弹窗的钱包区增加“历史花费”,前端只展示 BFF 顶层字段,不自行汇总账单;只有 BFF 返回 `canReconcileConsumption=true` 时展示手动对账按钮。
- 验证方式:SpacetimeDB 钱包聚合测试、api-server / admin-web 定向测试、`npm run spacetime:generate``npm run check:spacetime-schema``npm run check:spacetime-runtime-access``npm run admin-web:typecheck``npm run check:encoding``git diff --check`
## 2026-07-28 画布 Agent 的通用 function-calling harness 与画布 prompt 分层
- 背景:画布 Agent 的 JSON 输出协议、tool schema 注入、memory / hook、轮次保护和“全部工具待确认即结束回合”原先位于 `platform-editor-agent/src/framework`,与规范展板、已有图编辑路由、模型超时和画布工具混在同一 crate;八类工具还重复携带待确认控制话术。旧 `platform-agent` 已随 Creative Agent 退役,不能作为新公共层复活。
- 决策:新增无旧玩法依赖的现役 `platform-agent-harness`,只承载业务中立的 function-calling 执行协议;`platform-editor-agent` 通过兼容 re-export 复用该 crate,并继续承载画布 LLM profile、角色 prompt、公共美术工具路由策略、图片上下文和工具实现。无工具场景同样注入 JSON 响应格式;prompt 不再宣称工具并发执行;request 级 system prompt 必须真实进入本轮请求。画布对话额外注入最近一条已完成图片的有界 `latestGeneratedImage` 元数据,后续编辑仍只用 SHA-256 `imageId` 选图。
- 执行与失败决策:prompt 每轮通过 `AgentMemory::begin_staged` 使用与调用方 memory 行为等价、写入隔离的 `StagedAgentMemory` 事务;成功或已有工具活动时显式 `commit()`,直接 drop 表示回滚。无工具活动失败时回滚本轮 staged 增量,已发生工具活动后失败时提交已发生工具事实并追加 terminal error closure。外部 future drop / abort 若发生在工具完成后,提交工具结果与取消闭环;若发生在工具执行中,提交“已启动、结果未知”与取消闭环,后续先 reconcile,不能假装副作用未发生。harness 通过 `PromptRunError { error, partial_outputs }` 显式返回终态错误和失败前输出;结构化工具失败还必须向调用方保留 `ToolFailure.kind/retryable/fatal` 与原始 `output`,不在 harness 内压成单一字符串。api-server 的 18 分钟总 deadline 以 runtime future 下沉到 runnercompletion 可被 deadline 终止,工具在开始前检查、开始后等待返回、返回后携带结果收口;禁止外层 timeout drop prompt 或中途取消 effectful tool 后伪造空 partial。
- 保留边界:会话幂等、OSS 消息、120 秒前端软提示、20 分钟 transport、18 分钟 handler 总 deadline、1024 tokens、8 分钟 provider attempt、泥点计费、确认入队和 external job 懒回填均不进入公共 harness。SpacetimeDB schema、前端 wire DTO 和侧边栏 UI 不变。
- 验证方式:`cargo test -p platform-agent-harness``cargo test -p platform-editor-agent``cargo test -p api-server editor_agent``cargo check -p api-server --locked`、DDD 边界检查、Rustfmt、编码检查和 `git diff --check`
+12 -3
View File
@@ -3272,8 +3272,17 @@
- 现象:画布 Agent 已生成有效工具规划,却最终只保存 `ERROR max turns reached: 3`,助手文本和待确认工具卡都消失。
- 原因:八类画布工具的 `call()` 只返回待用户确认的规划结果,但 function-calling runner 在成功工具后仍继续请求 LLM,只靠 prompt 要求模型不再重试;模型连续返回工具调用直到上限后,错误结果又丢弃此前累积的输出。
- 处理:工具通过框架契约显式声明 `requires_user_confirmation`;当本批全部工具都成功且等待确认时,runner 在处理完整批次后立即返回已有助手文本和工具结果。未知工具、参数错误、hook skip、普通连续工具和不可解析响应仍继续受 `max_turns` 门禁保护。不要用单纯提高轮次上限掩盖终止条件缺失。
- 验证:runner 回归测试必须同时覆盖“待确认工具只调用一次 LLM 并成功结束”“普通连续工具仍会触发 max-turn 门禁”。
- 关联:`server-rs/crates/platform-editor-agent/src/framework/run.rs``server-rs/crates/platform-editor-agent/src/framework/tool.rs``server-rs/crates/platform-editor-agent/src/agent/tools/`
- 验证:runner 回归测试必须同时覆盖“待确认工具只调用一次 LLM 并成功结束”“普通连续工具仍会触发 max-turn 门禁”“多工具按数组顺序执行”“request 级 system prompt 真实进入请求”;公共 prompt 在无工具时仍必须包含 runner 所需的 JSON 响应格式,且不得宣称并发执行
- 关联:`server-rs/crates/platform-agent-harness/src/run.rs``server-rs/crates/platform-agent-harness/src/tool.rs``server-rs/crates/platform-editor-agent/src/agent/tools/`
## Agent 终态失败不能吞掉已发生的工具事实
- 现象:同一轮 prompt 中前面工具已经成功生成待确认结果,但后续工具、hook、completion 或 `max_turns` 失败后,API 只保存最后一条 `ERROR `,已执行工具和用户本轮语义从会话历史中消失。
- 原因:runner 只返回单一 `PromptError`,或者直接向 committed memory 逐步写入,无法区分“尚未发生外部工具事实,整轮可回滚”与“已发生工具事实,只能提交并闭合错误”。工具失败若被压成字符串,调用方还会丢失 `kind``retryable``fatal` 和原始 `output`
- 处理:用 `PromptRunError { error, partial_outputs }` 保留失败前输出,并将本轮 memory 先写入 staged buffer。无工具活动失败时整体回滚 staged 增量;有成功或失败工具活动时提交已发生事实,并追加 terminal error closure。api-server 按 `partial_outputs` 顺序先持久化成功工具的 `not_completed` 待确认消息,再追加 `ERROR ` 终态消息;`ToolFailed` 保留给调用方做诊断和流程决策,不伪装成成功确认卡。
- 取消边界:不能在 prompt future 内对 `agent.memory.take()` 后跨 await 持有,也不能用统一 `VecMemory` staging 绕过自定义 memory 的限长、摘要或脱敏规则。`AgentMemory::begin_staged` 必须产生行为等价、写入隔离的 `StagedAgentMemory`,成功或已有工具活动时显式 `commit()`,直接 drop 才表示回滚。外部 drop 若发生在工具完成后,guard 必须提交结果与取消闭环;若工具仍在执行,至少提交“已启动、结果未知”事实,供后续 reconcile。正式总 deadline 应作为 runner 内部 future 终止 completion;工具开始前检查 deadline,一旦开始则不能中途 drop,必须等待结果后再携带 partial outputs 收口。外层 timeout 只适合作为进程级最后保险,不能承担业务收口。
- 验证:至少覆盖“无工具 completion 失败回滚 staged 用户消息”“非 fatal 工具失败对调用方暴露 `kind/retryable/fatal/output`”“成功工具后终态失败保留 partial tool output”“有工具活动时 committed memory 末尾存在 error closure”以及“API 增量中待确认工具位于 terminal `ERROR ` 之前”。上一张图继续编辑的 prompt 测试必须断言 `latestGeneratedImage.imageId -> edit-image.object_image_id`,且最终 prompt 不含 `source_image_id`
- 关联:`server-rs/crates/platform-agent-harness/src/run.rs``server-rs/crates/platform-agent-harness/src/tool.rs``server-rs/crates/platform-editor-agent/src/agent/prompt.rs``server-rs/crates/api-server/src/editor_agent/api.rs`
## 画布 Agent 的规划请求不能关闭瞬时失败重试
@@ -3281,7 +3290,7 @@
- 原因:规划请求虽然有 Agent 专用单次 timeout,但 `editor_agent_llm_client``max_retries` 硬编码为 0VectorEngine `gpt-5.4-mini` 的偶发长尾、连接超时或可重试上游状态会在第一次失败后直接持久化成 system error。framework 的英文 `completion error` 前缀也被原样暴露给用户。
- 处理:120 秒改为前端软提示阈值:POST 仍 pending 时显示不入库的“仍在处理中,请耐心等待”;provider 明确断开/失败才写正式错误。专用 provider 单 attempt 使用 8 分钟 hard timeout,请求发起阶段读取 `GENARRATIVE_LLM_MAX_RETRIES`,但画布 Agent 最多重试 1 次且重试退避最多 60 秒。不要只计算单次 complete 的最坏时间:runner 还可因非法 JSON/工具校验失败进入后续轮次,必须从 handler 入口开始计算 18 分钟总 deadline,进入 `agent.prompt(...)` 时扣除会话锁/上下文准备已用时间,为持久化和前端 20 分钟 timeout 留出余量。响应头后的体读取/解析错误按明确失败收口,必须使用真实 attempt 计数;规划、配置和定价错误对用户统一为中文,原始诊断只记后端日志。重试发生在任何生成工具执行前,不会重复提交生成任务或扣费,不要通过提高前端 timeout 或 runner `max_turns` 掩盖 provider 重试缺失。
- 验证:`platform-editor-agent` 测试锁定 8 分钟 hard timeout 与中文错误;前端 fake timer 用例锁定 120 秒前只显示思考动画、到点后显示耐心等待、成功/失败后移除;`platform-llm` 回归用例锁定第二次 attempt 成功响应头后的 body timeout 仍报累计 2 次;`api-server` 测试锁定专用 client retry、18 分钟整体 deadline 与中文直达错误。运行态排障按同一 request id 对齐 `platform_llm` failure stage 与 `/messages` 总耗时,并确认仍 pending 的请求不再在 120 秒形成错误气泡。
- 关联:`server-rs/crates/platform-editor-agent/src/agent/agent.rs``server-rs/crates/platform-editor-agent/src/framework/error.rs``server-rs/crates/api-server/src/state.rs``src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts``src/components/image-editor/EditorAgentConversation/MessageBubble.tsx``src/services/image-editor/editorAgentClient.ts`
- 关联:`server-rs/crates/platform-editor-agent/src/agent/agent.rs``server-rs/crates/platform-agent-harness/src/error.rs``server-rs/crates/api-server/src/state.rs``src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts``src/components/image-editor/EditorAgentConversation/MessageBubble.tsx``src/services/image-editor/editorAgentClient.ts`
## 前端退役目录不能只靠扫描和 ignore 隔离
@@ -59,7 +59,7 @@
- SpacetimeDB schema guard 比较当前工作树与基线提交时,两侧都必须分别读取各自 `Cargo.toml``lib.path`,再沿 `mod` / `#[path]` 只扫描该快照 crate root 可达的 schema;不得递归扫描整个 `src/`,否则原位保留的旧源码会与现役历史数据壳产生假 accessor 重复。
- `module-runtime` 仍是账号、钱包、公共设置、追踪和 feature gate 的现役领域 crate;其混合源码中的 `CreationEntry*`、旧公开作品、旧存档 / 浏览历史 / 游玩统计 DTO、command、mapper 和规则必须以编译条件退出,且不再依赖只为旧创作契约存在的 `shared-contracts`。历史 schema 只继续编译 `RuntimeBrowseHistoryThemeMode` 六个变体和完整保序的 `RuntimeProfileWalletLedgerSourceType` 等持久化 ABI,不保留围绕这些类型的旧业务实现。
- 纯模板 crate 和专属运行态 crate 不属于 workspace members、default members 或任何在运 crate 的依赖图;源码目录保持原样。
- `platform-agent` 及其专属 `langchainrust` 依赖同样退出 workspace 与 `api-server` 依赖图;现役编辑器 Agent 仅需的模型常量收口到 `platform-llm`,不再通过旧拼图 Phase 1 / Creative Agent 执行器 crate 复用。
- `platform-agent` 及其专属 `langchainrust` 依赖同样退出 workspace 与 `api-server` 依赖图;现役编辑器 Agent 仅需的模型常量收口到 `platform-llm`,不再通过旧拼图 Phase 1 / Creative Agent 执行器 crate 复用。后续抽出的 `platform-agent-harness` 是无旧玩法依赖的通用 JSON function-calling 底座,不得依赖、复用或重新挂回本条退役 crate。
- `platform-auth` 不再编译 runtime guest token`platform-wechat` 不再编译旧生成结果订阅服务,只保留现役认证和支付协议。
## 验收
@@ -24,7 +24,7 @@ SpacetimeDB 版本口径:当前 Rust crate `spacetimedb`、`spacetimedb-sdk`
- HTTP 与运维入口:`api-server``pingora-gateway``server-manager-panel`
- 现役领域模块:`module-ai``module-assets``module-auth``module-editor-agent``module-runtime``module-runtime` 继续承载账号、钱包、公共设置、追踪、功能门禁等现役平台领域能力;该 crate 名称不代表旧玩法 runtime 路由仍在运行。
- 平台副作用:`platform-agent``platform-auth``platform-audio``platform-hyper3d``platform-image``platform-llm``platform-matting``platform-oss``platform-speech``platform-wechat`
- 平台副作用:`platform-agent-harness``platform-editor-agent``platform-auth``platform-audio``platform-hyper3d``platform-image``platform-llm``platform-matting``platform-oss``platform-speech``platform-wechat`已退役 Creative Agent 的旧 `platform-agent` 只保留历史源码,不属于现役 workspace 或依赖图。
- 共享层:`shared-contracts``shared-kernel``shared-logging`
- SpacetimeDB`spacetime-client``spacetime-module`
- 测试支撑:`tests-support`
@@ -76,11 +76,16 @@ npm run check:server-rs-ddd
- `/api/editor/projects/{projectId}/agent-conversations` 负责当前工程会话列表和新建;`/api/editor/agent-conversations/{conversationId}` 负责详情读取、终态工具消息懒回填和软删;`POST /api/editor/agent-conversations/{conversationId}/messages` 负责发送消息并返回普通 JSON `EditorAgentMessageResponse`,画布 Agent 不提供 `/messages/stream` SSE 路由。消息请求必须携带最长 128 字符的 `clientMessageId`;前端对该 POST 显式启用 1 次瞬时 transport 重试,并复用同一个序列化 body、`clientMessageId``x-request-id`。同一会话在锁内按该键幂等,重复键同内容返回已有回合或从已保存用户消息继续,异内容返回 `409`。数字 `EditorAgentMessage.id` 仍只作为工具确认 / 取消的后端消息定位符,不能复用为客户端幂等键。
- `module-editor-agent` 只承载纯领域校验:标题派生、附件上限、消息输入规则和会话软删访问规则;不直接依赖 Axum、SpacetimeDB、OSS、LLM 或 Tokio。
- `platform-agent-harness` 只承载与具体业务无关的 JSON function-calling 协议、工具 schema 注入、memory、hook、typed tool、轮次保护和待用户确认终止语义;`platform-editor-agent` 在其上叠加画布专属 LLM profile、system prompt、跨工具路由规则、图片上下文与八类生成工具。公共 harness 不依赖画布 DTO、计费、OSS、Axum 或 external job,也不得复用已退役的旧 `platform-agent`。harness 失败契约固定为 `PromptRunError { error, partial_outputs }``partial_outputs` 显式携带失败前已产生的助手文本、成功工具和结构化失败工具输出。`ToolFailure``kind``retryable``fatal` 及工具原始 `output` 必须对 harness 调用方可见,不得在公共层压成字符串或擅自丢弃。
- prompt runner 对每个调用通过 `AgentMemory::begin_staged` 创建行为等价、写入隔离的 `StagedAgentMemory` 事务:限长、摘要、脱敏或持久化 memory 的 append 语义必须在本轮模型请求前生效,不得统一降级成 `VecMemory`。成功结束或已发生工具活动时必须显式调用 staged `commit()`,直接 drop 表示回滚。无工具活动的 completion、hook、解析或 `max_turns` 失败丢弃 staged transaction;已有成功或失败工具活动时在末尾追加 terminal error closure 后提交。外部 future drop / abort 若发生在工具完成后,必须提交工具结果与取消闭环;若工具仍在执行,则提交“已启动、结果未知”事实与取消闭环,供后续 reconcile,不能假装工具没有发生。
- 画布 handler 的 18 分钟总 deadline 通过 runtime 提供的 deadline future 下沉到公共 runnercompletion await 可被 deadline 终止;effectful tool 在开始前检查 deadline,开始后不被中途 drop,返回后再携带结果收口为 `PromptRunError`。禁止用外层 timeout 直接 drop 整个 prompt future 并伪造空 `partial_outputs`
- `spacetime-module``editor_agent_conversation` 只保存元数据;创建、列表、读取、更新时间和软删通过 `create_editor_agent_conversation_and_return``list_editor_agent_conversations_and_return``get_editor_agent_conversation_and_return``touch_editor_agent_conversation_and_return``delete_editor_agent_conversation_and_return` procedure 完成,`api-server` 只能经 `spacetime-client` facade 访问。
- 完整消息文档存 OSS `editor-agent/{conversationId}.json`,由 `api-server` 负责 2 MiB 上限、会话内串行锁、读改写、消息与工具结果持久化和 `touch` 元数据更新时间;该 JSON 不进入 `editor_canvas.layers_json`,也不作为画布布局真相。LLM 未配置、连接已经断开、请求明确失败、达到最终安全上限或规划不可解析时,必须写入 `role=system`、正文以 `ERROR ` 开头的消息,并通过 `deltaMessages` 返回,`errorMessage` 保持为空;前端隐藏前缀并显示红色错误气泡,面向用户的错误正文使用中文语义,不暴露 `completion error` 等 framework 内部前缀或原始配置/定价错误;原始诊断只写后端结构化日志。后端仍把该 system 消息注入后续 LLM memory,使 Agent 能读取失败上下文。普通 JSON POST 尚未结束不形成持久化消息;工具失败同样必须形成可回读记录,不能只返回瞬时错误。
- 画布 Agent 的 `gpt-5.4-mini` Chat Completions 规划使用 1024 `max_tokens`。前端在 POST pending 120 秒后显示不入库的耐心等待提示;provider request future 明确返回 connect/timeout/HTTP/transport 错误时立即进入正式失败,尚未返回则继续等待。专用 provider 单 attempt hard timeout 为 8 分钟;请求发起阶段的 timeout、连接失败、`408``429``5xx` 读取 `GENARRATIVE_LLM_MAX_RETRIES`,但画布 Agent 最多重试 1 次,显式配置 0 仍可关闭,专用重试退避最多 60 秒。消息规划生命周期从 handler 入口开始计入 18 分钟总 deadline,进入 `agent.prompt(...)` 时只使用剩余预算;该 deadline 覆盖会话锁/上下文准备与最多 3 轮规划,并为错误持久化/HTTP 返回预留约 2 分钟,不允许多轮规划绕过前端 20 分钟 timeout。已收到成功响应头后的响应体读取或解析失败直接按明确失败收口,并使用该成功响应所属的真实 attempt 记录错误。重试只包围 LLM 规划请求并发生在任何待确认工具执行之前,因此不会重复提交生成任务或扣费。
- 对话附件只允许引用当前工程 `editor_project_resource` 或当前账号 `editor_asset` 的图片;前端可提交展示用 `imageSrc` / `thumbnailSrc`,后端必须按 `resourceId` / `assetId` 重新归一、校验 owner / project 和 `objectKey`,再给 LLM 或生成工具使用。
- planning prompt 注入的 `latestGeneratedImage.imageId` 只能映射到 `edit-image.object_image_id``source_image_id` 不是现役 `edit-image` schema 字段,prompt、tool args、确认执行和测试中都不得生成或兼容该字段。
- 画布 Agent 工具复用既有编辑器图片生成 / 修改 / 图标 spritesheet BFF,并继续使用后端模型定价和 `execute_billable_asset_operation_with_cost`;前端不提交 `priceMudPoints`
- api-server 对 `PromptRunError` 的持久化顺序固定为:先按 `partial_outputs` 原顺序映射已成功工具,将其保存为 `status=not_completed` 且无 `externalJobId` 的待确认消息;再在同一会话增量末尾追加 `ERROR ` terminal system 消息并整体写入 OSS。后续规划失败不得吞掉失败前已执行的成功工具结果;结构化 `ToolFailed` 可用于调用方诊断与流程决策,但画布确认面不得把它伪装成成功待确认卡。
- `/messages/{messageId}/confirm``/messages/{messageId}/cancel` 只返回成功确认;前端成功后立即重新读取整个会话,以会话详情中的权威消息状态和 `externalJobId` 驱动气泡展示与任务轮询。
- 会话详情的终态懒回填必须在单次 GET 和同一 conversation lock 内完成有界重试:任务结果读取、completed payload 解析或工具 formatter 首次失败后最多重试 3 次,每次等待 100ms 并重新读取主任务。任务读取失败或 completed 任务暂缺 `result_payload_json` 时,本次重试耗尽后仍保留 OSS 工具消息的 `not_completed + externalJobId`,由下次会话读取继续 reconcile;JSON 损坏、结果结构不兼容或 formatter 失败等确定性致命错误在重试耗尽后原子写为 `failed`,保存“重试 3 次后仍失败”的最后错误,避免永久循环。
- 画布 Agent 是“正式任务 payload 不进入通用用户 read model”规则的窄例外消费者:`GET /api/editor/agent-conversations/{conversationId}` 只按会话中已有的 `externalJobId` 定向读取主任务,完成后由对应工具 formatter 从 `result_payload_json` 提取并归一有界的图片 / 视频 / 音频引用,写入 OSS 工具消息后返回。前端仍不得通过通用任务列表 / 状态接口读取或解析 `request_payload_json` / `result_payload_json`;OSS 轻量媒体引用只是会话展示与后续 Agent 上下文,不替代 `editor_project_resource``editor_asset`、结构化画布表或 `external_generation_job` 的业务真相。未激活结构化存储的 canvas 才继续以 `editor_canvas.layers_json` 作为 legacy 布局真相。
@@ -106,12 +106,16 @@
- 编排复用 `creative_agent_gpt5_client` 的 LLM 接入配置(同 provider/env,独立用途标识),画布 Agent 规划请求固定使用 VectorEngine `gpt-5.4-mini` Chat Completionsfunction-calling 注册八类工具。
- 每个用户回合必须由 LLM 返回结构化计划;LLM 未配置、连接已经断开、请求明确失败、达到最终安全上限或返回格式不可解析时,后端写入正文为 `ERROR <错误内容>` 的 system 消息,不使用本地关键词或“收到:...”回显兜底。面向用户的规划错误使用中文语义,不暴露 `completion error` 等 framework 内部前缀或原始配置/定价诊断;原始错误只记录在后端日志。该错误消息与其它 system 消息一样进入后续 LLM memory,使 Agent 能看到上一轮失败上下文。普通 JSON POST 尚未结束只表示 provider request future 仍在等待,不能伪装成已持久化失败。
- 规划 prompt 必须自动带入上一条已完成生成结果的 `latestGeneratedImage` 引用,内容只包含上一轮 generation 的 `toolName` / `resourceId` / `objectKey` / `assetObjectId` 等轻量元数据,不把私有签名 URL 或大图内容塞进 prompt。
- 规划 prompt 必须自动带入上一条已完成生成结果的 `latestGeneratedImage` 引用,内容只包含上一轮 generation 的 `toolName` / `imageId` / `resourceId` / `objectKey` / `assetObjectId` 等轻量元数据,不把私有签名 URL 或大图内容塞进 prompt。
- 工具参数中的图片 ID 是由真实 object key 或图片地址计算的稳定 SHA-256 标识;真实 data key 仅存于 api-server 的工具上下文映射,所有图片工具在执行时查表恢复,不能把 object key 或图片地址作为 LLM 可见的工具 ID。
- 用户使用「这张」「刚才那个」「上一张」「把衣服换成……」等方式指代或编辑上一张结果图时,LLM 默认选择 `edit_image` 并引用 `latestGeneratedImage` 作为源图;除非用户明确要求全新生成,否则不能因为本轮没有重新上传附件而降级为 `generate_image`
- 规划 prompt 必须显式区分“规范展板”和“实际素材产出”:规范图、视觉规范图、风格规范图、素材规范展板、角色规范图等规范展板请求走 `generate_image`,并补齐统一视角、线条粗细、色卡、材质、阴影、圆角、状态层级、尺寸标注等要求;实际角色立绘才走 `generate_character`,多个图标素材 / 图集才走 `generate_icon_spritesheet`
- 画布 Agent 规划请求使用 Chat Completions 和 1024 `max_tokens`。发送后 120 秒是前端软提示阈值,不是 provider 失败 deadline:若普通 JSON POST 仍 pending,消息流临时显示“仍在处理中,请耐心等待”并继续等待,提示不写入 OSS 消息历史;连接或请求明确失败则立即按正式错误收口。provider 单 attempt 保留 8 分钟 hard timeout;请求发起阶段的 timeout、连接失败、`408``429``5xx` 读取 `GENARRATIVE_LLM_MAX_RETRIES`,但画布 Agent 最多重试 1 次,专用重试退避最多 60 秒。消息规划生命周期从 handler 入口开始计入 18 分钟总 deadline,进入 `agent.prompt(...)` 时使用扣除会话锁和上下文准备后的剩余预算;该 deadline 覆盖非法 JSON/工具校验失败触发的后续规划轮,并为错误持久化和 HTTP 返回保留约 2 分钟,不再让前端 20 分钟 transport timeout 先触发。已收到成功响应头后的响应体读取或解析失败直接按明确失败收口,错误计数/日志使用该响应所属的真实 attempt。规划重试发生在任何生成工具执行之前,不会重复提交生成任务或扣费;生成图片/编辑图片仍走对应生成工具和模型计费。
- 用户使用「这张」「刚才那个」「上一张」「把衣服换成……」等方式指代或编辑上一张结果图时,LLM 默认选择 `edit-image`,并把 `latestGeneratedImage.imageId` 传入 `edit-image.object_image_id`;不得构造工具 schema 中不存在的 `source_image_id`除非用户明确要求全新生成,否则不能因为本轮没有重新上传附件而降级为 `generate-image`
- 规划 prompt 必须显式区分“规范展板”和“实际素材产出”:规范图、视觉规范图、风格规范图、素材规范展板、角色规范图等规范展板请求走 `generate-image`,并补齐统一视角、线条粗细、色卡、材质、阴影、圆角、状态层级、尺寸标注等要求;实际角色立绘才走 `generate-character`,多个图标素材 / 图集才走 `generate-icon-spritesheet`
- 画布 Agent 规划请求使用 Chat Completions 和 1024 `max_tokens`。发送后 120 秒是前端软提示阈值,不是 provider 失败 deadline:若普通 JSON POST 仍 pending,消息流临时显示“仍在处理中,请耐心等待”并继续等待,提示不写入 OSS 消息历史;连接或请求明确失败则立即按正式错误收口。provider 单 attempt 保留 8 分钟 hard timeout;请求发起阶段的 timeout、连接失败、`408``429``5xx` 读取 `GENARRATIVE_LLM_MAX_RETRIES`,但画布 Agent 最多重试 1 次,专用重试退避最多 60 秒。消息规划生命周期从 handler 入口开始计入 18 分钟总 deadline,进入 `agent.prompt(...)` 时使用扣除会话锁和上下文准备后的剩余预算;该 deadline 必须作为 runner 内部 deadline future 参与 completion await,并在每个 tool 开始前、返回后检查,不能用外层 `tokio::timeout` 丢弃整个 prompt future,也不能中途 drop 已开始的工具。工具一旦开始就等待其返回,再按 deadline 携带结果收口;当前八类画布工具只做同步参数校验并返回待确认,因此不会延长正式生成链。deadline 命中时仍按 `PromptRunError` 返回已经完成的工具结果、提交对应 staged memory 并追加终态错误。该 deadline 覆盖非法 JSON/工具校验失败触发的后续规划轮,并为错误持久化和 HTTP 返回保留约 2 分钟,不再让前端 20 分钟 transport timeout 先触发。已收到成功响应头后的响应体读取或解析失败直接按明确失败收口,错误计数/日志使用该响应所属的真实 attempt。规划重试发生在任何生成工具执行之前,不会重复提交生成任务或扣费;生成图片/编辑图片仍走对应生成工具和模型计费。
- function-calling runner 必须把“等待用户确认”作为显式工具语义:当本批所有工具都校验成功并进入待确认状态时,立即以成功结果结束当前规划回合并持久化助手文本与待确认卡,不得继续依赖 LLM 自行停止;未知工具、参数错误、普通连续工具和不可解析响应仍受 `max_turns` 保护。
- runner 失败必须返回显式的 `PromptRunError { error, partial_outputs }`,不得只返回终态错误而丢弃本轮已产生的文本或工具事实。prompt 执行使用 `AgentMemory::begin_staged` 创建行为等价且写入隔离的 `StagedAgentMemory` 事务,限长、摘要、脱敏等 append 规则必须在本轮 completion 前生效;成功或已发生工具活动时必须显式调用 `commit()`,直接 drop staged transaction 表示回滚,不得统一复制成 `VecMemory` 或仅替换 box 冒充持久化提交。本轮无工具活动失败时回滚 staged 用户消息、助手文本和不可解析响应;已有工具活动时在末尾追加 terminal error closure 后提交。外部 drop / abort 若尚无工具活动则回滚并保持原 committed memory;若工具已完成则提交结果与取消闭环,若工具仍在执行则提交“已启动、结果未知”事实与取消闭环,后续必须先 reconcile 再决定是否重试。
- `ToolFailure` 必须以结构化工具失败输出暴露给 harness 调用方:调用方能读取 `kind``retryable``fatal` 和工具返回的原始 `output`;不得把它们压成单一错误字符串。这些字段只提供流程决策与诊断事实,是否重试、如何展示或持久化仍由业务调用方决定。
- api-server 收到带 `partial_outputs` 的终态失败时,必须先按原顺序把其中已成功工具转成 `status=not_completed` 待确认消息并写入同一会话增量,再追加 `ERROR <错误内容>` 终态 system 消息;不得因后续轮次、其它工具或 `max_turns` 失败而吞掉已经执行并返回的工具结果。
- 通用 JSON function-calling 协议、工具 schema 注入、memory / hook、`max_turns` 和“全部工具待确认即结束回合”统一由现役 `platform-agent-harness` 承载;无工具时也必须输出同一 JSON 响应格式。画布角色 prompt、规范展板 / 已有图路由、模型与超时 profile、八类工具、计费、OSS 会话和 external job 编排继续留在 `platform-editor-agent` / `api-server`,不得回流已退役的旧 `platform-agent`
- **对话回合免费**(聊天、分析回复不扣泥点),仅 Agent 实际触发生成工具时按对应模型定价扣泥点。
- 工具调用前后端校验泥点余额;不足时该次生成失败并在对话中以明确错误气泡告知,对话本身可继续。
@@ -152,7 +156,7 @@
- `GET/POST /api/editor/projects/{projectId}/agent-conversations`(列表/新建);
- `GET/DELETE /api/editor/agent-conversations/{conversationId}`(详情/软删);
- `POST /api/editor/agent-conversations/{conversationId}/messages`JSON);
- Agent 编排(function-calling 循环、工具内部调既有生成执行链路)放 api-server 编排层,独立文件,不复用 `creative_agent.rs` 内存会话。
- 通用 function-calling harness 放 `platform-agent-harness`;画布 Agent profile 与工具放 `platform-editor-agent`;会话、计费、OSS 和工具内部生成执行链路 api-server 编排层承接,不复用 `creative_agent.rs` 内存会话。
- `shared-contracts` + `packages/shared``editorAgent` 会话、消息、工具确认展示与轻量媒体结果 DTO;消息响应返回 `conversation``deltaMessages` 和可选 `errorMessage`
## 实施顺序
+10
View File
@@ -4018,6 +4018,15 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "platform-agent-harness"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "platform-audio"
version = "0.1.0"
@@ -4056,6 +4065,7 @@ name = "platform-editor-agent"
version = "0.1.0"
dependencies = [
"hmac",
"platform-agent-harness",
"platform-audio",
"platform-image",
"platform-llm",
+2
View File
@@ -38,6 +38,7 @@ members = [
"crates/platform-oss",
"crates/platform-auth",
"crates/platform-audio",
"crates/platform-agent-harness",
"crates/platform-hyper3d",
"crates/platform-image",
"crates/platform-llm",
@@ -67,6 +68,7 @@ module-assets = { path = "crates/module-assets", default-features = false }
module-auth = { path = "crates/module-auth", default-features = false }
module-editor-agent = { path = "crates/module-editor-agent", default-features = false }
module-runtime = { path = "crates/module-runtime", default-features = false }
platform-agent-harness = { path = "crates/platform-agent-harness", default-features = false }
platform-editor-agent = { path = "crates/platform-editor-agent", default-features = false }
platform-auth = { path = "crates/platform-auth", default-features = false }
platform-audio = { path = "crates/platform-audio", default-features = false }
@@ -1,4 +1,3 @@
use std::future::IntoFuture;
use std::time::Duration;
use axum::extract::{Path, State};
@@ -10,7 +9,9 @@ use module_editor_agent::{
use platform_editor_agent::framework::agent_builder::AgentBuilder;
use platform_editor_agent::framework::error::PromptError;
use platform_editor_agent::framework::memory::VecMemory;
use platform_editor_agent::framework::run::{PromptOutput, format_tool_call_message};
use platform_editor_agent::framework::run::{
PromptOutput, PromptRunError, format_tool_call_message,
};
use platform_llm::LlmMessage;
use serde::Serialize;
use serde_json::{Value, json};
@@ -47,6 +48,7 @@ use crate::http_error::AppError;
use crate::request_context::RequestContext;
use crate::state::AppState;
use platform_editor_agent::agent::agent::LlmChatAgentBuilder;
use platform_editor_agent::agent::prompt::{build_prompt_memory, editor_agent_system_prompt};
use platform_editor_agent::agent::tools::context::EditorToolContext;
use platform_editor_agent::agent::tools::edit_image::EditImageTool;
use platform_editor_agent::agent::tools::generate_background_music::GenerateBackgroundMusicTool;
@@ -57,7 +59,7 @@ use platform_editor_agent::agent::tools::generate_sound_effect::GenerateSoundEff
use platform_editor_agent::agent::tools::generate_ui_design::GenerateUiDesignTool;
use platform_editor_agent::agent::tools::generate_video::GenerateVideoTool;
use shared_kernel::{build_prefixed_uuid_id, normalize_optional_string, normalize_required_string};
use tokio::time::{Instant, timeout};
use tokio::time::Instant;
const EDITOR_AGENT_CLIENT_MESSAGE_ID_MAX_CHARS: usize = 128;
const EDITOR_AGENT_PROMPT_TIMEOUT_MS: u64 = 18 * 60_000;
@@ -190,18 +192,9 @@ pub async fn editor_agent_message(
};
// The current user message is passed separately to prompt(), so memory stops before it.
let previous_messages: Vec<LlmMessage> = document.messages[..history_end]
.iter()
.map(|message| match message.role {
EditorAgentMessageRole::User => LlmMessage::user(&message.text),
EditorAgentMessageRole::Assistant => LlmMessage::assistant(&message.text),
EditorAgentMessageRole::System => LlmMessage::system(&message.text),
})
// Tool calls and attachment bookkeeping are separate system messages.
.rev()
.take(18)
.rev()
.collect();
// Tool calls and attachment bookkeeping are separate system messages. The prompt memory also
// appends one bounded latestGeneratedImage context entry for natural-language image references.
let previous_messages = build_prompt_memory(&document, history_end);
// Build tool context from document
let tool_context = context::build_tool_context(&document);
@@ -272,16 +265,19 @@ pub async fn editor_agent_message(
let remaining_prompt_duration =
remaining_editor_agent_prompt_duration(message_started_at.elapsed());
let agent_result = run_editor_agent_prompt_with_timeout(
agent.prompt(LlmMessage::user(user_message.text.clone())),
remaining_prompt_duration,
)
.await;
let agent_result = agent
.prompt(LlmMessage::user(user_message.text.clone()))
.deadline(
tokio::time::sleep(remaining_prompt_duration),
editor_agent_prompt_deadline_error(),
)
.await;
let assistant_now = now_rfc3339();
let (outputs, terminal_error) = split_prompt_result(agent_result);
match build_delta_messages(
agent_result,
outputs,
&assistant_now,
document.messages.len(),
&tool_context,
@@ -293,11 +289,12 @@ pub async fn editor_agent_message(
&conversation,
&mut document,
conversation_summary,
error.to_string(),
error.display_with_agent_label("美术 Agent").to_string(),
)
.await
}
Ok(delta_messages) => {
Ok(mut delta_messages) => {
append_terminal_error(&mut delta_messages, document.messages.len(), terminal_error);
for msg in &delta_messages {
document.messages.push(msg.clone());
}
@@ -316,20 +313,33 @@ fn remaining_editor_agent_prompt_duration(elapsed: Duration) -> Duration {
Duration::from_millis(EDITOR_AGENT_PROMPT_TIMEOUT_MS).saturating_sub(elapsed)
}
async fn run_editor_agent_prompt_with_timeout<F>(
future: F,
duration: Duration,
) -> Result<Vec<PromptOutput>, PromptError>
where
F: IntoFuture<Output = Result<Vec<PromptOutput>, PromptError>>,
{
timeout(duration, future.into_future())
.await
.unwrap_or_else(|_| {
Err(PromptError::CompletionError(
EDITOR_AGENT_PROMPT_TIMEOUT_MESSAGE.to_string(),
))
})
fn editor_agent_prompt_deadline_error() -> PromptError {
PromptError::CompletionError(EDITOR_AGENT_PROMPT_TIMEOUT_MESSAGE.to_string())
}
fn split_prompt_result(
result: Result<Vec<PromptOutput>, PromptRunError>,
) -> (Vec<PromptOutput>, Option<PromptError>) {
match result {
Ok(outputs) => (outputs, None),
Err(error) => {
let (terminal_error, partial_outputs) = error.into_parts();
(partial_outputs, Some(terminal_error))
}
}
}
fn append_terminal_error(
delta_messages: &mut Vec<EditorAgentMessage>,
messages_offset: usize,
terminal_error: Option<PromptError>,
) {
if let Some(error) = terminal_error {
delta_messages.push(build_editor_agent_error_message(
messages_offset + delta_messages.len(),
error.display_with_agent_label("美术 Agent"),
));
}
}
fn build_editor_agent_error_message(
@@ -564,14 +574,14 @@ mod tests {
crate::editor_generation_config::load_editor_generation_pricing_from_paths(None)
.expect("default editor pricing should load");
let messages = build_delta_messages(
Ok(vec![PromptOutput::Tool(ToolCallOutput {
vec![PromptOutput::Tool(ToolCallOutput {
tool_call: ToolCall {
id: "tool-call-1".to_string(),
name: tool_name.to_string(),
args: json!({ "prompt": "轻快冒险音乐" }),
},
output: output.clone(),
})]),
})],
"2026-07-23T00:00:00Z",
0,
&EditorToolContext::default(),
@@ -598,7 +608,7 @@ mod tests {
crate::editor_generation_config::load_editor_generation_pricing_from_paths(None)
.expect("default editor pricing should load");
let messages = build_delta_messages(
Ok(vec![PromptOutput::Tool(ToolCallOutput {
vec![PromptOutput::Tool(ToolCallOutput {
tool_call: ToolCall {
id: "tool-call-1".to_string(),
name: GenerateVideoTool::NAME.to_string(),
@@ -611,7 +621,7 @@ mod tests {
}),
},
output: json!({ "message": "runner pending output" }),
})]),
})],
"2026-07-23T00:00:00Z",
0,
&EditorToolContext::default(),
@@ -642,15 +652,8 @@ mod tests {
assert_eq!(display_value("sound"), Some("on"));
}
#[tokio::test]
async fn prompt_timeout_applies_to_the_whole_agent_run() {
let error = run_editor_agent_prompt_with_timeout(
std::future::pending::<Result<Vec<PromptOutput>, PromptError>>(),
Duration::from_millis(1),
)
.await
.expect_err("pending agent run should hit the prompt deadline");
#[test]
fn prompt_deadline_applies_to_the_whole_agent_run() {
assert_eq!(EDITOR_AGENT_PROMPT_TIMEOUT_MS, 1_080_000);
assert_eq!(
remaining_editor_agent_prompt_duration(Duration::from_secs(17 * 60)),
@@ -661,37 +664,64 @@ mod tests {
Duration::ZERO
);
assert_eq!(
error.to_string(),
editor_agent_prompt_deadline_error()
.display_with_agent_label("美术 Agent")
.to_string(),
"美术 Agent 规划失败:规划总时长已达到 18 分钟安全上限"
);
}
}
fn editor_agent_system_prompt() -> &'static str {
r#"
* image_id str format is like: sha256:*
* when user referenced/uploaded image, a system message will notify you the image id(s).
YOU MUST USE THESE IMAGE IDs(or more from former context) IN YOUR TOOL CALLS.(or why user upload them?)
* to confirm a pending tool call, user should click a confirm button in their UI, instead of tell you "ok"/"confirm".
If in that case, you should tip the user to use the confirm button, instead of repeat that pending tool call.
* 用户所说的 规范图/参考图/生成的图/... 没有本质区别,all can be some image_id
* 实际生成工具由后端按模型定价扣泥点, 不能承诺免费生成
#[test]
fn terminal_failure_keeps_partial_outputs_for_delta_persistence() {
let result = Err(PromptRunError::new(
PromptError::MaxTurnsReached { max_turns: 3 },
vec![PromptOutput::Tool(ToolCallOutput {
tool_call: ToolCall {
id: "0".to_string(),
name: GenerateImageTool::NAME.to_string(),
args: json!({
"prompt": "一座漂浮在云海上的城堡",
"reference_image_ids": []
}),
},
output: json!({ "message": "等待用户确认" }),
})],
));
你是 Genarrative 图片画布 Agent,负责帮助用户理解、规划和触发画布生成工具. 对话回复要简短.
"#
}
let (outputs, terminal_error) = split_prompt_result(result);
let mut delta_messages = build_delta_messages(
outputs,
"2026-07-28T00:00:00Z",
4,
&EditorToolContext::default(),
&EditorGenerationPricingConfig {
models: Default::default(),
},
)
.expect("successful partial tool output should still build a confirmation card");
append_terminal_error(&mut delta_messages, 4, terminal_error);
assert_eq!(delta_messages.len(), 2);
assert_eq!(delta_messages[0].id, 4);
assert!(delta_messages[0].tool_call.is_some());
assert_eq!(delta_messages[1].id, 5);
assert_eq!(delta_messages[1].role, EditorAgentMessageRole::System);
assert_eq!(
delta_messages[1].text,
"ERROR 美术 Agent 规划轮数已达上限:3"
);
}
}
fn build_delta_messages(
result: Result<Vec<PromptOutput>, PromptError>,
outputs: Vec<PromptOutput>,
created_at: &str,
messages_offset: usize,
tool_context: &EditorToolContext,
pricing: &EditorGenerationPricingConfig,
) -> Result<Vec<EditorAgentMessage>, PromptError> {
let outputs = result?;
let mut messages = Vec::with_capacity(outputs.len());
for (i, out) in outputs.into_iter().enumerate() {
let absolute_idx = messages_offset + i;
for out in outputs {
let absolute_idx = messages_offset + messages.len();
match out {
PromptOutput::Text(text) => {
messages.push(EditorAgentMessage {
@@ -741,6 +771,10 @@ fn build_delta_messages(
created_at: created_at.to_string(),
});
}
// Tool failures are retained by the shared harness for callers that need structured
// retry/diagnostic policy. The editor surface must not render them as confirmation
// cards; a terminal failure is appended below as the existing ERROR system message.
PromptOutput::ToolFailed(_) => {}
}
}
@@ -0,0 +1,12 @@
[package]
name = "platform-agent-harness"
edition.workspace = true
version.workspace = true
license.workspace = true
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt", "time"] }
@@ -1,8 +1,8 @@
use crate::framework::error::PromptError;
use crate::framework::hook::Hook;
use crate::framework::memory::AgentMemory;
use crate::framework::run::PromptRequest;
use crate::framework::tool::{Tool, ToolDyn};
use crate::error::PromptError;
use crate::hook::Hook;
use crate::memory::AgentMemory;
use crate::run::PromptRequest;
use crate::tool::{Tool, ToolDyn};
pub struct Agent<M: LlmApiAdaptor<Message>, Message> {
pub model: M,
@@ -76,4 +76,8 @@ pub trait LlmApiAdaptor<Message>: Send + Sync {
fn tool_result_message(&self, tool_name: &str, output: &str) -> Message;
fn build_assistant_message(&self, text: &str) -> Message;
fn build_error_message(&self, error: &PromptError) -> Message {
self.tool_result_message("agent-error", &error.to_string())
}
}
@@ -1,7 +1,7 @@
use crate::framework::agent::{Agent, LlmApiAdaptor};
use crate::framework::hook::Hook;
use crate::framework::memory::AgentMemory;
use crate::framework::tool::Tool;
use crate::agent::{Agent, LlmApiAdaptor};
use crate::hook::Hook;
use crate::memory::AgentMemory;
use crate::tool::Tool;
pub trait AgentBuilder<Message, M: LlmApiAdaptor<Message>> {
type Client;
@@ -0,0 +1,100 @@
#[derive(Debug, Clone)]
pub enum PromptError {
CompletionError(String),
ToolError(String),
InternalError(String),
MaxTurnsReached { max_turns: usize },
}
impl PromptError {
/// 使用业务入口自己的 Agent 名称展示错误,同时保持 harness 默认文案中性。
pub fn display_with_agent_label<'a>(&'a self, agent_label: &'a str) -> PromptErrorDisplay<'a> {
PromptErrorDisplay {
error: self,
agent_label,
}
}
fn fmt_with_agent_label(
&self,
formatter: &mut std::fmt::Formatter<'_>,
agent_label: &str,
) -> std::fmt::Result {
match self {
Self::CompletionError(message) => {
write!(formatter, "{agent_label} 规划失败:{message}")
}
Self::ToolError(message) => {
write!(formatter, "{agent_label} 工具执行失败:{message}")
}
Self::InternalError(message) => {
write!(formatter, "{agent_label} 内部错误:{message}")
}
Self::MaxTurnsReached { max_turns } => {
write!(formatter, "{agent_label} 规划轮数已达上限:{max_turns}")
}
}
}
}
pub struct PromptErrorDisplay<'a> {
error: &'a PromptError,
agent_label: &'a str,
}
impl std::fmt::Display for PromptErrorDisplay<'_> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.error.fmt_with_agent_label(formatter, self.agent_label)
}
}
impl std::fmt::Display for PromptError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.fmt_with_agent_label(formatter, "Agent")
}
}
impl std::error::Error for PromptError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_display_uses_neutral_agent_label() {
let error = PromptError::CompletionError("LLM 请求超时,累计尝试 2 次".to_string());
assert_eq!(
error.to_string(),
"Agent 规划失败:LLM 请求超时,累计尝试 2 次"
);
}
#[test]
fn caller_can_supply_a_surface_specific_agent_label() {
assert_eq!(
PromptError::CompletionError("LLM 请求超时".to_string())
.display_with_agent_label("美术 Agent")
.to_string(),
"美术 Agent 规划失败:LLM 请求超时"
);
assert_eq!(
PromptError::ToolError("参数无效".to_string())
.display_with_agent_label("美术 Agent")
.to_string(),
"美术 Agent 工具执行失败:参数无效"
);
assert_eq!(
PromptError::InternalError("序列化失败".to_string())
.display_with_agent_label("美术 Agent")
.to_string(),
"美术 Agent 内部错误:序列化失败"
);
assert_eq!(
PromptError::MaxTurnsReached { max_turns: 3 }
.display_with_agent_label("美术 Agent")
.to_string(),
"美术 Agent 规划轮数已达上限:3"
);
}
}
@@ -1,5 +1,5 @@
use crate::framework::run::{TextFlow, ToolCallFlow};
use crate::framework::tool::ToolCall;
use crate::run::{TextFlow, ToolCallFlow};
use crate::tool::ToolCall;
pub trait Hook: Send + Sync {
fn on_text_reply(&self, _text: &str) -> TextFlow {
@@ -0,0 +1,10 @@
//! 与具体业务 Agent、模型和界面无关的 function-calling harness。
pub mod agent;
pub mod agent_builder;
pub mod error;
pub mod hook;
pub mod memory;
pub mod prompt;
pub mod run;
pub mod tool;
@@ -0,0 +1,57 @@
//! Agent 对话记忆的最小公共抽象。
use serde::{Deserialize, Serialize};
pub trait AgentMemoryBuffer<Message>: Send + Sync {
fn get_memory(&self) -> &[Message];
fn append_message(&mut self, message: Message);
}
pub trait AgentMemory<Message>: AgentMemoryBuffer<Message> {
/// 创建与当前 memory 行为等价、但写入彼此隔离的本轮事务副本。
fn begin_staged(&self) -> Box<dyn StagedAgentMemory<Message>>;
}
pub trait StagedAgentMemory<Message>: AgentMemoryBuffer<Message> {
/// 显式提交本轮事务,并返回下一轮继续使用的 committed memory。
///
/// 持久化实现应在这里发布 staged 写入;直接 drop staged transaction 表示回滚。
fn commit(self: Box<Self>) -> Box<dyn AgentMemory<Message>>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VecMemory<Message> {
messages: Vec<Message>,
}
impl<Message> VecMemory<Message> {
pub fn new(messages: Vec<Message>) -> Self {
Self { messages }
}
pub fn into_inner(self) -> Vec<Message> {
self.messages
}
}
impl<Message: Send + Sync> AgentMemoryBuffer<Message> for VecMemory<Message> {
fn get_memory(&self) -> &[Message] {
&self.messages
}
fn append_message(&mut self, message: Message) {
self.messages.push(message);
}
}
impl<Message: Clone + Send + Sync + 'static> AgentMemory<Message> for VecMemory<Message> {
fn begin_staged(&self) -> Box<dyn StagedAgentMemory<Message>> {
Box::new(self.clone())
}
}
impl<Message: Clone + Send + Sync + 'static> StagedAgentMemory<Message> for VecMemory<Message> {
fn commit(self: Box<Self>) -> Box<dyn AgentMemory<Message>> {
self
}
}
@@ -0,0 +1,105 @@
use serde_json::Value;
pub const PENDING_USER_CONFIRMATION_MESSAGE: &str =
"tool call is pending user confirmation; end this turn when all tool calls are pending";
#[derive(Debug, Clone, PartialEq)]
pub struct ToolPromptSpec {
pub name: String,
pub description: String,
pub parameters: Value,
}
/// 把业务 system prompt、统一 JSON function-calling 输出契约和工具 schema 组合起来。
pub fn build_tools_system_prompt(base_prompt: &str, tool_specs: &[ToolPromptSpec]) -> String {
let mut prompt = String::new();
prompt.push_str(base_prompt);
prompt.push_str("\n\nYou have access to the following tools.\n\n");
prompt.push_str("## JSON Response Format\n");
prompt.push_str("respond with valid JSON only (no markdown fences):\n");
prompt.push_str("{\n");
prompt.push_str(" \"reply_text\": \"your message to the user\",\n");
prompt.push_str(" \"tool_calls\": [\n {\n");
prompt.push_str(" \"tool_name\": \"tool_name_here\",\n");
prompt.push_str(" \"args\": { \"argument_name\": \"argument_value\" }\n");
prompt.push_str(" }\n ]\n");
prompt.push_str("}\n\n");
prompt.push_str("If you don't need to use a tool, respond with:\n");
prompt.push_str("{\n");
prompt.push_str(" \"reply_text\": \"your message\",\n");
prompt.push_str(" \"tool_calls\": []\n");
prompt.push_str("}\n\n");
prompt.push_str("## Available Tools\n\n");
if tool_specs.is_empty() {
prompt.push_str("(No tools available.)\n");
} else {
for tool in tool_specs {
prompt.push_str(&format!("- {}\n", tool.name));
prompt.push_str(&format!(" Description: {}\n", tool.description));
prompt.push_str(" Arguments JSON Schema:\n");
let parameters = serde_json::to_string_pretty(&tool.parameters)
.unwrap_or_else(|_| tool.parameters.to_string());
prompt.push_str(&parameters);
prompt.push('\n');
}
prompt.push_str("tool_calls can contain multiple calls in one turn. ");
prompt.push_str("Calls are executed sequentially in array order. ");
prompt.push_str("Valid tool calls are recorded as system messages. ");
prompt.push_str(
"Some tools require user confirmation. Do not retry the same tool call while it is pending. \
If all tool calls are pending confirmation, end the turn and wait for the user's action.",
);
}
prompt
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn tool_prompt_keeps_the_shared_json_and_confirmation_contract() {
let prompt = build_tools_system_prompt(
"业务提示词",
&[ToolPromptSpec {
name: "generate-image".to_string(),
description: "生成图片".to_string(),
parameters: json!({
"type": "object",
"properties": { "prompt": { "type": "string" } },
"required": ["prompt"]
}),
}],
);
assert!(prompt.starts_with("业务提示词"));
assert!(prompt.contains("\"reply_text\""));
assert!(prompt.contains("\"tool_calls\""));
assert!(prompt.contains("- generate-image"));
assert!(prompt.contains("\"required\": ["));
assert!(prompt.contains("Do not retry the same tool call"));
assert!(prompt.contains("all tool calls are pending confirmation"));
assert!(prompt.contains("executed sequentially in array order"));
assert!(!prompt.contains("concurrently"));
}
#[test]
fn tool_prompt_preserves_the_no_tools_shape() {
let prompt = build_tools_system_prompt("基础提示词", &[]);
assert!(prompt.starts_with("基础提示词"));
assert!(prompt.contains("## JSON Response Format"));
assert!(prompt.contains("\"tool_calls\": []"));
assert!(prompt.ends_with("## Available Tools\n\n(No tools available.)\n"));
}
#[test]
fn pending_confirmation_message_is_shared_control_flow_copy() {
assert!(PENDING_USER_CONFIRMATION_MESSAGE.contains("pending user confirmation"));
assert!(PENDING_USER_CONFIRMATION_MESSAGE.contains("end this turn"));
}
}
File diff suppressed because it is too large Load Diff
@@ -1,3 +1,5 @@
//! Function-calling 工具及其结构化执行结果的公共契约。
use serde::{Deserialize, Serialize};
use std::pin::Pin;
@@ -6,6 +6,7 @@ license.workspace = true
[dependencies]
hmac = { workspace = true }
platform-agent-harness = { workspace = true }
platform-llm = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
@@ -4,8 +4,8 @@ use crate::framework::error::PromptError;
use crate::framework::hook::Hook;
use crate::framework::memory::AgentMemory;
use crate::framework::tool::{Tool, ToolDyn};
use platform_agent_harness::prompt::{ToolPromptSpec, build_tools_system_prompt};
use platform_llm::{EDITOR_AGENT_GPT5_MODEL, LlmClient, LlmMessage, LlmTextRequest};
use serde_json::Value;
const EDITOR_AGENT_LLM_MAX_OUTPUT_TOKENS: u32 = 1024;
const EDITOR_AGENT_LLM_HARD_REQUEST_TIMEOUT_MS: u64 = 480_000;
@@ -35,6 +35,13 @@ impl LlmApiAdaptor<LlmMessage> for LlmCompletionModel {
fn build_assistant_message(&self, text: &str) -> LlmMessage {
LlmMessage::assistant(text)
}
fn build_error_message(&self, error: &PromptError) -> LlmMessage {
LlmMessage::system(format!(
"ERROR {}",
error.display_with_agent_label("美术 Agent")
))
}
}
fn build_editor_agent_llm_request(messages: Vec<LlmMessage>) -> LlmTextRequest {
@@ -122,59 +129,6 @@ impl AgentBuilder<LlmMessage, LlmCompletionModel> for LlmChatAgentBuilder {
}
}
struct ToolPromptSpec {
name: String,
description: String,
parameters: Value,
}
fn build_tools_system_prompt(base_prompt: &str, tool_specs: &[ToolPromptSpec]) -> String {
let mut prompt = String::new();
prompt.push_str(base_prompt);
prompt.push_str("\n\nYou have access to the following tools.\n\n");
if tool_specs.is_empty() {
prompt.push_str("(No tools available.)\n");
} else {
prompt.push_str("## JSON Response Format\n");
prompt.push_str("respond with valid JSON only (no markdown fences):\n");
prompt.push_str("{\n");
prompt.push_str(" \"reply_text\": \"your message to the user\",\n");
prompt.push_str(" \"tool_calls\": [\n {\n");
prompt.push_str(" \"tool_name\": \"tool_name_here\",\n");
prompt.push_str(" \"args\": { \"argument_name\": \"argument_value\" }\n");
prompt.push_str(" }\n ]\n");
prompt.push_str("}\n\n");
prompt.push_str("If you don't need to use a tool, respond with:\n");
prompt.push_str("{\n");
prompt.push_str(" \"reply_text\": \"your message\",\n");
prompt.push_str(" \"tool_calls\": []\n");
prompt.push_str("}\n\n");
prompt.push_str("## Available Tools\n\n");
for tool in tool_specs {
prompt.push_str(&format!("- {}\n", tool.name));
prompt.push_str(&format!(" Description: {}\n", tool.description));
prompt.push_str(" Arguments JSON Schema:\n");
let parameters = serde_json::to_string_pretty(&tool.parameters)
.unwrap_or_else(|_| tool.parameters.to_string());
prompt.push_str(&parameters);
prompt.push_str("\n");
}
prompt.push_str(
"as you see, tool_calls is an array, several tools calls can be executed in one turn concurrently. ",
);
prompt.push_str("your valid tool call will be recorded as system message");
prompt.push_str(
"some tools calls needs user's confirmation, you should not retry the same tool call in this case.\
And if all tool calls are pending confirmation, you should just end the turn, as you cant do more before user's action"
);
}
prompt
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1,3 +1,4 @@
pub mod agent;
pub mod asset;
pub mod prompt;
pub mod tools;
@@ -0,0 +1,238 @@
use crate::agent::asset::ImageId;
pub use platform_agent_harness::prompt::PENDING_USER_CONFIRMATION_MESSAGE;
use platform_llm::LlmMessage;
use serde_json::json;
use shared_contracts::editor_agent::{
EditorAgentConversationMessagesDocument, EditorAgentGeneratedImage, EditorAgentMessage,
EditorAgentMessageRole, EditorAgentToolCallStatus,
};
const EDITOR_AGENT_MAX_RECENT_PROMPT_MESSAGES: usize = 18;
const EXISTING_IMAGE_EDIT_POLICY: &str = "已有图片的修改必须选择 edit-image,不得退回全新生成";
const SPEC_BOARD_ROUTE_POLICY: &str =
"规范图、视觉规范图、风格规范图或素材规范展板必须选择 generate-image";
const SPEC_BOARD_CONTENT_POLICY: &str = "规范展板 prompt 必须写明统一视角、线条粗细、描边、填充风格、材质、阴影、圆角、状态层级、色卡或色号、尺寸标注和排版层级";
pub fn editor_agent_system_prompt() -> &'static str {
r#"
* image_id 字符串格式为 sha256:*。
* 用户引用或上传图片时,system message 会提供对应 image_id;规划相关工具调用时必须使用这些 image_id 或上下文中已有的 image_id。
* 待确认工具必须由用户在界面点击确认按钮执行。用户只在对话中回复“确认”或“可以”时,应提示其点击确认按钮,不得重复提交同一待确认工具。
* 用户所说的规范图、参考图和已生成图片都可以作为 image_id 图片上下文。
* 实际生成工具由后端按模型定价扣泥点,不能承诺免费生成。
你是 Genarrative 图片画布 Agent,只负责帮助用户理解、规划和触发画布生成工具。对话回复要简短。
"#
}
pub fn build_prompt_memory(
document: &EditorAgentConversationMessagesDocument,
history_end: usize,
) -> Vec<LlmMessage> {
let history = &document.messages[..history_end.min(document.messages.len())];
let mut messages = history
.iter()
.map(|message| match message.role {
EditorAgentMessageRole::User => LlmMessage::user(&message.text),
EditorAgentMessageRole::Assistant => LlmMessage::assistant(&message.text),
EditorAgentMessageRole::System => LlmMessage::system(&message.text),
})
.rev()
.take(EDITOR_AGENT_MAX_RECENT_PROMPT_MESSAGES)
.rev()
.collect::<Vec<_>>();
if let Some(latest_generated_image) = build_latest_generated_image_prompt_context(history) {
messages.push(LlmMessage::system(latest_generated_image));
}
messages
}
fn build_latest_generated_image_prompt_context(messages: &[EditorAgentMessage]) -> Option<String> {
let (tool_name, image) = messages.iter().rev().find_map(|message| {
let tool_call = message.tool_call.as_ref()?;
if tool_call.status != EditorAgentToolCallStatus::Completed {
return None;
}
Some((tool_call.tool_name.as_str(), tool_call.images.first()?))
})?;
let image_id = ImageId::from_data_key(generated_image_data_key(image));
let context = json!({
"toolName": tool_name,
"imageId": image_id,
"resourceId": image.resource_id.as_deref(),
"objectKey": image.object_key.as_deref(),
"assetObjectId": image.asset_object_id.as_deref(),
});
Some(format!(
"latestGeneratedImage: {context}\n用户指代“这张”“刚才那个”或“上一张”时,使用 imageId 作为 edit-image 的 object_image_id。"
))
}
fn generated_image_data_key(image: &EditorAgentGeneratedImage) -> String {
image
.object_key
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| value.trim_start_matches('/').to_string())
.unwrap_or_else(|| image.image_src.clone())
}
pub fn edit_image_tool_description() -> String {
format!(
"仅用于修改已有图片:换衣服、改颜色、替换背景、局部重绘,或保持主体、构图、姿势不变的编辑。用户指代“这张”“刚才那个”“上一张”“改成”“换成”时优先使用本工具。{EXISTING_IMAGE_EDIT_POLICY};必须使用已有图片上下文,没有参考图时不要调用,应提示用户先选择参考图。"
)
}
pub fn generate_image_tool_description() -> String {
format!(
"用于从文字生成全新图片:新场景、新物体、新插画或新背景;也专用于规范图、视觉规范图、风格规范图、素材规范展板。不要用来修改已有图,{EXISTING_IMAGE_EDIT_POLICY}{SPEC_BOARD_ROUTE_POLICY}{SPEC_BOARD_CONTENT_POLICY};角色规范展板还要含头身比例、标准立绘、动作帧样例、服饰配饰分层和专属色卡。完整 prompt 必须包含画面、主体、风格、构图和背景。"
)
}
pub fn generate_character_tool_description() -> String {
format!(
"仅用于生成新的角色形象、人物立绘或普通角色设定图。{EXISTING_IMAGE_EDIT_POLICY};角色规范图、角色美术视觉规范设定图或规范展板属于规范展板,{SPEC_BOARD_ROUTE_POLICY}"
)
}
pub fn generate_icon_spritesheet_tool_description() -> String {
format!(
"仅用于生成多个图标成品、图标素材图集或 spritesheet。必须提供图标规范或风格参考图,并填写多个 icon_descriptions;没有参考图时不要调用,应提示用户先选择参考图。图标规范图或图标视觉规范展板属于规范展板,{SPEC_BOARD_ROUTE_POLICY}"
)
}
pub fn generate_ui_design_tool_description() -> String {
format!(
"仅用于生成完整可用的 UI 设计图或界面稿,包括 HUD、弹窗、面板、按钮组合和整页界面。不要用于提取图标、拆素材;{EXISTING_IMAGE_EDIT_POLICY}。UI 规范图、组件规范展板或视觉规范展板属于规范展板,{SPEC_BOARD_ROUTE_POLICY}"
)
}
#[cfg(test)]
mod tests {
use super::*;
use shared_contracts::editor_agent::{
EDITOR_AGENT_MESSAGES_DOCUMENT_VERSION, EditorAgentToolCall, EditorAgentToolCallDisplayArgs,
};
fn message(
id: usize,
role: EditorAgentMessageRole,
text: &str,
tool_call: Option<EditorAgentToolCall>,
) -> EditorAgentMessage {
EditorAgentMessage {
id,
client_message_id: None,
role,
text: text.to_string(),
attachments: Vec::new(),
tool_call,
created_at: "2026-07-28T00:00:00Z".to_string(),
}
}
#[test]
fn shared_tool_policy_keeps_spec_and_edit_routes_consistent() {
let generate_image = generate_image_tool_description();
let edit_image = edit_image_tool_description();
let character = generate_character_tool_description();
let icons = generate_icon_spritesheet_tool_description();
let ui = generate_ui_design_tool_description();
assert!(generate_image.contains(SPEC_BOARD_CONTENT_POLICY));
assert!(edit_image.contains("上一张"));
assert!(character.contains(SPEC_BOARD_ROUTE_POLICY));
assert!(icons.contains(SPEC_BOARD_ROUTE_POLICY));
assert!(ui.contains(SPEC_BOARD_ROUTE_POLICY));
for description in [&generate_image, &edit_image, &character, &ui] {
assert!(description.contains(EXISTING_IMAGE_EDIT_POLICY));
}
}
#[test]
fn shared_pending_message_is_control_flow_not_tool_specific_copy() {
assert!(PENDING_USER_CONFIRMATION_MESSAGE.contains("pending user confirmation"));
assert!(PENDING_USER_CONFIRMATION_MESSAGE.contains("end this turn"));
}
#[test]
fn prompt_memory_injects_the_latest_completed_generated_image() {
let generated = EditorAgentToolCall {
tool_name: "generate-image".to_string(),
status: EditorAgentToolCallStatus::Completed,
args: json!({ "prompt": "一只橙色小猫" }),
display_args: EditorAgentToolCallDisplayArgs::default(),
external_job_id: Some("job-1".to_string()),
images: vec![EditorAgentGeneratedImage {
resource_id: Some("resource-1".to_string()),
object_key: Some("generated/editor/cat.png".to_string()),
asset_object_id: Some("asset-object-1".to_string()),
image_src: "/generated/editor/cat.png".to_string(),
thumbnail_src: None,
width: Some(1024),
height: Some(1024),
}],
videos: Vec::new(),
audios: Vec::new(),
error: None,
};
let document = EditorAgentConversationMessagesDocument {
version: EDITOR_AGENT_MESSAGES_DOCUMENT_VERSION,
conversation_id: "conversation-1".to_string(),
messages: vec![
message(
0,
EditorAgentMessageRole::System,
"tool result",
Some(generated),
),
message(1, EditorAgentMessageRole::User, "把上一张改成蓝色", None),
],
};
let memory = build_prompt_memory(&document, 1);
let latest_context = &memory.last().expect("latest image context").content;
assert!(latest_context.starts_with("latestGeneratedImage:"));
assert!(latest_context.contains("\"toolName\":\"generate-image\""));
assert!(latest_context.contains("\"resourceId\":\"resource-1\""));
assert!(latest_context.contains("\"objectKey\":\"generated/editor/cat.png\""));
assert!(latest_context.contains("\"imageId\":\"sha256:"));
assert!(latest_context.contains("edit-image 的 object_image_id"));
assert!(!latest_context.contains("source_image_id"));
assert!(!latest_context.contains("https://"));
}
#[test]
fn prompt_memory_ignores_unfinished_tool_results() {
let pending = EditorAgentToolCall {
tool_name: "generate-image".to_string(),
status: EditorAgentToolCallStatus::NotCompleted,
args: json!({}),
display_args: EditorAgentToolCallDisplayArgs::default(),
external_job_id: None,
images: Vec::new(),
videos: Vec::new(),
audios: Vec::new(),
error: None,
};
let document = EditorAgentConversationMessagesDocument {
version: EDITOR_AGENT_MESSAGES_DOCUMENT_VERSION,
conversation_id: "conversation-1".to_string(),
messages: vec![message(
0,
EditorAgentMessageRole::System,
"pending",
Some(pending),
)],
};
let memory = build_prompt_memory(&document, document.messages.len());
assert_eq!(memory.len(), 1);
assert!(!memory[0].content.contains("latestGeneratedImage"));
}
}
@@ -1 +0,0 @@
pub const TOOL_CALL_PENDING_MESSAGE: &'static str = "this tool call is pending user confirmation. if all tool calls are pending, just end this turn";
@@ -1,5 +1,5 @@
use crate::agent::asset::ImageId;
use crate::agent::tools::common::TOOL_CALL_PENDING_MESSAGE;
use crate::agent::prompt::{PENDING_USER_CONFIRMATION_MESSAGE, edit_image_tool_description};
use crate::agent::tools::context::EditorToolContext;
use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind};
use platform_image::GPT_IMAGE_2_MODEL;
@@ -67,8 +67,7 @@ impl Tool for EditImageTool {
type Output = EditImageToolOutput;
fn description(&self) -> String {
"仅用于修改已有图片:换衣服、改颜色、替换背景、局部重绘,或保持主体、构图、姿势不变的编辑。用户指代“这张”“刚才那个”“上一张”“改成”“换成”时优先使用本工具。必须使用已有图片上下文;没有参考图时不要调用,应提示用户先选择参考图。"
.to_string()
edit_image_tool_description()
}
fn parameters(&self) -> Value {
@@ -110,7 +109,7 @@ impl Tool for EditImageTool {
return Err(error);
}
Ok(EditImageToolOutput {
message: TOOL_CALL_PENDING_MESSAGE.to_string(),
message: PENDING_USER_CONFIRMATION_MESSAGE.to_string(),
})
}
}
@@ -1,4 +1,4 @@
use crate::agent::tools::common::TOOL_CALL_PENDING_MESSAGE;
use crate::agent::prompt::PENDING_USER_CONFIRMATION_MESSAGE;
use crate::framework::tool::{Tool, ToolFailure};
use platform_audio::SUNO_DEFAULT_MODEL;
use serde::{Deserialize, Serialize};
@@ -65,7 +65,7 @@ impl Tool for GenerateBackgroundMusicTool {
async move {
self.validate_args(&args)?;
Ok(GenerateBackgroundMusicToolOutput {
message: TOOL_CALL_PENDING_MESSAGE.to_string(),
message: PENDING_USER_CONFIRMATION_MESSAGE.to_string(),
})
}
}

Some files were not shown because too many files have changed in this diff Show More