mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 01:50:13 +01:00
fix(task-chat): keep a yielded reply after its card is answered
A run that posted a question card is waiting on that card. Once the card was answered, the turn became an accepted response wake and the wait summary replaced the agent's reply. Any non-suppressed card from the run now keeps it out of the response-wake path. Wait stubs in the backward scan are skipped, so a stub echoed after the authored reply no longer hides it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1ccf0f9bc
commit
c134d72620
4 files changed
+40
-5
No files matched your search
@@ -3242,6 +3242,29 @@ describe("TaskChatThread runtime transcript selection", () => {
|
|||||||
expect(container.textContent).not.toContain(
|
expect(container.textContent).not.toContain(
|
||||||
"Waiting for user answers on the interview question card.",
|
"Waiting for user answers on the interview question card.",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 4. Answered: the card leaves pending and the turn becomes an accepted
|
||||||
|
// response wake. The authored reply must keep its slot, not be replaced
|
||||||
|
// by the wait summary.
|
||||||
|
renderThread({
|
||||||
|
comments: [],
|
||||||
|
onAdd: async () => {},
|
||||||
|
issueStatus: "in_progress",
|
||||||
|
interactions: [
|
||||||
|
{
|
||||||
|
...question,
|
||||||
|
status: "answered",
|
||||||
|
resolvedAt: new Date("2026-08-25T18:01:00.000Z"),
|
||||||
|
updatedAt: new Date("2026-08-25T18:01:00.000Z"),
|
||||||
|
} as IssueThreadInteraction,
|
||||||
|
],
|
||||||
|
activeRun: undefined,
|
||||||
|
linkedRuns: [linkedRun("succeeded")],
|
||||||
|
});
|
||||||
|
expect(container.textContent).toContain("I've posted a question card");
|
||||||
|
expect(container.textContent).not.toContain(
|
||||||
|
"Waiting for user answers on the interview question card.",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps a yielded question turn out of the final-response slot", () => {
|
it("keeps a yielded question turn out of the final-response slot", () => {
|
||||||
|
|||||||
@@ -1716,14 +1716,17 @@ export function TaskChatThread(props: TaskChatThreadProps) {
|
|||||||
agentName: meta?.agentName,
|
agentName: meta?.agentName,
|
||||||
running: false,
|
running: false,
|
||||||
});
|
});
|
||||||
const sourceHasPendingAttention = (interactions ?? []).some(
|
// A run that posted a card yielded to that card. Its wake summary
|
||||||
|
// describes the wait, not an answer, so it never becomes the turn's
|
||||||
|
// reply, even after the card is answered.
|
||||||
|
const sourceHasAttentionCard = (interactions ?? []).some(
|
||||||
(interaction) =>
|
(interaction) =>
|
||||||
interaction.sourceRunId === source.id &&
|
interaction.sourceRunId === source.id &&
|
||||||
interaction.status === "pending",
|
!isSuppressedThreadInteraction(interaction),
|
||||||
);
|
);
|
||||||
const sourceAcceptedResponseWake =
|
const sourceAcceptedResponseWake =
|
||||||
source.status === "succeeded" &&
|
source.status === "succeeded" &&
|
||||||
!sourceHasPendingAttention &&
|
!sourceHasAttentionCard &&
|
||||||
paperclipRunnerAcceptedResponseWake(parsedSource, source.id);
|
paperclipRunnerAcceptedResponseWake(parsedSource, source.id);
|
||||||
const sourceYielded =
|
const sourceYielded =
|
||||||
(acceptedSemanticResultDisposition(meta?.resultJson) === "yielded" ||
|
(acceptedSemanticResultDisposition(meta?.resultJson) === "yielded" ||
|
||||||
|
|||||||
@@ -1237,6 +1237,14 @@ describe("buildTurnTimelineRows (DOT-217)", () => {
|
|||||||
} satisfies TaskChatItem;
|
} satisfies TaskChatItem;
|
||||||
expect(paperclipRunnerFinalResponse([reply, result])).toBe(reply);
|
expect(paperclipRunnerFinalResponse([reply, result])).toBe(reply);
|
||||||
expect(paperclipRunnerFinalResponse([reply], { yielded: true })).toBe(reply);
|
expect(paperclipRunnerFinalResponse([reply], { yielded: true })).toBe(reply);
|
||||||
|
// A provider may echo the wait stub after its authored reply.
|
||||||
|
expect(
|
||||||
|
paperclipRunnerFinalResponse([
|
||||||
|
reply,
|
||||||
|
{ ...reply, id: "stub", text: result.summary },
|
||||||
|
result,
|
||||||
|
]),
|
||||||
|
).toBe(reply);
|
||||||
expect(
|
expect(
|
||||||
paperclipRunnerFinalResponse(
|
paperclipRunnerFinalResponse(
|
||||||
[{ ...reply, text: "Waiting for Which tools should the storefront use?." }],
|
[{ ...reply, text: "Waiting for Which tools should the storefront use?." }],
|
||||||
|
|||||||
@@ -1483,7 +1483,8 @@ export function paperclipRunnerFinalResponse(
|
|||||||
// The wait itself is not an answer, but prose the agent wrote for the user
|
// The wait itself is not an answer, but prose the agent wrote for the user
|
||||||
// before yielding (e.g. explaining the question card it just posted) is.
|
// before yielding (e.g. explaining the question card it just posted) is.
|
||||||
// It streamed into view while the turn was live; dropping it on settle
|
// It streamed into view while the turn was live; dropping it on settle
|
||||||
// makes the conversation flash and lose that reply.
|
// makes the conversation flash and lose that reply. Wait stubs are
|
||||||
|
// skipped, not terminal: a provider may echo one after its reply.
|
||||||
const waitSummaries = [
|
const waitSummaries = [
|
||||||
options?.waitSummary,
|
options?.waitSummary,
|
||||||
...yieldedResults.map((result) => result.summary),
|
...yieldedResults.map((result) => result.summary),
|
||||||
@@ -1491,7 +1492,7 @@ export function paperclipRunnerFinalResponse(
|
|||||||
for (let index = parsed.length - 1; index >= 0; index -= 1) {
|
for (let index = parsed.length - 1; index >= 0; index -= 1) {
|
||||||
const item = parsed[index];
|
const item = parsed[index];
|
||||||
if (item.kind !== "message" || item.channel !== "final") continue;
|
if (item.kind !== "message" || item.channel !== "final") continue;
|
||||||
if (isYieldedWaitStub(item.text, waitSummaries)) return undefined;
|
if (isYieldedWaitStub(item.text, waitSummaries)) continue;
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
Reference in new issue
Block a user