merge(session): finalisation Codex — réponse non finalisée sur session Codex — #240 (QA verte)
This commit is contained in:
@ -699,18 +699,22 @@ impl CodexExecSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn event_supersedes_pending_final(event: &ReplyEvent) -> bool {
|
fn event_supersedes_pending_final(event: &ReplyEvent) -> bool {
|
||||||
!matches!(
|
match event {
|
||||||
event,
|
ReplyEvent::Progress { progress } => {
|
||||||
ReplyEvent::Heartbeat
|
progress.kind != ReplyProgressKind::Turn
|
||||||
| ReplyEvent::RateLimited { .. }
|
&& matches!(
|
||||||
| ReplyEvent::Progress {
|
progress.stage,
|
||||||
progress: ReplyProgress {
|
ReplyProgressStage::Started | ReplyProgressStage::Delta
|
||||||
kind: ReplyProgressKind::Turn,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
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> {
|
fn agent_message_progress_from_line(line: &str) -> Result<Option<ReplyEvent>, AgentSessionError> {
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
|
|||||||
@ -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]
|
#[tokio::test]
|
||||||
async fn codex_zero_agent_message_has_no_final() {
|
async fn codex_zero_agent_message_has_no_final() {
|
||||||
let fake = FakeCli::printing(&[
|
let fake = FakeCli::printing(&[
|
||||||
|
|||||||
@ -383,6 +383,7 @@ describe("CustomAgentChatView", () => {
|
|||||||
|
|
||||||
expect(agentBubble).toBeTruthy();
|
expect(agentBubble).toBeTruthy();
|
||||||
expect(errorBubble).toBeTruthy();
|
expect(errorBubble).toBeTruthy();
|
||||||
|
expect(within(errorBubble!).getByText("Session Codex")).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
within(agentBubble!).getByText(expectedChatTime(firstDeltaReceivedAtMs)),
|
within(agentBubble!).getByText(expectedChatTime(firstDeltaReceivedAtMs)),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
|||||||
@ -144,6 +144,23 @@ function describeOpenFailure(e: unknown): string {
|
|||||||
return message || "Cause technique non disponible.";
|
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> {
|
function instrumentationError(e: unknown): Record<string, unknown> {
|
||||||
if (e && typeof e === "object") {
|
if (e && typeof e === "object") {
|
||||||
const err = e as GatewayError & { name?: string };
|
const err = e as GatewayError & { name?: string };
|
||||||
@ -1018,6 +1035,7 @@ export function CustomAgentChatView({
|
|||||||
[agentId, conversationId, projectId],
|
[agentId, conversationId, projectId],
|
||||||
);
|
);
|
||||||
const displayTurns = turns;
|
const displayTurns = turns;
|
||||||
|
const sessionLabel = structuredSessionLabel(profile.structuredAdapter);
|
||||||
const [requesterDelegation, setRequesterDelegation] =
|
const [requesterDelegation, setRequesterDelegation] =
|
||||||
useState<RequesterDelegationState>({ active: false });
|
useState<RequesterDelegationState>({ active: false });
|
||||||
|
|
||||||
@ -2162,6 +2180,7 @@ export function CustomAgentChatView({
|
|||||||
key={index}
|
key={index}
|
||||||
turn={turn}
|
turn={turn}
|
||||||
requesterName={agentName}
|
requesterName={agentName}
|
||||||
|
sessionLabel={sessionLabel}
|
||||||
expandedProgressKeys={expandedProgressKeys}
|
expandedProgressKeys={expandedProgressKeys}
|
||||||
onToggleProgressExpanded={(key) =>
|
onToggleProgressExpanded={(key) =>
|
||||||
setExpandedProgressKeys((prev) => {
|
setExpandedProgressKeys((prev) => {
|
||||||
@ -2352,11 +2371,13 @@ export function CustomAgentChatView({
|
|||||||
function ChatBubble({
|
function ChatBubble({
|
||||||
turn,
|
turn,
|
||||||
requesterName,
|
requesterName,
|
||||||
|
sessionLabel,
|
||||||
expandedProgressKeys,
|
expandedProgressKeys,
|
||||||
onToggleProgressExpanded,
|
onToggleProgressExpanded,
|
||||||
}: {
|
}: {
|
||||||
turn: ChatTurn;
|
turn: ChatTurn;
|
||||||
requesterName: string;
|
requesterName: string;
|
||||||
|
sessionLabel: string | null;
|
||||||
expandedProgressKeys: Set<string>;
|
expandedProgressKeys: Set<string>;
|
||||||
onToggleProgressExpanded: (key: string) => void;
|
onToggleProgressExpanded: (key: string) => void;
|
||||||
}) {
|
}) {
|
||||||
@ -2406,6 +2427,9 @@ function ChatBubble({
|
|||||||
labelClassName="font-semibold text-danger"
|
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: " : ""}
|
{turn.role === "unknown" ? "Chunk inconnu reçu: " : ""}
|
||||||
<span className="whitespace-pre-wrap break-words">{turn.text}</span>
|
<span className="whitespace-pre-wrap break-words">{turn.text}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user