通知分类收敛成生产读取器与单测共用的唯一实现
- direct_codex_notification_event 收编 resolution / request / item / rawItem / Terminal 全部分支,并接收 turnId
- 读取器删掉自己那份内联分类,只保留「必须有 turnId 才处理」与活动/正文节流
- agentMessage 缺 itemId 的告警与 direct-missing-item:{turnId} 兜底身份原样保留,避免行为变化
- 新增 direct_notification_classification_covers_the_reader_branches 钉住各分支;E2E 假 app-server 用例继续覆盖读取器
This commit is contained in:
@@ -1092,15 +1092,29 @@ fn direct_codex_reasoning_delta_event(
|
||||
})
|
||||
}
|
||||
|
||||
/// 通知 → 回合事件的唯一分类函数:运行态读取器与单测共用这一份。
|
||||
///
|
||||
/// 读取器只负责"必须有 turnId 才处理"的前置条件与节流(活动 / 正文),分类不在这里之外
|
||||
/// 再做一遍——两份实现分叉过一次,结果是生产路径从不产生 `ReasoningDelta`。
|
||||
///
|
||||
/// Preparing notifications may carry private plan/reasoning text; expose only the safe
|
||||
/// activity category. Other categories may retain their bounded, redacted intermediate text.
|
||||
fn direct_codex_notification_event(
|
||||
method: &str,
|
||||
params: &serde_json::Value,
|
||||
intermediate_text: Option<String>,
|
||||
safe_activity: Option<&'static str>,
|
||||
turn_id: &str,
|
||||
) -> Option<CodexTurnEvent> {
|
||||
if let Some(event) = direct_codex_reasoning_delta_event(method, params) {
|
||||
return Some(event);
|
||||
}
|
||||
if let Some(kind) = direct_codex_resolution_event_type(method) {
|
||||
return Some(CodexTurnEvent::Request {
|
||||
kind,
|
||||
params: params.clone(),
|
||||
});
|
||||
}
|
||||
let (activity, intermediate_text) = match (&intermediate_text, safe_activity) {
|
||||
(Some(_), Some(activity)) if activity == "preparing" => (Some(activity), None),
|
||||
_ => (safe_activity, intermediate_text),
|
||||
@@ -1115,14 +1129,19 @@ fn direct_codex_notification_event(
|
||||
"item/agentMessage/delta" => params
|
||||
.get("delta")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|delta| {
|
||||
let item_id = params
|
||||
.get("itemId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| "direct-missing-item".to_string());
|
||||
.unwrap_or_else(|| {
|
||||
eprintln!(
|
||||
"agent.direct_codex.protocol_warning event=item/agentMessage/delta missing_item_id"
|
||||
);
|
||||
format!("direct-missing-item:{turn_id}")
|
||||
});
|
||||
CodexTurnEvent::AgentMessageDelta {
|
||||
item_id,
|
||||
delta: delta.to_string(),
|
||||
@@ -1138,6 +1157,13 @@ fn direct_codex_notification_event(
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
)),
|
||||
method if direct_codex_request_event_type(method).is_some() => {
|
||||
Some(CodexTurnEvent::Request {
|
||||
kind: direct_codex_request_event_type(method)
|
||||
.expect("request kind checked above"),
|
||||
params: params.clone(),
|
||||
})
|
||||
}
|
||||
_ => Some(CodexTurnEvent::Terminal(params.clone())),
|
||||
}
|
||||
}
|
||||
@@ -3999,8 +4025,8 @@ async fn read_game_creator_codex_app_server_stdout(
|
||||
// 思考正文走正文通道,不参与 `preparing` 活动的降级与节流:活动节流按类别抑制
|
||||
// 连续的 preparing,逐段思考正文会被整段吃掉——这正是生产路径此前从不产生
|
||||
// `ReasoningDelta` 的原因。正文仍沿用下面的正文节流,避免重复 chunk 反复入队。
|
||||
let reasoning_delta = direct_codex_reasoning_delta_event(method, ¶ms);
|
||||
if reasoning_delta.is_none() {
|
||||
let is_reasoning_delta = direct_codex_reasoning_delta_event(method, ¶ms).is_some();
|
||||
if !is_reasoning_delta {
|
||||
if let Some(activity) = safe_activity {
|
||||
let last_activity = last_direct_activity_by_turn
|
||||
.entry(turn_id.clone())
|
||||
@@ -4018,68 +4044,14 @@ async fn read_game_creator_codex_app_server_stdout(
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let event = if let Some(event) = reasoning_delta {
|
||||
event
|
||||
} else if let Some(kind) = direct_codex_resolution_event_type(method) {
|
||||
CodexTurnEvent::Request { kind, params }
|
||||
} else if let Some(activity) = safe_activity {
|
||||
// Preparing notifications may carry private plan/reasoning text;
|
||||
// expose only the safe activity category. Other categories may
|
||||
// retain their bounded, redacted intermediate text below.
|
||||
if activity == "preparing" {
|
||||
CodexTurnEvent::Activity(activity)
|
||||
} else if let Some(text) = intermediate_text {
|
||||
CodexTurnEvent::IntermediateText(text)
|
||||
} else {
|
||||
CodexTurnEvent::Activity(activity)
|
||||
}
|
||||
} else if let Some(text) = intermediate_text {
|
||||
CodexTurnEvent::IntermediateText(text)
|
||||
} else {
|
||||
match method {
|
||||
"item/agentMessage/delta" => {
|
||||
let Some(delta) = params
|
||||
.get("delta")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let item_id = params
|
||||
.get("itemId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| {
|
||||
eprintln!(
|
||||
"agent.direct_codex.protocol_warning event=item/agentMessage/delta missing_item_id"
|
||||
);
|
||||
format!("direct-missing-item:{turn_id}")
|
||||
});
|
||||
CodexTurnEvent::AgentMessageDelta {
|
||||
item_id,
|
||||
delta: delta.to_string(),
|
||||
}
|
||||
}
|
||||
"item/started" | "item/completed" => CodexTurnEvent::Item {
|
||||
completed: method == "item/completed",
|
||||
params,
|
||||
},
|
||||
"rawResponseItem/completed" => CodexTurnEvent::RawItem(
|
||||
params
|
||||
.get("item")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
),
|
||||
method if direct_codex_request_event_type(method).is_some() => {
|
||||
CodexTurnEvent::Request {
|
||||
kind: direct_codex_request_event_type(method)
|
||||
.expect("request kind checked above"),
|
||||
params,
|
||||
}
|
||||
}
|
||||
_ => CodexTurnEvent::Terminal(params),
|
||||
}
|
||||
let Some(event) = direct_codex_notification_event(
|
||||
method,
|
||||
¶ms,
|
||||
intermediate_text,
|
||||
safe_activity,
|
||||
&turn_id,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let sender = if method == "turn/completed" {
|
||||
last_direct_activity_by_turn.remove(&turn_id);
|
||||
@@ -4824,6 +4796,7 @@ mod tests {
|
||||
&reasoning,
|
||||
Some("思考正文".to_string()),
|
||||
Some("preparing"),
|
||||
"turn-1",
|
||||
),
|
||||
Some(CodexTurnEvent::ReasoningDelta { .. })
|
||||
),
|
||||
@@ -4850,6 +4823,7 @@ mod tests {
|
||||
&plan,
|
||||
Some("private plan text must not leak".to_string()),
|
||||
Some("preparing"),
|
||||
"turn-1",
|
||||
),
|
||||
Some(CodexTurnEvent::Activity("preparing"))
|
||||
));
|
||||
@@ -4861,11 +4835,88 @@ mod tests {
|
||||
&command_output,
|
||||
None,
|
||||
Some("command-exec"),
|
||||
"turn-1",
|
||||
),
|
||||
Some(CodexTurnEvent::Activity("command-exec"))
|
||||
));
|
||||
}
|
||||
|
||||
/// 判据:读取器与单测共用同一个分类函数,这些分支的行为被钉在这里。
|
||||
///
|
||||
/// 参数:method / params / 正文候选 / 安全活动类别 / turnId。
|
||||
#[test]
|
||||
fn direct_notification_classification_covers_the_reader_branches() {
|
||||
let empty = serde_json::json!({});
|
||||
let resolved = serde_json::json!({ "requestId": "request-1" });
|
||||
assert!(matches!(
|
||||
direct_codex_notification_event(
|
||||
"serverRequest/resolved",
|
||||
&resolved,
|
||||
None,
|
||||
None,
|
||||
"turn-1"
|
||||
),
|
||||
Some(CodexTurnEvent::Request {
|
||||
kind: DirectThreadRequestKind::RequestResolved,
|
||||
..
|
||||
})
|
||||
));
|
||||
let approval = serde_json::json!({ "requestId": "request-2" });
|
||||
assert!(matches!(
|
||||
direct_codex_notification_event(
|
||||
"item/fileChange/requestApproval",
|
||||
&approval,
|
||||
None,
|
||||
None,
|
||||
"turn-1",
|
||||
),
|
||||
Some(CodexTurnEvent::Request {
|
||||
kind: DirectThreadRequestKind::ApprovalRequested,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(matches!(
|
||||
direct_codex_notification_event("item/started", &empty, None, None, "turn-1"),
|
||||
Some(CodexTurnEvent::Item {
|
||||
completed: false,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(matches!(
|
||||
direct_codex_notification_event(
|
||||
"rawResponseItem/completed",
|
||||
&serde_json::json!({ "item": { "type": "reasoning" } }),
|
||||
None,
|
||||
None,
|
||||
"turn-1",
|
||||
),
|
||||
Some(CodexTurnEvent::RawItem(_))
|
||||
));
|
||||
// 空正文的 assistant 增量既不产事件,也不产 Terminal 兜底。
|
||||
assert!(direct_codex_notification_event(
|
||||
"item/agentMessage/delta",
|
||||
&serde_json::json!({ "delta": "" }),
|
||||
None,
|
||||
None,
|
||||
"turn-1",
|
||||
)
|
||||
.is_none());
|
||||
assert!(matches!(
|
||||
direct_codex_notification_event(
|
||||
"item/agentMessage/delta",
|
||||
&serde_json::json!({ "delta": "正文" }),
|
||||
None,
|
||||
None,
|
||||
"turn-1",
|
||||
),
|
||||
Some(CodexTurnEvent::AgentMessageDelta { delta, .. }) if delta == "正文"
|
||||
));
|
||||
assert!(matches!(
|
||||
direct_codex_notification_event("turn/completed", &empty, None, None, "turn-1"),
|
||||
Some(CodexTurnEvent::Terminal(_))
|
||||
));
|
||||
}
|
||||
|
||||
fn test_llm() -> GameCreatorLlmConfig {
|
||||
GameCreatorLlmConfig {
|
||||
api_key: "fixture-secret".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user