merge(session): finalisation Codex — réponse non finalisée sur session Codex — #240 (QA verte)
This commit is contained in:
@ -699,17 +699,21 @@ impl CodexExecSession {
|
||||
}
|
||||
|
||||
fn event_supersedes_pending_final(event: &ReplyEvent) -> bool {
|
||||
!matches!(
|
||||
event,
|
||||
ReplyEvent::Heartbeat
|
||||
| ReplyEvent::RateLimited { .. }
|
||||
| ReplyEvent::Progress {
|
||||
progress: ReplyProgress {
|
||||
kind: ReplyProgressKind::Turn,
|
||||
..
|
||||
}
|
||||
}
|
||||
match event {
|
||||
ReplyEvent::Progress { progress } => {
|
||||
progress.kind != ReplyProgressKind::Turn
|
||||
&& matches!(
|
||||
progress.stage,
|
||||
ReplyProgressStage::Started | ReplyProgressStage::Delta
|
||||
)
|
||||
}
|
||||
ReplyEvent::ToolActivity { .. } | ReplyEvent::Error { .. } => true,
|
||||
ReplyEvent::Final { .. }
|
||||
| ReplyEvent::TextDelta { .. }
|
||||
| ReplyEvent::Announcement { .. }
|
||||
| ReplyEvent::Heartbeat
|
||||
| ReplyEvent::RateLimited { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_message_progress_from_line(line: &str) -> Result<Option<ReplyEvent>, AgentSessionError> {
|
||||
|
||||
@ -1385,6 +1385,41 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_agent_message_survives_late_completed_item_as_final() {
|
||||
let fake = FakeCli::printing(&[
|
||||
r#"{"type":"thread.started","thread_id":"c"}"#,
|
||||
r#"{"type":"turn.started"}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"agent_message","text":"réponse exploitable"}}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i1","type":"reasoning","text":"post-traitement"}}"#,
|
||||
r#"{"type":"turn.completed","usage":{}}"#,
|
||||
]);
|
||||
let s = CodexExecSession::new(
|
||||
SessionId::new_random(),
|
||||
fake.command(),
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let events: Vec<_> = s.send("x").await.expect("send").collect();
|
||||
let finals: Vec<_> = events
|
||||
.iter()
|
||||
.filter_map(|event| match event {
|
||||
ReplyEvent::Final { content } => Some(content.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
finals,
|
||||
vec!["réponse exploitable"],
|
||||
"a late completed Codex item must not erase the last agent_message"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_zero_agent_message_has_no_final() {
|
||||
let fake = FakeCli::printing(&[
|
||||
|
||||
@ -383,6 +383,7 @@ describe("CustomAgentChatView", () => {
|
||||
|
||||
expect(agentBubble).toBeTruthy();
|
||||
expect(errorBubble).toBeTruthy();
|
||||
expect(within(errorBubble!).getByText("Session Codex")).toBeTruthy();
|
||||
expect(
|
||||
within(agentBubble!).getByText(expectedChatTime(firstDeltaReceivedAtMs)),
|
||||
).toBeTruthy();
|
||||
|
||||
@ -144,6 +144,23 @@ function describeOpenFailure(e: unknown): string {
|
||||
return message || "Cause technique non disponible.";
|
||||
}
|
||||
|
||||
function structuredSessionLabel(
|
||||
adapter: AgentProfile["structuredAdapter"],
|
||||
): string | null {
|
||||
switch (adapter) {
|
||||
case "codex":
|
||||
return "Session Codex";
|
||||
case "claude":
|
||||
return "Session Claude";
|
||||
case "openCode":
|
||||
return "Session OpenCode";
|
||||
case "openAiCompatible":
|
||||
return "Session HTTP";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function instrumentationError(e: unknown): Record<string, unknown> {
|
||||
if (e && typeof e === "object") {
|
||||
const err = e as GatewayError & { name?: string };
|
||||
@ -1018,6 +1035,7 @@ export function CustomAgentChatView({
|
||||
[agentId, conversationId, projectId],
|
||||
);
|
||||
const displayTurns = turns;
|
||||
const sessionLabel = structuredSessionLabel(profile.structuredAdapter);
|
||||
const [requesterDelegation, setRequesterDelegation] =
|
||||
useState<RequesterDelegationState>({ active: false });
|
||||
|
||||
@ -2162,6 +2180,7 @@ export function CustomAgentChatView({
|
||||
key={index}
|
||||
turn={turn}
|
||||
requesterName={agentName}
|
||||
sessionLabel={sessionLabel}
|
||||
expandedProgressKeys={expandedProgressKeys}
|
||||
onToggleProgressExpanded={(key) =>
|
||||
setExpandedProgressKeys((prev) => {
|
||||
@ -2352,11 +2371,13 @@ export function CustomAgentChatView({
|
||||
function ChatBubble({
|
||||
turn,
|
||||
requesterName,
|
||||
sessionLabel,
|
||||
expandedProgressKeys,
|
||||
onToggleProgressExpanded,
|
||||
}: {
|
||||
turn: ChatTurn;
|
||||
requesterName: string;
|
||||
sessionLabel: string | null;
|
||||
expandedProgressKeys: Set<string>;
|
||||
onToggleProgressExpanded: (key: string) => void;
|
||||
}) {
|
||||
@ -2406,6 +2427,9 @@ function ChatBubble({
|
||||
labelClassName="font-semibold text-danger"
|
||||
/>
|
||||
)}
|
||||
{turn.role === "error" && sessionLabel && (
|
||||
<p className="mb-1 font-medium text-danger/85">{sessionLabel}</p>
|
||||
)}
|
||||
{turn.role === "unknown" ? "Chunk inconnu reçu: " : ""}
|
||||
<span className="whitespace-pre-wrap break-words">{turn.text}</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user