feat: summarize codex stream progress events
This commit is contained in:
@@ -1679,6 +1679,49 @@ public final class BossUi {
|
||||
}
|
||||
}
|
||||
|
||||
JSONObject streamEvents = progress == null ? null : progress.optJSONObject("streamEvents");
|
||||
if (streamEvents != null) {
|
||||
int agentDeltaCount = streamEvents.optInt("agentDeltaCount", 0);
|
||||
int planDeltaCount = streamEvents.optInt("planDeltaCount", 0);
|
||||
int reasoningDeltaCount = streamEvents.optInt("reasoningDeltaCount", 0);
|
||||
int toolProgressCount = streamEvents.optInt("toolProgressCount", 0);
|
||||
int commandOutputChunkCount = streamEvents.optInt("commandOutputChunkCount", 0);
|
||||
int terminalInteractionCount = streamEvents.optInt("terminalInteractionCount", 0);
|
||||
int fileOutputChunkCount = streamEvents.optInt("fileOutputChunkCount", 0);
|
||||
boolean hasStreamEvents = agentDeltaCount > 0 || planDeltaCount > 0 || reasoningDeltaCount > 0 ||
|
||||
toolProgressCount > 0 || commandOutputChunkCount > 0 || terminalInteractionCount > 0 ||
|
||||
fileOutputChunkCount > 0;
|
||||
if (hasStreamEvents) {
|
||||
card.addView(divider(context));
|
||||
card.addView(sectionTitle(context, "流式增量"));
|
||||
String status = streamEvents.optString("status", "").trim();
|
||||
if (!TextUtils.isEmpty(status)) {
|
||||
card.addView(detailRow(context, "◇", "状态 " + status, "", false));
|
||||
}
|
||||
if (agentDeltaCount > 0) {
|
||||
card.addView(detailRow(context, "", "回复片段 " + agentDeltaCount, "", false, true));
|
||||
}
|
||||
if (planDeltaCount > 0) {
|
||||
card.addView(detailRow(context, "", "计划片段 " + planDeltaCount, "", false, true));
|
||||
}
|
||||
if (reasoningDeltaCount > 0) {
|
||||
card.addView(detailRow(context, "", "思考片段 " + reasoningDeltaCount, "", false, true));
|
||||
}
|
||||
if (toolProgressCount > 0) {
|
||||
card.addView(detailRow(context, "", "工具进度 " + toolProgressCount, "", false, true));
|
||||
}
|
||||
if (commandOutputChunkCount > 0) {
|
||||
card.addView(detailRow(context, "", "命令输出片段 " + commandOutputChunkCount, "", false, true));
|
||||
}
|
||||
if (terminalInteractionCount > 0) {
|
||||
card.addView(detailRow(context, "", "终端交互 " + terminalInteractionCount, "", false, true));
|
||||
}
|
||||
if (fileOutputChunkCount > 0) {
|
||||
card.addView(detailRow(context, "", "文件输出片段 " + fileOutputChunkCount, "", false, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray warnings = progress == null ? null : progress.optJSONArray("warnings");
|
||||
if (warnings != null && warnings.length() > 0) {
|
||||
card.addView(divider(context));
|
||||
|
||||
@@ -1234,6 +1234,58 @@ public class ProjectDetailActivityUiTest {
|
||||
assertFalse(viewTreeContainsText(messageView, "sk-secret-should-not-render"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executionProgressMessageRendersCodexStreamEventsSection() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "stream-events")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "Boss开发主线程");
|
||||
TestProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
JSONObject message = new JSONObject()
|
||||
.put("id", "progress-stream-events-1")
|
||||
.put("sender", "master")
|
||||
.put("senderLabel", "主 Agent")
|
||||
.put("body", "执行进度")
|
||||
.put("kind", "execution_progress")
|
||||
.put("sentAt", "2026-06-03T11:20:00+08:00")
|
||||
.put("executionProgress", new JSONObject()
|
||||
.put("status", "running")
|
||||
.put("steps", new JSONArray()
|
||||
.put(new JSONObject().put("text", "接收 Codex 流式事件").put("status", "running")))
|
||||
.put("streamEvents", new JSONObject()
|
||||
.put("status", "streaming")
|
||||
.put("agentDeltaCount", 2)
|
||||
.put("planDeltaCount", 1)
|
||||
.put("reasoningDeltaCount", 3)
|
||||
.put("toolProgressCount", 1)
|
||||
.put("commandOutputChunkCount", 4)
|
||||
.put("terminalInteractionCount", 1)
|
||||
.put("fileOutputChunkCount", 1)
|
||||
.put("delta", "secret delta should-not-render")
|
||||
.put("output", "secret output should-not-render")
|
||||
.put("content", "secret content should-not-render")));
|
||||
|
||||
View messageView = ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"buildMessageView",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, message)
|
||||
);
|
||||
|
||||
assertTrue(viewTreeContainsText(messageView, "流式增量"));
|
||||
assertTrue(viewTreeContainsText(messageView, "状态 streaming"));
|
||||
assertTrue(viewTreeContainsText(messageView, "回复片段 2"));
|
||||
assertTrue(viewTreeContainsText(messageView, "计划片段 1"));
|
||||
assertTrue(viewTreeContainsText(messageView, "思考片段 3"));
|
||||
assertTrue(viewTreeContainsText(messageView, "工具进度 1"));
|
||||
assertTrue(viewTreeContainsText(messageView, "命令输出片段 4"));
|
||||
assertTrue(viewTreeContainsText(messageView, "终端交互 1"));
|
||||
assertTrue(viewTreeContainsText(messageView, "文件输出片段 1"));
|
||||
assertFalse(viewTreeContainsText(messageView, "should-not-render"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executionProgressMessageRendersCodexToolActivitySection() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
|
||||
@@ -163,6 +163,7 @@
|
||||
- 第二十五批另补 `mcpGovernanceSummary / userInteractionGovernanceSummary / guardianGovernanceSummary` MCP、用户交互和 Guardian 治理能力摘要:设备详情页会显示 MCP OAuth/resource/tool/elicitation、tool requestUserInput、Guardian denied action approval 和 permission request approval 等能力分组;这些字段只读,不在 heartbeat 中调用任何 MCP、用户输入或 Guardian 放行动作。
|
||||
- 第二十六批另补 `runtimeEventSummary / extensionEventSummary / threadLifecycleEventSummary` 运行事件、扩展事件和线程生命周期事件能力摘要:设备详情页会显示 process output/exited、raw response completed、skills changed、plugin installed、thread started/closed/archived/unarchived/name updated 等能力分组;这些字段只读,不在 heartbeat 中主动触发进程、插件、Skill 或线程生命周期动作。
|
||||
- 第二十七批另补 `streamDeltaEventSummary` 流式增量事件能力摘要:设备详情页会显示 agent delta、plan delta、reasoning delta、MCP progress、command output、terminal interaction 和 file output 等能力分组;该字段只读,不保存原始增量文本、命令输出、推理正文或文件输出。
|
||||
- 当前任务执行态也已补 `executionProgress.streamEvents`:App Server runner 会把 agent / plan / reasoning / MCP / command / terminal / file 的流式 delta 归一成计数,Android 进度卡展示“流式增量”,不保存或渲染原始 delta、命令输出、终端输入、推理正文或文件输出。
|
||||
- 当前 boss-agent 已支持 Mac OTA:`local-agent/boss-agent-ota-runner.mjs` 默认开启,每 5 分钟检查服务端最新包;状态页可手动检查或下载并安装,安装时保留原绑定配置,只更新版本号和本机 runtime 路径。最新验证版本为 `20260516221619`,已在 MacBook Air `macbook-air` 上确认 OTA 下载校验、暂存、覆盖安装后不会误切到默认 `config.cloud.json`。正式分发脚本已预留 Developer ID 公证路径:`BOSS_AGENT_NOTARIZE=1` 配合 notary profile 或 Apple ID 凭据。
|
||||
- 当前量产治理已补设备撤权和任务可靠性底座:`revoke_device` 会清空设备 token、标记离线并阻断 heartbeat / 任务认领 / Skill 同步 / 日志上报 / boss-agent OTA;`MasterAgentTask` claim 会记录 attempt 和 lease,运行中任务可按租约重试,超过上限转 `timed_out`,用户或管理员可通过 cancel 接口转 `canceled` 且迟到 complete 不覆盖终态。
|
||||
- 当前群聊 `dispatch_execution` 完成回写已补幂等,重复完成不会再向群聊重复追加结果
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
- 当前 `RemoteRuntimeAdapter` 还负责拦截固定模式的线程内部环境提示;命中后会直接改写成失败,避免把只读/cwd 这类脏文本写进聊天记录
|
||||
- 当前普通单线程 `conversation_reply` 在真正执行 `codex exec resume` 前,会先把 Boss 用户消息镜像进目标 Codex Desktop rollout;定位优先走 `state_5.sqlite`,不可用时回退扫描 `~/.codex/sessions`,并按 `sourceMessageId` 去重
|
||||
- 当前 Codex Desktop 同步新增常驻刷新桥:`scripts/codex-desktop-refresh-bridge-daemon.mjs` 通过 launchd 监听 `127.0.0.1:4318`,暴露 `POST /api/v1/codex-desktop/refresh`、`GET /api/v1/codex-desktop/events`、`GET /api/v1/codex-desktop/events/recent` 和 `GET /api/v1/codex-desktop/capabilities`;`local-agent` 会优先调用 refresh endpoint,失败时回退到 `scripts/codex-desktop-refresh-hint.mjs` 命令式刷新。SSE 事件只包含线程引用、消息 ID、状态、deep link 等安全元数据,不包含用户正文或内部 prompt;`scripts/codex-desktop-event-consumer.mjs` 可作为 Desktop 插件/IPC 接入前的订阅 smoke;`scripts/codex-desktop-integration-probe.mjs` 负责只读探测 Codex.app 能力
|
||||
- 当前新增 Codex App Server runner:`local-agent/codex-app-server-runner.mjs`。boss-agent 默认配置 `codexAppServerEnabled=true`,会接管 `conversation_reply / dispatch_execution`;它默认通过 stdio 启动 `codex app-server`,也支持 `codexAppServerTransport=ws + codexAppServerUrl=ws://127.0.0.1:<port>` 或 `codexAppServerTransport=unix + codexAppServerUrl=unix:///absolute/path.sock` 连接同机长驻 App Server,bearer token 可通过 `codexAppServerAuthTokenFile` 读取并在握手时发送 `Authorization: Bearer <token>`。runner 执行 `initialize -> thread/resume|thread/start -> turn/start|turn/steer`,并把 `item/agentMessage/delta` 或 `item/completed` 归一成 Boss 任务回复;当 App Server 对单个 JSON-RPC 请求返回 `-32001 / retry later` 时,runner 会做最多 3 次指数退避重试。turn 启动前失败可回退 CLI,turn 启动后失败不回退,避免重复执行。2026-05-31 起,runner 会把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 归一成 `executionProgress.steps / branch / artifacts / agents`,把 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated` 归一成 `executionProgress.approvals / warnings / fileChanges`,把 `thread/status/changed`、`thread/realtime/started|transcript|outputAudio|itemAdded|error|closed` 归一成 `executionProgress.threadStatus / realtime`,把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`,并把 `thread/goal/*`、`thread/settings/updated`、`thread/compacted`、`account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice`、`ThreadItem.collabToolCall`、`ThreadItem.contextCompaction`、`mcpToolCall`、`dynamicToolCall`、`webSearch`、`imageView`、`imageGeneration`、`hook/started|completed`、`windowsSandbox/setupCompleted`、`enteredReviewMode`、`exitedReviewMode`、`commandExecution`、`ThreadItem.plan`、`ThreadItem.reasoning.summary` 归一成线程配置、账号状态、模型校验、安全提醒、线程协作、上下文压缩、工具活动、图片产物、钩子生命周期、Windows 沙箱准备状态、计划步骤和思考摘要;新版 `ThreadItem.collabToolCall.receiverThreadIds / agentsStates` 只归一为目标数量和 agent 状态集合。服务端 complete 回写会与本地 Git/GitHub 进度合并,且不保存 SDP、音频 base64、raw realtime item、remote installationId、cwd、turnId、配置路径、collab 源/目标线程 ID、receiverThreadIds、collab prompt、agentsStates 私有消息、tool arguments/result/contentItems、web URL token、命令正文/输出、raw reasoning content、reasoning item id、imageGeneration revisedPrompt/result、hook sourcePath/statusMessage/entries、Windows sandbox sourcePath/samplePaths/本地绝对路径或未清洗的 MCP 错误。heartbeat 同时支持按 TTL 拉取 `model/list / skills/list / hooks/list / plugin/list / app/list / modelProvider/capabilities/read`,并把摘要保存在 `capabilities.codexAppServer.metadata`。
|
||||
- 当前新增 Codex App Server runner:`local-agent/codex-app-server-runner.mjs`。boss-agent 默认配置 `codexAppServerEnabled=true`,会接管 `conversation_reply / dispatch_execution`;它默认通过 stdio 启动 `codex app-server`,也支持 `codexAppServerTransport=ws + codexAppServerUrl=ws://127.0.0.1:<port>` 或 `codexAppServerTransport=unix + codexAppServerUrl=unix:///absolute/path.sock` 连接同机长驻 App Server,bearer token 可通过 `codexAppServerAuthTokenFile` 读取并在握手时发送 `Authorization: Bearer <token>`。runner 执行 `initialize -> thread/resume|thread/start -> turn/start|turn/steer`,并把 `item/agentMessage/delta` 或 `item/completed` 归一成 Boss 任务回复;当 App Server 对单个 JSON-RPC 请求返回 `-32001 / retry later` 时,runner 会做最多 3 次指数退避重试。turn 启动前失败可回退 CLI,turn 启动后失败不回退,避免重复执行。2026-05-31 起,runner 会把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 归一成 `executionProgress.steps / branch / artifacts / agents`,把 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated` 归一成 `executionProgress.approvals / warnings / fileChanges`,把 `thread/status/changed`、`thread/realtime/started|transcript|outputAudio|itemAdded|error|closed` 归一成 `executionProgress.threadStatus / realtime`,把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`,并把 `thread/goal/*`、`thread/settings/updated`、`thread/compacted`、`account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice`、`ThreadItem.collabToolCall`、`ThreadItem.contextCompaction`、`mcpToolCall`、`dynamicToolCall`、`webSearch`、`imageView`、`imageGeneration`、`hook/started|completed`、`windowsSandbox/setupCompleted`、`enteredReviewMode`、`exitedReviewMode`、`commandExecution`、`ThreadItem.plan`、`ThreadItem.reasoning.summary` 归一成线程配置、账号状态、模型校验、安全提醒、线程协作、上下文压缩、工具活动、图片产物、钩子生命周期、Windows 沙箱准备状态、计划步骤和思考摘要;新版 `ThreadItem.collabToolCall.receiverThreadIds / agentsStates` 只归一为目标数量和 agent 状态集合。2026-06-03 起,runner 还会把 `item/agentMessage/delta`、`item/plan/delta`、`item/reasoning/summaryPartAdded|summaryTextDelta|textDelta`、`item/mcpToolCall/progress`、`command/exec/outputDelta`、`item/commandExecution/outputDelta|terminalInteraction` 和 `item/fileChange/outputDelta` 归一成 `executionProgress.streamEvents` 计数。服务端 complete/progress 回写会与本地 Git/GitHub 进度合并,且不保存 SDP、音频 base64、raw realtime item、remote installationId、cwd、turnId、配置路径、collab 源/目标线程 ID、receiverThreadIds、collab prompt、agentsStates 私有消息、tool arguments/result/contentItems、web URL token、命令正文/输出、raw reasoning content、reasoning item id、原始 delta、terminal input、file output、imageGeneration revisedPrompt/result、hook sourcePath/statusMessage/entries、Windows sandbox sourcePath/samplePaths/本地绝对路径或未清洗的 MCP 错误。heartbeat 同时支持按 TTL 拉取 `model/list / skills/list / hooks/list / plugin/list / app/list / modelProvider/capabilities/read`,并把摘要保存在 `capabilities.codexAppServer.metadata`。
|
||||
- App Server heartbeat discovery 现在还会按 TTL 拉取 `experimentalFeature/list / collaborationMode/list / permissionProfile/list / mcpServerStatus/list`,写入 `capabilities.codexAppServer.metadata.experimentalFeatures / collaborationModes / permissionProfiles / mcpServers`。这些字段用于 APP/后台治理页展示 Codex 当前可用的实验特性、多 Agent/协作模式、权限 profile 和 MCP 服务健康;MCP 请求固定使用 `detail=toolsAndAuthOnly`,服务端状态里不保存 resource URI、工具参数、permission profile 文件规则、本地路径或密钥。
|
||||
- App Server heartbeat discovery 现在还会按 TTL 拉取 `account/read / account/rateLimits/read / config/read / configRequirements/read / externalAgentConfig/detect`,写入 `capabilities.codexAppServer.metadata.accountSummary / rateLimitSummary / appConfigSummary / configRequirements / externalAgentMigration`。这些字段用于 APP/后台展示账号、额度、App 配置、企业托管要求和外部 Agent 迁移候选摘要;当前只做观测,不通过 Boss 远程写 `config.toml` 或执行外部 Agent 导入,且不保存邮箱、完整 config、API key、本地路径或迁移描述。
|
||||
- App Server heartbeat discovery 现在还会按 TTL 拉取 `thread/list / thread/loaded/list`,写入 `capabilities.codexAppServer.metadata.threadSummary`。该字段用于 APP/后台展示 Codex 当前可见线程数量、加载态、活跃态和非归档线程轻量目录;目录只保留 `id / name / sourceKind / status / updatedAt / loaded`,不保存 cwd、本地路径、turn 内容、用户正文或内部 prompt。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -48,7 +48,7 @@
|
||||
- 当前 App Server 能力发现已新增审查、Windows 沙箱和文件搜索事件能力摘要:local-agent 会把已验证进入当前协议快照的 review start、Windows sandbox readiness / setup start / setup completed、fuzzy file search updated / completed 写入设备 `codexAppServer.metadata.reviewGovernanceSummary / windowsSandboxGovernanceSummary / fuzzyFileSearchSummary`;设备详情页会显示“审查治理 / Windows 沙箱 / 文件搜索事件”。这些字段只读,不在 heartbeat 中调用任何审查启动、沙箱设置或文件搜索动作。
|
||||
- 当前 App Server 能力发现已新增 MCP、用户交互和 Guardian 治理能力摘要:local-agent 会把已验证进入当前协议快照的 MCP OAuth / resource / tool / elicitation、tool requestUserInput、Guardian denied action approval 和 permission request approval 写入设备 `codexAppServer.metadata.mcpGovernanceSummary / userInteractionGovernanceSummary / guardianGovernanceSummary`;设备详情页会显示“MCP 治理 / 用户交互 / Guardian 治理”。这些字段只读,不在 heartbeat 中调用任何 MCP、用户输入或 Guardian 放行动作。
|
||||
- 当前 App Server 能力发现已新增运行事件、扩展事件和线程生命周期事件能力摘要:local-agent 会把已验证进入当前协议快照的 process output / exited、raw response completed、skills changed、plugin installed、thread started / closed / archived / unarchived / name updated 写入设备 `codexAppServer.metadata.runtimeEventSummary / extensionEventSummary / threadLifecycleEventSummary`;设备详情页会显示“运行事件 / 扩展事件 / 线程生命周期”。这些字段只读,不在 heartbeat 中主动触发进程、插件、Skill 或线程生命周期动作。
|
||||
- 当前 App Server 能力发现已新增流式增量事件能力摘要:local-agent 会把已验证进入当前协议快照的 agent delta、plan delta、reasoning delta、MCP tool progress、command output、terminal interaction 和 file output 写入设备 `codexAppServer.metadata.streamDeltaEventSummary`;设备详情页会显示“流式增量”。这些字段只读,不保存原始增量文本、命令输出、推理正文或文件输出。
|
||||
- 当前 App Server 能力发现已新增流式增量事件能力摘要:local-agent 会把已验证进入当前协议快照的 agent delta、plan delta、reasoning delta、MCP tool progress、command output、terminal interaction 和 file output 写入设备 `codexAppServer.metadata.streamDeltaEventSummary`;设备详情页会显示“流式增量”。同批已把执行中的 delta 事件接入 `executionProgress.streamEvents`,APP 进度卡只展示各类片段计数,不保存原始增量文本、命令输出、终端输入、推理正文或文件输出。
|
||||
- 当前 App Server 能力发现已支持共享 Skill 根目录下发:配置 `codexAppServerSkillExtraRoots` / `BOSS_CODEX_APP_SERVER_SKILL_EXTRA_ROOTS` 时,local-agent 会先调用 `skills/extraRoots/set`,再刷新 `skills/list`,并把 `skillExtraRootsSummary` 写入设备 `codexAppServer.metadata`;设备详情页会显示“共享 Skill 根”。该链路只保存数量、basename 和状态,不保存根目录绝对路径、Skill 文件路径或配置原文。
|
||||
- 当前 App Server 能力发现已新增 Hook 治理摘要:local-agent 会在 heartbeat discovery 中拉取 `hooks/list`,并把 hook 数、启用数、受管 / 可信 / 修改 / 未信任计数、warning / error 计数写入设备 `codexAppServer.metadata.hookSummary`;设备详情页会显示“Hook”。该链路不保存 hook key、command、sourcePath、statusMessage、hash、error message 或本地路径。
|
||||
- 当前量产 B+ 架构开发文档已新增:`docs/architecture/enterprise_ai_ops_architecture_cn.md`。该文档把 PPT 中的主 Agent / 业务 Agent / 老板端 / 经理端 / 员工端 / 治理层 / 系统层 / 设备层 / 执行层 / 接入层整理成后续产品架构约束,并明确数据库备份、业务回退、Codex 协议扩展和 Skill 治理方向;它是规划文档,不代表当前全部已落地
|
||||
|
||||
@@ -2143,6 +2143,7 @@ function createProgressCollector() {
|
||||
let reasoningSummary;
|
||||
let accountStatus;
|
||||
let modelVerification;
|
||||
let streamEvents;
|
||||
|
||||
const upsertArtifact = (artifact) => {
|
||||
if (!artifact || artifacts.some((item) => item.label === artifact.label)) {
|
||||
@@ -2245,11 +2246,71 @@ function createProgressCollector() {
|
||||
}
|
||||
};
|
||||
|
||||
const ensureStreamEvents = () => {
|
||||
if (!streamEvents) {
|
||||
streamEvents = {
|
||||
status: "streaming",
|
||||
agentDeltaCount: 0,
|
||||
planDeltaCount: 0,
|
||||
reasoningDeltaCount: 0,
|
||||
toolProgressCount: 0,
|
||||
commandOutputChunkCount: 0,
|
||||
terminalInteractionCount: 0,
|
||||
fileOutputChunkCount: 0,
|
||||
};
|
||||
}
|
||||
return streamEvents;
|
||||
};
|
||||
|
||||
const incrementStreamEvent = (key) => {
|
||||
const state = ensureStreamEvents();
|
||||
state.status = state.status === "completed" ? "completed" : "streaming";
|
||||
state[key] = Math.min(9999, Number(state[key] ?? 0) + 1);
|
||||
};
|
||||
|
||||
return {
|
||||
observe(message) {
|
||||
if (!message || typeof message !== "object") {
|
||||
return;
|
||||
}
|
||||
if (message.method === "item/agentMessage/delta") {
|
||||
incrementStreamEvent("agentDeltaCount");
|
||||
return;
|
||||
}
|
||||
if (message.method === "item/plan/delta") {
|
||||
incrementStreamEvent("planDeltaCount");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
message.method === "item/reasoning/summaryPartAdded" ||
|
||||
message.method === "item/reasoning/summaryTextDelta" ||
|
||||
message.method === "item/reasoning/textDelta"
|
||||
) {
|
||||
incrementStreamEvent("reasoningDeltaCount");
|
||||
return;
|
||||
}
|
||||
if (message.method === "item/mcpToolCall/progress") {
|
||||
incrementStreamEvent("toolProgressCount");
|
||||
return;
|
||||
}
|
||||
if (message.method === "command/exec/outputDelta" || message.method === "item/commandExecution/outputDelta") {
|
||||
incrementStreamEvent("commandOutputChunkCount");
|
||||
return;
|
||||
}
|
||||
if (message.method === "item/commandExecution/terminalInteraction") {
|
||||
incrementStreamEvent("terminalInteractionCount");
|
||||
return;
|
||||
}
|
||||
if (message.method === "item/fileChange/outputDelta") {
|
||||
incrementStreamEvent("fileOutputChunkCount");
|
||||
return;
|
||||
}
|
||||
if (message.method === "turn/completed") {
|
||||
if (streamEvents) {
|
||||
streamEvents.status = "completed";
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.method === "turn/plan/updated") {
|
||||
const nextSteps = extractPlanItems(message.params)
|
||||
.map((item, index) => {
|
||||
@@ -2634,6 +2695,9 @@ function createProgressCollector() {
|
||||
if (modelVerification) {
|
||||
result.modelVerification = { ...modelVerification };
|
||||
}
|
||||
if (streamEvents) {
|
||||
result.streamEvents = { ...streamEvents };
|
||||
}
|
||||
return Object.keys(result).length > 0 ? result : undefined;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -258,6 +258,17 @@ export interface ExecutionProgressReasoningSummary {
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface ExecutionProgressStreamEvents {
|
||||
status: string;
|
||||
agentDeltaCount?: number;
|
||||
planDeltaCount?: number;
|
||||
reasoningDeltaCount?: number;
|
||||
toolProgressCount?: number;
|
||||
commandOutputChunkCount?: number;
|
||||
terminalInteractionCount?: number;
|
||||
fileOutputChunkCount?: number;
|
||||
}
|
||||
|
||||
export interface ExecutionProgressSnapshot {
|
||||
taskId: string;
|
||||
projectId: string;
|
||||
@@ -291,6 +302,7 @@ export interface ExecutionProgressSnapshot {
|
||||
threadCollaboration?: ExecutionProgressThreadCollaboration;
|
||||
toolActivities?: ExecutionProgressToolActivity[];
|
||||
reasoningSummary?: ExecutionProgressReasoningSummary;
|
||||
streamEvents?: ExecutionProgressStreamEvents;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -335,6 +347,15 @@ export interface ExecutionProgressInput {
|
||||
content?: unknown;
|
||||
itemId?: unknown;
|
||||
};
|
||||
streamEvents?: Partial<ExecutionProgressStreamEvents> & {
|
||||
delta?: unknown;
|
||||
text?: unknown;
|
||||
content?: unknown;
|
||||
output?: unknown;
|
||||
command?: unknown;
|
||||
itemId?: unknown;
|
||||
turnId?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ForwardSource {
|
||||
@@ -4027,6 +4048,7 @@ function normalizeExecutionProgressSnapshot(raw: Partial<ExecutionProgressSnapsh
|
||||
reasoningSummary: nativeRemoteControl
|
||||
? undefined
|
||||
: normalizeExecutionProgressReasoningSummary(raw.reasoningSummary),
|
||||
streamEvents: nativeRemoteControl ? undefined : normalizeExecutionProgressStreamEvents(raw.streamEvents),
|
||||
updatedAt: raw.updatedAt ?? nowIso(),
|
||||
};
|
||||
}
|
||||
@@ -5871,6 +5893,39 @@ function normalizeExecutionProgressReasoningSummary(
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStreamEventCount(value: unknown) {
|
||||
const count = normalizeOptionalNumber(value);
|
||||
return count === undefined ? undefined : Math.max(0, Math.min(9999, count));
|
||||
}
|
||||
|
||||
function normalizeExecutionProgressStreamEvents(
|
||||
input?: ExecutionProgressInput["streamEvents"],
|
||||
): ExecutionProgressStreamEvents | undefined {
|
||||
if (!input) {
|
||||
return undefined;
|
||||
}
|
||||
const streamEvents: ExecutionProgressStreamEvents = {
|
||||
status: safeExecutionProgressText(input.status) || "streaming",
|
||||
agentDeltaCount: normalizeStreamEventCount(input.agentDeltaCount),
|
||||
planDeltaCount: normalizeStreamEventCount(input.planDeltaCount),
|
||||
reasoningDeltaCount: normalizeStreamEventCount(input.reasoningDeltaCount),
|
||||
toolProgressCount: normalizeStreamEventCount(input.toolProgressCount),
|
||||
commandOutputChunkCount: normalizeStreamEventCount(input.commandOutputChunkCount),
|
||||
terminalInteractionCount: normalizeStreamEventCount(input.terminalInteractionCount),
|
||||
fileOutputChunkCount: normalizeStreamEventCount(input.fileOutputChunkCount),
|
||||
};
|
||||
const hasCount = [
|
||||
streamEvents.agentDeltaCount,
|
||||
streamEvents.planDeltaCount,
|
||||
streamEvents.reasoningDeltaCount,
|
||||
streamEvents.toolProgressCount,
|
||||
streamEvents.commandOutputChunkCount,
|
||||
streamEvents.terminalInteractionCount,
|
||||
streamEvents.fileOutputChunkCount,
|
||||
].some((count) => typeof count === "number" && count > 0);
|
||||
return hasCount ? streamEvents : undefined;
|
||||
}
|
||||
|
||||
function defaultExecutionProgressStepTexts(task: Pick<MasterAgentTask, "taskType" | "relayViaMasterAgent">) {
|
||||
if (task.taskType === "browser_control") {
|
||||
return [
|
||||
@@ -6029,6 +6084,7 @@ function buildExecutionProgressSnapshot(
|
||||
reasoningSummary: nativeRemoteControl
|
||||
? undefined
|
||||
: normalizeExecutionProgressReasoningSummary(input?.reasoningSummary),
|
||||
streamEvents: nativeRemoteControl ? undefined : normalizeExecutionProgressStreamEvents(input?.streamEvents),
|
||||
updatedAt: nowIso(),
|
||||
};
|
||||
}
|
||||
|
||||
77
tests/fixtures/codex-app-server-runtime.mjs
vendored
77
tests/fixtures/codex-app-server-runtime.mjs
vendored
@@ -1401,6 +1401,83 @@ rl.on("line", (line) => {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_STREAM_DELTA_EVENTS === "1") {
|
||||
send({
|
||||
method: "item/plan/delta",
|
||||
params: {
|
||||
threadId: message.params?.threadId,
|
||||
turnId: "turn-fixture",
|
||||
itemId: "plan-delta-secret-should-not-leak",
|
||||
delta: "secret plan delta should not leak",
|
||||
},
|
||||
});
|
||||
send({
|
||||
method: "item/reasoning/summaryPartAdded",
|
||||
params: {
|
||||
threadId: message.params?.threadId,
|
||||
turnId: "turn-fixture",
|
||||
itemId: "reasoning-delta-secret-should-not-leak",
|
||||
summaryIndex: 0,
|
||||
part: "secret reasoning summary part should not leak",
|
||||
},
|
||||
});
|
||||
send({
|
||||
method: "item/reasoning/summaryTextDelta",
|
||||
params: {
|
||||
threadId: message.params?.threadId,
|
||||
turnId: "turn-fixture",
|
||||
itemId: "reasoning-delta-secret-should-not-leak",
|
||||
summaryIndex: 0,
|
||||
delta: "secret reasoning summary delta should not leak",
|
||||
},
|
||||
});
|
||||
send({
|
||||
method: "item/reasoning/textDelta",
|
||||
params: {
|
||||
threadId: message.params?.threadId,
|
||||
turnId: "turn-fixture",
|
||||
itemId: "reasoning-delta-secret-should-not-leak",
|
||||
delta: "secret raw reasoning delta should not leak",
|
||||
},
|
||||
});
|
||||
send({
|
||||
method: "item/mcpToolCall/progress",
|
||||
params: {
|
||||
threadId: message.params?.threadId,
|
||||
turnId: "turn-fixture",
|
||||
itemId: "mcp-progress-secret-should-not-leak",
|
||||
message: "secret mcp progress should not leak",
|
||||
},
|
||||
});
|
||||
send({
|
||||
method: "item/commandExecution/outputDelta",
|
||||
params: {
|
||||
threadId: message.params?.threadId,
|
||||
turnId: "turn-fixture",
|
||||
itemId: "command-output-secret-should-not-leak",
|
||||
stream: "stdout",
|
||||
delta: "secret command output should not leak",
|
||||
},
|
||||
});
|
||||
send({
|
||||
method: "item/commandExecution/terminalInteraction",
|
||||
params: {
|
||||
threadId: message.params?.threadId,
|
||||
turnId: "turn-fixture",
|
||||
itemId: "terminal-interaction-secret-should-not-leak",
|
||||
input: "secret terminal input should not leak",
|
||||
},
|
||||
});
|
||||
send({
|
||||
method: "item/fileChange/outputDelta",
|
||||
params: {
|
||||
threadId: message.params?.threadId,
|
||||
turnId: "turn-fixture",
|
||||
itemId: "file-output-secret-should-not-leak",
|
||||
delta: "secret file output should not leak",
|
||||
},
|
||||
});
|
||||
}
|
||||
send({
|
||||
method: "item/agentMessage/delta",
|
||||
params: {
|
||||
|
||||
@@ -933,6 +933,56 @@ test("codex app-server runner maps collab tool calls and context compaction with
|
||||
}
|
||||
});
|
||||
|
||||
test("codex app-server runner summarizes stream deltas without leaking raw delta content", async () => {
|
||||
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_STREAM_DELTA_EVENTS;
|
||||
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_STREAM_DELTA_EVENTS = "1";
|
||||
try {
|
||||
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
|
||||
codexAppServerEnabled: true,
|
||||
codexAppServerCommand: process.execPath,
|
||||
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
|
||||
codexAppServerWorkdir: repoRoot,
|
||||
codexAppServerTimeoutMs: 5000,
|
||||
masterAgentModel: "gpt-5.4",
|
||||
});
|
||||
|
||||
const result = await executeCodexAppServerTask(runnerConfig, {
|
||||
taskId: "task-app-server-stream-deltas",
|
||||
taskType: "conversation_reply",
|
||||
targetCodexThreadRef: "019d-app-server-thread",
|
||||
targetCodexFolderRef: repoRoot,
|
||||
executionPrompt: "检查 Codex 流式增量进度",
|
||||
});
|
||||
|
||||
assert.equal(result.status, "completed");
|
||||
assert.deepEqual(result.executionProgress.streamEvents, {
|
||||
status: "completed",
|
||||
agentDeltaCount: 1,
|
||||
planDeltaCount: 1,
|
||||
reasoningDeltaCount: 3,
|
||||
toolProgressCount: 1,
|
||||
commandOutputChunkCount: 1,
|
||||
terminalInteractionCount: 1,
|
||||
fileOutputChunkCount: 1,
|
||||
});
|
||||
const serialized = JSON.stringify(result.executionProgress);
|
||||
assert.equal(serialized.includes("secret plan delta should not leak"), false);
|
||||
assert.equal(serialized.includes("secret reasoning summary part should not leak"), false);
|
||||
assert.equal(serialized.includes("secret reasoning summary delta should not leak"), false);
|
||||
assert.equal(serialized.includes("secret raw reasoning delta should not leak"), false);
|
||||
assert.equal(serialized.includes("secret mcp progress should not leak"), false);
|
||||
assert.equal(serialized.includes("secret command output should not leak"), false);
|
||||
assert.equal(serialized.includes("secret terminal input should not leak"), false);
|
||||
assert.equal(serialized.includes("secret file output should not leak"), false);
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_STREAM_DELTA_EVENTS;
|
||||
} else {
|
||||
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_STREAM_DELTA_EVENTS = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("codex app-server runner maps tool, search, image, review, and command activities without leaking payloads", async () => {
|
||||
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_TOOL_ACTIVITY_EVENTS;
|
||||
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_TOOL_ACTIVITY_EVENTS = "1";
|
||||
|
||||
@@ -528,6 +528,78 @@ test("POST task progress preserves Codex thread collaboration summaries without
|
||||
assert.equal(serialized.includes("turn-secret-should-not-persist"), false);
|
||||
});
|
||||
|
||||
test("POST task progress preserves Codex stream event counters without leaking raw deltas", async () => {
|
||||
const task = await data.queueMasterAgentTask({
|
||||
taskId: "route-progress-stream-events-task",
|
||||
projectId: "group-progress-test",
|
||||
taskType: "dispatch_execution",
|
||||
requestMessageId: "msg-route-progress-stream-events",
|
||||
requestText: "检查 Codex 流式增量",
|
||||
executionPrompt: "检查 Codex 流式增量",
|
||||
requestedBy: "krisolo",
|
||||
requestedByAccount: "krisolo",
|
||||
deviceId: "mac-studio",
|
||||
targetProjectId: "master-agent",
|
||||
targetThreadId: "master-agent-thread",
|
||||
});
|
||||
await data.claimNextMasterAgentTask("mac-studio");
|
||||
|
||||
const response = await postProgress(
|
||||
new NextRequest(`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/progress`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-boss-device-token": "boss-mac-studio-token",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
deviceId: "mac-studio",
|
||||
status: "running",
|
||||
executionProgress: {
|
||||
steps: [{ text: "接收 Codex 流式事件", status: "running" }],
|
||||
streamEvents: {
|
||||
status: "streaming",
|
||||
agentDeltaCount: 2,
|
||||
planDeltaCount: 1,
|
||||
reasoningDeltaCount: 3,
|
||||
toolProgressCount: 1,
|
||||
commandOutputChunkCount: 4,
|
||||
terminalInteractionCount: 1,
|
||||
fileOutputChunkCount: 1,
|
||||
delta: "secret delta should-not-persist",
|
||||
text: "secret text should-not-persist",
|
||||
output: "secret output should-not-persist",
|
||||
content: "secret content should-not-persist",
|
||||
command: "cat secret should-not-persist",
|
||||
turnId: "turn-secret-should-not-persist",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
{ params: Promise.resolve({ taskId: task.taskId }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
const state = await data.readState();
|
||||
const progress = state.projects
|
||||
.find((project) => project.id === "master-agent")
|
||||
?.messages.find((message) => message.executionProgress?.taskId === task.taskId)
|
||||
?.executionProgress;
|
||||
assert.deepEqual(progress?.streamEvents, {
|
||||
status: "streaming",
|
||||
agentDeltaCount: 2,
|
||||
planDeltaCount: 1,
|
||||
reasoningDeltaCount: 3,
|
||||
toolProgressCount: 1,
|
||||
commandOutputChunkCount: 4,
|
||||
terminalInteractionCount: 1,
|
||||
fileOutputChunkCount: 1,
|
||||
});
|
||||
const serialized = JSON.stringify(progress);
|
||||
assert.equal(serialized.includes("should-not-persist"), false);
|
||||
assert.equal(serialized.includes("turn-secret"), false);
|
||||
});
|
||||
|
||||
test("POST task progress preserves Codex tool activity summaries without leaking raw payloads", async () => {
|
||||
const task = await data.queueMasterAgentTask({
|
||||
taskId: "route-progress-tool-activity-task",
|
||||
|
||||
Reference in New Issue
Block a user