fix(server): preserve codex assistant item boundaries
This commit is contained in:
@@ -983,6 +983,51 @@ describe("Codex app-server provider", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("emits a markdown divider when a new Codex assistant item starts after the previous one completed", () => {
|
||||||
|
const session = createSession();
|
||||||
|
const events: AgentStreamEvent[] = [];
|
||||||
|
session.subscribe((event) => events.push(event));
|
||||||
|
|
||||||
|
asInternals(session).handleNotification("item/agentMessage/delta", {
|
||||||
|
itemId: "assistant-item-3",
|
||||||
|
delta:
|
||||||
|
"I’m in the waiting phase now. The next read is intentionally delayed so we get meaningful CI state instead of churn.",
|
||||||
|
});
|
||||||
|
asInternals(session).handleNotification("item/completed", {
|
||||||
|
item: {
|
||||||
|
id: "assistant-item-3",
|
||||||
|
type: "agentMessage",
|
||||||
|
text: "I’m in the waiting phase now. The next read is intentionally delayed so we get meaningful CI state instead of churn.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
asInternals(session).handleNotification("item/agentMessage/delta", {
|
||||||
|
itemId: "assistant-item-4",
|
||||||
|
delta:
|
||||||
|
"CI is still cooking. I’m staying on the current run rather than jumping around, because the first red job will tell us exactly whether anything else needs work.",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(events).toEqual([
|
||||||
|
{
|
||||||
|
type: "timeline",
|
||||||
|
provider: "codex",
|
||||||
|
turnId: "test-turn",
|
||||||
|
item: {
|
||||||
|
type: "assistant_message",
|
||||||
|
text: "I’m in the waiting phase now. The next read is intentionally delayed so we get meaningful CI state instead of churn.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "timeline",
|
||||||
|
provider: "codex",
|
||||||
|
turnId: "test-turn",
|
||||||
|
item: {
|
||||||
|
type: "assistant_message",
|
||||||
|
text: "\n\n---\n\nCI is still cooking. I’m staying on the current run rather than jumping around, because the first red job will tell us exactly whether anything else needs work.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test("streams Codex reasoning deltas and does not replay completed reasoning", () => {
|
test("streams Codex reasoning deltas and does not replay completed reasoning", () => {
|
||||||
const session = createSession();
|
const session = createSession();
|
||||||
const events: AgentStreamEvent[] = [];
|
const events: AgentStreamEvent[] = [];
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ const APP_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT_MS = 2_000;
|
|||||||
const APP_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS = 1_000;
|
const APP_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS = 1_000;
|
||||||
const CODEX_PROVIDER = "codex" as const;
|
const CODEX_PROVIDER = "codex" as const;
|
||||||
const CODEX_IMAGE_ATTACHMENT_DIR = "paseo-attachments";
|
const CODEX_IMAGE_ATTACHMENT_DIR = "paseo-attachments";
|
||||||
|
const ASSISTANT_MESSAGE_BOUNDARY_MARKDOWN = "\n\n---\n\n";
|
||||||
const CODEX_PLAN_IMPLEMENTATION_PROMPT_PREFIX =
|
const CODEX_PLAN_IMPLEMENTATION_PROMPT_PREFIX =
|
||||||
"The user approved the plan. Implement it now. Do not restate or revise the plan unless blocked.";
|
"The user approved the plan. Implement it now. Do not restate or revise the plan unless blocked.";
|
||||||
|
|
||||||
@@ -2539,6 +2540,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
private pendingReasoning = new Map<string, string[]>();
|
private pendingReasoning = new Map<string, string[]>();
|
||||||
private pendingCommandOutputDeltas = new Map<string, string[]>();
|
private pendingCommandOutputDeltas = new Map<string, string[]>();
|
||||||
private pendingFileChangeOutputDeltas = new Map<string, string[]>();
|
private pendingFileChangeOutputDeltas = new Map<string, string[]>();
|
||||||
|
private pendingAssistantMessageBoundary = false;
|
||||||
private terminalCommandByProcessId = new Map<string, string>();
|
private terminalCommandByProcessId = new Map<string, string>();
|
||||||
private pendingUnlabeledTerminalInteractions = new Set<string>();
|
private pendingUnlabeledTerminalInteractions = new Set<string>();
|
||||||
private emittedTerminalInteractionKeys = new Set<string>();
|
private emittedTerminalInteractionKeys = new Set<string>();
|
||||||
@@ -3633,11 +3635,21 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
if (parsed.kind === "agent_message_delta") {
|
if (parsed.kind === "agent_message_delta") {
|
||||||
const prev = this.pendingAgentMessages.get(parsed.itemId) ?? "";
|
const prev = this.pendingAgentMessages.get(parsed.itemId) ?? "";
|
||||||
this.pendingAgentMessages.set(parsed.itemId, prev + parsed.delta);
|
this.pendingAgentMessages.set(parsed.itemId, prev + parsed.delta);
|
||||||
|
const isFirstDeltaForItem = prev.length === 0;
|
||||||
this.emitEvent({
|
this.emitEvent({
|
||||||
type: "timeline",
|
type: "timeline",
|
||||||
provider: CODEX_PROVIDER,
|
provider: CODEX_PROVIDER,
|
||||||
item: { type: "assistant_message", text: parsed.delta },
|
item: {
|
||||||
|
type: "assistant_message",
|
||||||
|
text:
|
||||||
|
isFirstDeltaForItem && this.pendingAssistantMessageBoundary
|
||||||
|
? `${ASSISTANT_MESSAGE_BOUNDARY_MARKDOWN}${parsed.delta}`
|
||||||
|
: parsed.delta,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
if (isFirstDeltaForItem) {
|
||||||
|
this.pendingAssistantMessageBoundary = false;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (parsed.kind === "reasoning_delta") {
|
if (parsed.kind === "reasoning_delta") {
|
||||||
@@ -3714,6 +3726,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
this.pendingReasoning.clear();
|
this.pendingReasoning.clear();
|
||||||
this.pendingCommandOutputDeltas.clear();
|
this.pendingCommandOutputDeltas.clear();
|
||||||
this.pendingFileChangeOutputDeltas.clear();
|
this.pendingFileChangeOutputDeltas.clear();
|
||||||
|
this.pendingAssistantMessageBoundary = false;
|
||||||
this.warnedIncompleteEditToolCallIds.clear();
|
this.warnedIncompleteEditToolCallIds.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3893,6 +3906,9 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.consumeStreamedTextCompletion(timelineItem, itemId)) {
|
if (this.consumeStreamedTextCompletion(timelineItem, itemId)) {
|
||||||
|
if (timelineItem.type === "assistant_message") {
|
||||||
|
this.pendingAssistantMessageBoundary = true;
|
||||||
|
}
|
||||||
if (itemId) {
|
if (itemId) {
|
||||||
this.emittedItemCompletedIds.add(itemId);
|
this.emittedItemCompletedIds.add(itemId);
|
||||||
this.emittedItemStartedIds.delete(itemId);
|
this.emittedItemStartedIds.delete(itemId);
|
||||||
@@ -3912,6 +3928,9 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
this.warnOnIncompleteEditToolCall(timelineItem, "item_completed", parsed.item);
|
this.warnOnIncompleteEditToolCall(timelineItem, "item_completed", parsed.item);
|
||||||
}
|
}
|
||||||
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
|
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
|
||||||
|
if (timelineItem.type === "assistant_message") {
|
||||||
|
this.pendingAssistantMessageBoundary = true;
|
||||||
|
}
|
||||||
if (itemId) {
|
if (itemId) {
|
||||||
this.emittedItemCompletedIds.add(itemId);
|
this.emittedItemCompletedIds.add(itemId);
|
||||||
this.emittedItemStartedIds.delete(itemId);
|
this.emittedItemStartedIds.delete(itemId);
|
||||||
|
|||||||
Reference in New Issue
Block a user