完善单Agent流式聊天与原生工具规划

修复 OpenAI SSE 空 choices、异常尾包和读取中断导致完整回复丢失
固定聊天记录区域并增加持续等待状态与内部滚动
接入原生 function tool 计划协议并补齐严格校验和回归测试
更新 AI 游戏创作技术方案与决策记录
This commit is contained in:
AIGameCreator App
2026-07-12 02:32:22 +08:00
parent 44ac45cd5b
commit 65f2632198
10 changed files with 1583 additions and 136 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+24 -12
View File
@@ -4404,19 +4404,25 @@ export function WorkspaceLauncher({
},
);
} catch (error) {
if (pendingStreamDraftText) {
if (pendingStreamDraftText && pendingStreamFinishReason) {
reply = { replyText: pendingStreamDraftText };
setAgentChatStatus(
`Agent 回复已结束(${pendingStreamFinishReason}),正在保存已接收内容`,
);
} else if (pendingStreamDraftText) {
throw error;
} else {
setAgentChatStatus('流式连接失败,正在切换普通回复模式');
reply = await invoke<GameCreatorChatAgentReply>(
'chat_with_game_creator_role_agent',
{
projectPath: projectPathForChat,
agentId: agent.id,
prompt: content,
...agentChatSessionInvokeArgs(sessionIdForChat),
},
);
}
setAgentChatStatus('流式连接失败,正在切换普通回复模式');
reply = await invoke<GameCreatorChatAgentReply>(
'chat_with_game_creator_role_agent',
{
projectPath: projectPathForChat,
agentId: agent.id,
prompt: content,
...agentChatSessionInvokeArgs(sessionIdForChat),
},
);
}
} else {
reply = await invoke<GameCreatorChatAgentReply>(
@@ -5800,7 +5806,9 @@ export function WorkspaceLauncher({
<div
ref={agentChatMessagesRef}
className="launcher-agent-chat-messages"
role="log"
aria-label="Agent 聊天记录"
tabIndex={0}
>
{agentChatMessages.length > 0 ? (
agentChatMessages.map((message, index) => (
@@ -5819,9 +5827,13 @@ export function WorkspaceLauncher({
className="launcher-agent-chat-waiting"
role="status"
aria-live="polite"
aria-atomic="true"
>
<span aria-hidden="true" />
<strong>{agentChatStatus}</strong>
<div>
<strong>{agentChatStatus}</strong>
<small></small>
</div>
</div>
) : null}
</div>
+21 -1
View File
@@ -1060,6 +1060,7 @@ textarea {
.launcher-agent-chat-layout {
display: grid;
grid-template-columns: minmax(220px, 0.3fr) minmax(0, 1fr);
align-items: start;
gap: 12px;
width: 100%;
min-width: 0;
@@ -1150,6 +1151,8 @@ textarea {
.launcher-agent-chat-main {
display: grid;
grid-template-rows: repeat(6, auto);
align-content: start;
width: 100%;
overflow: visible;
}
@@ -1259,12 +1262,15 @@ textarea {
display: grid;
align-content: start;
gap: 10px;
height: clamp(280px, 44vh, 420px);
height: clamp(260px, 40vh, 380px);
max-height: 380px;
min-height: 0;
padding: 14px;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
.launcher-agent-chat-messages .message {
@@ -1303,17 +1309,31 @@ textarea {
}
.launcher-agent-chat-waiting strong {
display: block;
min-width: 0;
font-size: 12px;
overflow-wrap: anywhere;
}
.launcher-agent-chat-waiting small {
display: block;
margin-top: 2px;
color: #6b7280;
font-size: 11px;
}
@keyframes launcher-agent-chat-spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.launcher-agent-chat-waiting > span {
animation: none;
}
}
.launcher-agent-chat-composer {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
@@ -968,7 +968,7 @@ describe('AI 游戏创作 App 界面边界', () => {
).not.toBeNull();
});
it('opens the developer agent chat entry and persists selected agent messages', async () => {
it('keeps and persists a completed streamed reply when its tail event fails', async () => {
const persistedMessages: Array<{
role: 'user' | 'assistant' | 'tool';
content: string;
@@ -980,6 +980,10 @@ describe('AI 游戏创作 App 界面边界', () => {
agentId: null,
},
];
const streamedReply = '流式正文已经完整结束。';
let streamHandler:
| ((event: { payload: Record<string, unknown> }) => void)
| null = null;
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_local_conversation') {
@@ -994,12 +998,21 @@ describe('AI 游戏创作 App 界面边界', () => {
};
}
if (command === 'chat_with_game_creator_role_agent_stream') {
streamHandler?.({
payload: {
projectPath: args?.projectPath,
agentId: args?.agentId,
runId: args?.runId,
status: 'delta',
deltaText: streamedReply,
accumulatedText: streamedReply,
finishReason: 'stop',
},
});
throw new Error('LLM SSE 响应缺少 choices[0]');
}
if (command === 'chat_with_game_creator_role_agent') {
return {
replyText: mockRoleAgentReply(),
};
throw new Error('completed stream must not fall back to another request');
}
if (command === 'append_local_conversation_message') {
const message = args?.message as {
@@ -1021,7 +1034,21 @@ describe('AI 游戏创作 App 界面边界', () => {
throw new Error(`unexpected invoke ${command}`);
},
);
const listen = vi.fn(async () => () => {});
const listen = vi.fn(
async (
eventName: string,
handler: (event: { payload: Record<string, unknown> }) => void,
) => {
if (eventName === 'game-creator-role-agent-chat-stream') {
streamHandler = handler;
}
return () => {
if (streamHandler === handler) {
streamHandler = null;
}
};
},
);
window.__TAURI__ = { core: { invoke }, event: { listen } };
renderLauncherAgentChatAt('/?agent-chat');
@@ -1046,7 +1073,7 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(
await screen.findByText('请单独评估这个角色设定流程'),
).not.toBeNull();
expect(await screen.findByText(roleAgentMockReply)).not.toBeNull();
expect(await screen.findByText(streamedReply)).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('append_local_conversation_message', {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
@@ -1061,15 +1088,17 @@ describe('AI 游戏创作 App 界面边界', () => {
agentId: 'design-director',
message: {
role: 'assistant',
content: roleAgentMockReply,
content: streamedReply,
agentId: null,
},
});
expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_role_agent', {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
prompt: '请单独评估这个角色设定流程',
});
expect(invoke).not.toHaveBeenCalledWith(
'chat_with_game_creator_role_agent',
expect.anything(),
);
expect(
screen.queryByText(/已保存用户消息;Agent 回复失败/),
).toBeNull();
expect(invoke).toHaveBeenCalledWith(
'chat_with_game_creator_role_agent_stream',
expect.objectContaining({
@@ -1084,6 +1113,78 @@ describe('AI 游戏创作 App 界面边界', () => {
);
});
it('falls back to a normal reply when streaming fails before the first delta', async () => {
const persistedMessages: Array<{
role: 'user' | 'assistant' | 'tool';
content: string;
agentId: string | null;
}> = [];
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: args?.agentId,
messages: persistedMessages,
};
}
if (command === 'chat_with_game_creator_role_agent_stream') {
throw new Error('stream disconnected before first delta');
}
if (command === 'chat_with_game_creator_role_agent') {
return { replyText: '普通回复补位成功。' };
}
if (command === 'append_local_conversation_message') {
persistedMessages.push(
args?.message as {
role: 'user' | 'assistant' | 'tool';
content: string;
agentId: string | null;
},
);
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: args?.agentId,
messages: persistedMessages.map((message, index) => ({
schemaVersion: '1',
...message,
updatedAt: index + 1,
})),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: vi.fn(async () => () => {}) },
};
renderLauncherAgentChatAt('/?agent-chat');
fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), {
target: { value: '/tmp/authorized-game' },
});
fireEvent.click(screen.getByRole('button', { name: '读取历史' }));
expect(await screen.findByText(/已读取 0 条/)).not.toBeNull();
fireEvent.change(screen.getByLabelText('Agent 聊天内容'), {
target: { value: '流式失败时继续回答' },
});
fireEvent.click(screen.getByRole('button', { name: '发送' }));
expect(await screen.findByText('普通回复补位成功。')).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_role_agent', {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
prompt: '流式失败时继续回答',
});
expect(persistedMessages.at(-1)).toEqual({
role: 'assistant',
content: '普通回复补位成功。',
agentId: null,
});
});
it('keeps the launcher usable and persists a normal reply when runtime listens reject', async () => {
const persistedMessages: Array<{
role: 'user' | 'assistant' | 'tool';
@@ -1833,6 +1934,11 @@ describe('AI 游戏创作 App 界面边界', () => {
screen.getByLabelText('Agent 聊天记录'),
).findByRole('status');
expect(waitingForFirstDelta.textContent).toContain('已连接 Agent LLM');
expect(waitingForFirstDelta.textContent).toContain('请求仍在进行中');
expect(screen.getByLabelText('Agent 聊天记录').getAttribute('role')).toBe(
'log',
);
expect(screen.getByLabelText('Agent 聊天记录').tabIndex).toBe(0);
await act(async () => {
releaseFirstDelta?.();
});
@@ -21,7 +21,7 @@
- 背景:开发用单 Agent 聊天已经能真实调用各 Agent 的 LLM 路由并持久化对话,但 Agent 仍主要表现为同步问答,用户无法明确投递一个任务让某个 Agent 独立运行,也无法同时启动多个 Agent 的工作。
- 决策:在现有 `.agent/runtime``.agent/conversations` 基础上新增单 Agent 后台任务入口。Tauri 命令 `start_game_creator_agent_runtime_task` 立即写入该 Agent 的 runtime state/event/task history,追加用户任务到 `.agent/conversations/agents/<agentId>.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loopAgent 按轮输出 `thinkingSummary / plan / actions / response`,Runtime 按白名单和项目权限策略执行工具并记录 `action / observation` 事件,再把已有 observation 放回下一轮 prompt,让 Agent 修正计划、继续行动或用空 actions + response 收束;当前后台任务最多执行 3 轮 loop,仍未收束时再按最后计划和全部观察生成最终回复。完成或失败后把 assistant 回复或错误追加回对话,并写入 `.agent/agent.db` 审计记录。工具箱包含只读工具 `memory.read``conversation.read``asset.list``project.index``project.diff``file.list``file.read``agent.run_status`,以及受策略保护的写/运行工具 `memory.write``file.write``command.run_limited``blackboard.write``agent.message``agent.delegate``memory.write` 可追加或覆盖本 Agent 私有记忆、项目长期/短期记忆或黑板,`file.write` 只能写项目内相对路径,`command.run_limited` 只接受 `game.static_smoke` 并复用本地静态自检安全边界,`blackboard.write` 追加共享黑板,`agent.message` 写目标 Agent 对话,`agent.delegate` 把任务投递到目标 Agent 的独立后台队列;策略拒绝时不执行工具并把 `blocked` observation 回给 Agent;策略要求确认时不执行工具,而是持久化精确待确认动作并暂停该 Agent 队列,待开发者确认或拒绝后在同一 run 续跑。每个 Agent 的任务历史落在 `.agent/runtime/tasks/<agentId>.jsonl`,读 runtime 时按 `runId` 去重返回最近任务,任务视角状态使用 `pending / running / completed / failed`Runtime state 增加 `nextStep`UI 在 Runtime 面板和主 Agent 状态卡展示当前任务、动作、下一步与最近任务。不同 Agent 使用独立 `.agent/runtime/locks/<agentId>.lock`,允许并行运行;同一 Agent 已有运行任务时,新任务会先进入该 Agent 的 pending 队列,当前 drain 持锁完成后串行继续下一条 pending。该能力仍不是独立 OS 进程或跨重启离线常驻 worker。
- 2026-07-10 补充:后台 Runtime 每次追加 `.agent/runtime/events/<agentId>.jsonl` 后会通过 Tauri `game-creator-agent-runtime-update` 事件广播当前 `AgentRuntimeResult`;开发单 Agent 聊天页、项目内 Agent 对话弹窗和主窗口 Agent 状态列表都只把该事件作为实时 UI 通知并复用前端 runtime 归一化合并,事实源仍是 `.agent/runtime/agents``events``tasks` 文件。
- 2026-07-11 补充:开发单 Agent 聊天页保留整页纵向滚动,聊天消息区固定响应式高度并在内部滚动;Runtime 恢复确认区使用独立布局行,避免与 Runtime 详情或聊天内容重叠。Runtime 面板详情可折叠且折叠时不渲染详情 DOM,但状态标题与任务控制按钮继续保留;等待 LLM 时在消息区显示动态状态,连续流式 delta 合并到动画帧更新并跳过重复 Runtime state。OpenAI Chat SSE 会收集 usage-only 尾包、保留 finish reason 与上游 error message,收到 `[DONE]` 后立即结束;持久事件订阅失败时显示非致命错误,聊天事件监听不可用或首个文本片段前流式失败时降级普通回复并继续落盘。
- 2026-07-11 补充:开发单 Agent 聊天页保留整页纵向滚动,聊天消息区固定响应式高度并在内部滚动;Runtime 恢复确认区使用独立布局行,避免与 Runtime 详情或聊天内容重叠。Runtime 面板详情可折叠且折叠时不渲染详情 DOM,但状态标题与任务控制按钮继续保留;等待 LLM 时在消息区持续显示动态状态和进行中提示,连续流式 delta 合并到动画帧更新并跳过重复 Runtime state。OpenAI Chat SSE 会跳过空 `choices` 心跳 / 元数据事件,收集 usage-only 尾包、保留 finish reason 与上游 error message,收到 `[DONE]` 后立即结束;正文与 finish reason 已接收后出现尾包异常时保存已完成正文,不把整轮改写成失败。持久事件订阅失败时显示非致命错误,聊天事件监听不可用或首个文本片段前流式失败时降级普通回复并继续落盘。
- 2026-07-11 补充:为缩小单 Agent 与 Codex CLI 在代码任务上的差距,Runtime 工具箱新增 `project.search``file.patch`,并扩展 `file.read` 的按行分页。`project.search` 在项目内执行有界字面量检索,默认忽略大小写,返回相对路径、行号和匹配行,跳过 `.agent`、敏感配置、依赖和构建目录;权限继承 `file.read``file.read` 接受 `startLine / maxLines`,返回带行号的最多 240 行、8,000 字符上下文,允许 Agent 继续分页而不是只看到文件开头约 900 字符。`file.patch` 只做 `oldText -> newText` 精确替换,必须声明预期匹配数,匹配数不符时不写入;它继承 `file.write` 权限,复用项目写锁和 Runtime 动作账本,并追加不含代码正文的 `agent.runtime.file.patch` 审计记录。三者组成“搜索定位 -> 分段读取 -> 局部修改 -> 再次读取验证”的最小代码工作闭环,不开放任意 shell。
- 2026-07-11 补充:单 Agent 代码闭环新增开发专用 `project.verify`,用于在修改后执行项目根 `package.json` 已定义的 `check / typecheck / test / lint / build` 之一;当前只支持 npm,不接受自由命令、参数或工作目录。Agent 必须先读取 `package.json`,再把脚本名、完整原始脚本文本 `expectedCommand` 和 1-300 秒超时一起提交;Runtime 在真正执行前重新解析 JSON 并做精确一致性校验,脚本漂移时拒绝执行。该工具使用独立、默认 `confirm``project.verify` 权限,不再与 `command.run_limited` / `game.static_smoke` 共用授权;确认指纹覆盖脚本正文和超时。执行时不经过 App 自行拼接的 `bash -c`,而由 npm 执行已确认的项目脚本,并附加 `--ignore-scripts` 阻止 `pre/post` 生命周期旁路;继承环境被清理到 PATH 与必要平台变量,HOME/TMP/npm cache 隔离,stdin 关闭,输出保留有界头尾。Unix 下验证根进程正常结束或超时都会清理同进程组残留后代;项目写锁会按持有 PID 回收崩溃遗留锁,并拒绝 `.agent` 符号链接逃逸。进入进程执行后的成功、非零退出、启动失败和超时会写 `.agent/logs/command.log`、manifest command run 与 `agent.runtime.project.verify` 审计;输入预检拒绝则只进入 Runtime observation / error 事件。输出先过滤敏感内容再进入 observation。只要最新 `project.verify` 未通过,或通过后又发生 `file.write / file.patch / project.restore`Runtime 就拒绝模型用空 actions 假完成,继续要求修复和重新验证;耗尽 loop 仍未通过时保持失败。该能力会执行用户项目自身脚本,环境隔离不等同于 OS 沙箱,不能把不可信项目脚本视为安全代码;它不是自由 shell 代理,也不进入普通用户命令入口。
- 2026-07-11 补充:开发侧新增 headless 单 Agent Runtime 入口 `npm run ai-game-creator-shell:agent-task -- [--init] <projectPath> <agentId> <task>`。该入口不实现第二套 Agent,只复用 Tauri App 的持久任务队列、per-agent 锁、LLM 路由、权限策略、工具 action / observation loop、对话和审计文件,并轮询到 `completed / failed / waiting-for-confirmation` 后用稳定键值行退出;`--init` 只在显式传入且 manifest 不存在时初始化项目。遇到待确认动作时 CLI 返回非零并打印 actionId、tool 和脱敏摘要,后续仍由开发窗口完成确认,不提供静默 `--yes` 绕过。
@@ -31,6 +31,7 @@
- 2026-07-11 调整:后台结构化 planning 使用独立的 4,000 输出 token 上限,最终回复使用 2,400;两者显式请求 low reasoning effort 和 low text verbosity。`platform-llm` 会把 reasoning effort 同时映射到 OpenAI Responses 的 `reasoning.effort` 与 Chat Completions 的 `reasoning_effort`,未设置时不新增字段。真实 gpt-5.5 Chat 响应曾连续消耗约 1,000-1,400 completion tokens 却不返回 message content,低推理强度、较大的可见输出余量和 EmptyResponse 重试共同构成恢复策略。
- 2026-07-11 补充:后台单 Agent 的工具计划响应只接受可反序列化为计划 schema 的 JSON object。解析器提取模型输出中的首个完整对象并允许对象后带普通说明;未找到完整 JSON 对象,或提取对象无法反序列化为工具计划时,Runtime 最多追加 2 次自动格式修复请求。每次修复只携带限长、脱敏后的上一次无效输出,并写入 `agent.runtime.tool_plan.repair` 审计。两次修复后仍无有效对象则按工具规划失败处理;工具规划阶段的普通文本不得转换为默认的空 actions + response,也不得据此把任务标记为完成。
- 2026-07-11 调整:工具计划顶层 `thinkingSummary / plan / actions / response` 四个字段必须同时存在,未知顶层字段、空 thinkingSummary 和空 tool 均属于协议错误并进入同一格式修复预算,`{}` 或前置无关 JSON 对象不能再触发空计划收束。空 actions 表示 planning 收束;response 非空时直接采用,response 为空时进入独立的最终回复生成。`agent.runtime.project.verify` 审计同时保存 `runId / actionId / actionFingerprint`,使并行 Agent 的失败与通过记录能够精确归属到发起动作。
- 2026-07-12 补充:OpenAI Chat / Responses 的后台 Agent 工具 planning 改用唯一 `submit_agent_tool_plan` 原生 function tool,字符串 `tool_choice=required` 和 strict schema;只接受恰好一次同名调用,arguments 继续经过本地计划 schema、工具白名单和权限策略校验,错误函数、多调用或非法 arguments 进入原有两次格式修复预算且不产生副作用。Anthropic 保留文本 JSON 回退;planning 非流式,最终回复仍可流式。每轮成功协议写 `agent.runtime.tool_plan.protocol`,修复审计记录 protocol、callId 和 functionName。
- 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `preview.start`,让 Agent 在完成写盘或静态自检后能按策略自行启动当前项目的 `127.0.0.1` 本地 HTTP 预览。该工具复用 `preview.start` 权限策略、项目写锁、共享 `PreviewRegistry`、manifest 预览状态、`.agent/logs/preview.log` 和 run trace 追加逻辑;写入 `.agent/agent.db` 的审计类型为 `agent.runtime.preview.start`。发给 LLM 的 observation 只包含 localhost URL 和端口,不包含用户项目绝对路径。
- 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `canvas.asset_generate`,让美术类 Agent 可在 loop 中自行请求生成首版美术素材。该工具读取 AppData / Tauri 配置中的 `editorApi`,复用 `canvas.asset_generate` 权限策略、项目写锁、External Editor API 生成和下载链路、manifest 资产登记以及 `canvas.asset_generate` 本地索引记录;另写 `agent.runtime.canvas.asset_generate` 记录到 `.agent/agent.db`,标明触发的 agent 与本地素材路径。API Key 不进入 prompt observation、manifest、agent.db 或日志;策略要求确认或拒绝时不会调用外部 API。
- 补充:规范 Agent ID 统一使用 manifest taskId,例如 `art-asset-plan``code-prototype`;历史前端曾使用的 `group-role` 别名只在 Tauri command 层兼容并映射到规范 taskId。主窗口 Agent 状态列表通过 `read_game_creator_agent_runtimes` 批量读取 `.agent/runtime/agents/<taskId>.json` 和最近任务,把每个 Agent 的 Runtime 状态、当前动作和最近 task 直接显示在状态卡片和 `/agents` 汇总里。
@@ -52,7 +52,7 @@ Agent Runtime 负责:
- 2026-07-10 补充:`recentEvents` 接入前端归一态和 Runtime 状态面板,事件事实源仍是 `.agent/runtime/events/<agentId>.jsonl`;面板按时间展示最近 `thinking_summary / plan / action / observation / response / error` 事件,现在能同时看到 Agent 的计划、最近观察、最近事件、最近工具动作和任务队列。
- 2026-07-10 补充:后台 Runtime 每次追加 `.agent/runtime/events/<agentId>.jsonl` 后会通过 Tauri `game-creator-agent-runtime-update` 事件广播当前 `AgentRuntimeResult`,开发单 Agent 聊天页、项目内 Agent 对话弹窗和主窗口 Agent 状态卡用同一套前端归一化逻辑合并状态;该事件只做实时 UI 通知,`.agent/runtime/agents``events``tasks` 仍是重开项目后的事实源。
- 2026-07-10 补充:后台 Agent loop 的统一语义事件类型为 `thinking_summary / plan / action / observation / response / error`。普通失败和 loop 预算耗尽都会追加 `error` 事件,并继续保留 `turn.failed / turn.budget_exhausted` 生命周期事件兼容既有读取方;开发窗口、项目内 Agent 对话弹窗和主窗口状态卡通过现有最近事件列表直接展示统一错误事件及其安全详情。状态面板默认保持最新 4 条的紧凑视图,当前后端返回的最近事件超过 4 条时可展开查看全部返回记录,确保同一 run 的六类语义事件不会因 UI 硬截断而无法检查。
- 2026-07-11 补充:开发单 Agent 聊天页继续使用整页纵向滚动,不把 Runtime 锁进固定视口;聊天消息区使用固定响应式高度并在内部滚动,避免历史消息持续撑高聊天面板。可选的 Runtime 恢复确认区始终占据独立布局行,不能与 Runtime 详情或聊天消息重叠。Runtime 状态面板支持折叠详情,折叠时只卸载目标、计划、事件、动作和任务等详情 DOM,仍保留状态标题与取消、重试、确认、拒绝、刷新操作;等待 LLM 时在消息区显示连接 / 等待首包 / 接收中的动态状态。流式聊天的连续 delta 通过 `requestAnimationFrame` 合并为每帧最多一次消息更新,delta 不重复提交未变化的 Runtime stateOpenAI Chat SSE 的 usage-only 事件会回填最终 token usagefinish-only 事件会把结束原因送入状态流,上游 error 保留真实消息,`[DONE]` 立即结束读取;持久事件订阅失败时显示非致命 Runtime 错误,聊天事件监听不可用或流式请求在首个文本片段前失败时自动降级普通回复。
- 2026-07-11 补充:开发单 Agent 聊天页继续使用整页纵向滚动,不把 Runtime 锁进固定视口;聊天消息区使用固定响应式高度并在内部滚动,避免历史消息持续撑高聊天面板。可选的 Runtime 恢复确认区始终占据独立布局行,不能与 Runtime 详情或聊天消息重叠。Runtime 状态面板支持折叠详情,折叠时只卸载目标、计划、事件、动作和任务等详情 DOM,仍保留状态标题与取消、重试、确认、拒绝、刷新操作;等待 LLM 时在消息区持续显示连接 / 等待首包 / 接收中的动态状态和“请求仍在进行中”提示。流式聊天的连续 delta 通过 `requestAnimationFrame` 合并为每帧最多一次消息更新,delta 不重复提交未变化的 Runtime stateOpenAI Chat SSE 的`choices` 心跳 / 元数据事件会跳过,usage-only 尾包会回填最终 token usagefinish-only 事件会把结束原因送入状态流,上游 error 保留真实消息,`[DONE]` 立即结束读取;正文与 finish reason 已接收后即使尾包异常也保存完整正文,不再改判整轮失败。持久事件订阅失败时显示非致命 Runtime 错误,聊天事件监听不可用或流式请求在首个文本片段前失败时自动降级普通回复。
- 2026-07-11 补充:后台单 Agent 新增 Codex 风格的代码导航与局部编辑闭环。`project.search` 接受 `query / path / maxResults / caseSensitive`,在项目边界内做字面量搜索并返回 `path:line`,最多扫描 500 个、单个不超过 512 KiB 的文本文件,跳过 `.agent``.git``node_modules``dist``build``target``.next``coverage``.env*`;该工具映射到 `file.read` 权限。`file.read` 接受 `startLine / maxLines`,返回带行号的指定片段、总行数和下一页提示,单次最多 240 行、8,000 字符。`file.patch` 接受 `path / oldText / newText / expectedReplacements`,只在实际匹配数与预期一致时持锁写入,目标文件和修改后文件最大 2 MiB,成功后写 `agent.runtime.file.patch` 审计;该工具映射到 `file.write` 权限。Agent planning prompt 明确要求批量修改前创建 checkpoint,并可在修改后再次 `file.read` 验证;本轮不开放任意 shell 命令。
- 2026-07-11 补充:代码修改后的真实验证由开发专用 `project.verify` 承接。输入固定为 `script / expectedCommand / timeoutSeconds`,其中 script 只允许项目根 `package.json` 中的 `check / typecheck / test / lint / build`expectedCommand 必须与执行时重新读取的脚本正文完全一致,timeoutSeconds 为 1-300;当前执行器只支持 npm,其他 packageManager 或锁文件明确失败。工具映射到独立且默认需确认的 `project.verify` 权限,确认动作指纹绑定完整输入,不再因为放行验证而同时放行 `command.run_limited` 静态 smoke。执行器由 npm 运行已确认脚本,使用 `--ignore-scripts`、空 stdin、隔离 HOME/TMP/cache、清理后的环境、独立进程组和有界脱敏输出;Unix 下无论根进程正常结束还是超时都会清理同组残留后代。项目写锁记录 PID 和唯一 nonce,活进程继续持锁,Unix 死进程锁或跨平台超过安全时限的无效锁可回收,且控制路径拒绝符号链接。进入进程执行后的终态写命令日志和 manifest command runAgent 触发时另写 `agent.runtime.project.verify`;输入预检拒绝只写 Runtime observation / error 事件。失败输出作为 observation 回到下一轮 planning。最新验证失败,或验证通过后又执行 `file.write / file.patch / project.restore` 时,空 actions 不再代表完成,Runtime 会注入 `runtime.verification: blocked` 并继续 replan;loop 耗尽仍未形成新通过结果时保持失败。该能力会执行用户项目脚本,环境隔离不是 OS 沙箱;普通用户 `/smoke``game.static_smoke` 保持原边界,不暴露该开发工具。
- 2026-07-11 补充:开发验证可用 `npm run ai-game-creator-shell:agent-task -- [--init] <projectPath> <agentId> <task>` 无 UI 启动单 Agent 后台任务。CLI 只负责可选初始化、调用现有 Runtime、按 runId 轮询终态并打印 `status / phase / replyText / pendingActionId`,不复制 planning 或工具执行逻辑;默认 10 分钟轮询上限。`waiting-for-confirmation` 会以非零状态退出并要求转到开发窗口确认,CLI 不提供跳过项目权限的自动确认参数。该入口用于真实 provider 的可重复端到端验收,不进入普通用户界面。
@@ -61,6 +61,7 @@ Agent Runtime 负责:
- 2026-07-11 调整:后台任务的可执行正文上限统一为 4,000 字符。入队 JSONL、启动后的 `currentTask/currentGoal`、planning prompt、待确认动作 task context、确认续跑和重启恢复都保留同一份正文;对话仍保存用户原始消息。状态事件、列表卡片和 `agent.db` 摘要可继续使用较短安全预览,但不能再反向作为后续 LLM 执行输入。这样长任务末尾的验收标记和输出格式要求不会在队列边界被 180 字符截断。
- 2026-07-11 调整:后台 planning 不再复用普通聊天的 1,800 输出 token 上限,而是使用 4,000;最终回复使用 2,400。两类请求均设置 low reasoning effort / low text verbosityOpenAI Responses 序列化为 `reasoning.effort=low`OpenAI Chat Completions 序列化为可选 `reasoning_effort=low`。该设置用于避免推理模型把全部 completion 预算消耗在不可见 reasoning 后留下空 content,并继续叠加最多 3 次 EmptyResponse 重试。
- 2026-07-11 补充:后台单 Agent 的工具 planning 响应必须提供可反序列化为 `thinkingSummary / plan / actions / response` schema 的 JSON object。Runtime 从模型输出中解析首个完整对象,因此对象后的尾随说明可以忽略;只有普通文本、没有完整对象,或对象无法反序列化时都不构成有效工具计划。对于这两类无效输出,Runtime 最多追加 2 次自动格式修复请求,每次只把限长且经过统一敏感信息过滤的上一次输出作为修复上下文,并把修复尝试写入 `.agent/agent.db``agent.runtime.tool_plan.repair` 审计。修复预算耗尽后进入既有工具规划失败路径,不得把普通文本折算为空 actions + response,也不得因此进入 completed;最终回复阶段仍按其独立的普通文本契约处理。
- 2026-07-12 补充:OpenAI Chat / Responses 的后台工具 planning 优先注册唯一的 `submit_agent_tool_plan` function tool,并使用字符串形式 `tool_choice=required` 和 strict schemaRuntime 只接受恰好一次同名 function call,并把 arguments 复用现有 `AgentRuntimeToolPlan` 校验与两次格式修复循环。错误函数名、多次调用和非法 arguments 都不得执行工具。Anthropic 保留文本 JSON 回退,planning 强制非流式,最终普通回复继续按 Agent 配置决定是否流式。`platform-llm` 会在本地拒绝无 function tools 的 tool choice 和 Anthropic function tools,并把协议类型写入 `agent.runtime.tool_plan.protocol` 审计。
- 2026-07-11 调整:工具计划四个顶层字段均为必填并拒绝未知顶层字段;thinkingSummary 与 action.tool 必须非空。这样 `{}`、前置无关 JSON 或结构不完整对象会触发格式修复,不会成为假完成信号。空 actions 表示 planning 收束;response 非空时直接采用,response 为空时进入独立最终回复生成。`agent.runtime.project.verify` 记录补充 `runId / actionId / actionFingerprint`,用于在多 Agent 并行验证时把命令终态与具体 Runtime 动作关联。
- 2026-07-10 补充:Agent Runtime state / result 新增 `taskQueue`,从 `.agent/runtime/tasks/<agentId>.jsonl` 中每个 `runId` 的最新记录汇总 `total / pending / running / completed / failed / latestRunId`;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都读取该摘要,用于判断同一 Agent 是否仍有排队任务。该字段是运行观测摘要,不新增调度器、SQLite 或独立 worker。
- 2026-07-10 补充:Runtime 新增 `agent.schedule_ready` 调度入口。开发构建可在权限确认后扫描 manifest ready task,把依赖已完成且仍为 `pending` 的任务标成 `running`,并按 taskId 投递到对应 Agent 的既有后台队列;source 固定为 `agent-ready-task-scheduler`,审计记录写 `agent.runtime.ready_task.scheduled`。该入口只把 manifest ready task 接入现有 per-agent 队列、锁、JSONL、LLM loop、工具策略和事件流,不新增独立 worker,也不会在默认确认策略下静默启动。
+2
View File
@@ -46,6 +46,8 @@ pub async fn proxy_llm_chat_completions(
request_timeout_ms: None,
response_reasoning_effort: None,
response_text_verbosity: None,
function_tools: Vec::new(),
tool_choice: None,
};
if payload.stream {
@@ -54,5 +54,7 @@ pub fn build_gpt5_multimodal_request(
api_kind: LlmApiKind::OpenAiChat,
response_reasoning_effort: None,
response_text_verbosity: None,
function_tools: Vec::new(),
tool_choice: None,
}
}
File diff suppressed because it is too large Load Diff