Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/assistant-useful-brief-parts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sapiom/harness": patch
---

Use the same usefulness predicate for part selection and message reduction. Preserve the latest useful text/tool/file parts and report all omitted parts without claiming they were necessarily earlier.
4 changes: 4 additions & 0 deletions packages/harness/docs/assistant-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,7 @@ the exact previous revision; compacted tasks and recorded real replies remain.
A failed Terminal binding publication can roll back only its original close
generation and exact proposed marker. A queued End retains its durable tombstone
or confirmed removal, and other sessions keep their own process and binding.

Brief part selection keeps the latest useful Assistant text, tool results and
attachments even when blank or omitted control parts follow them. Omission counts
remain explicit without incorrectly labeling every removed part as earlier.
71 changes: 70 additions & 1 deletion packages/harness/src/core/assistant-continuation-brief.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ it("reserves user context and the newest Assistant parts when both messages are
expect(frozen.text).toContain("excerpt truncated");
expect(frozen.text).toContain("assistant state 12:");
expect(frozen.text).not.toContain("assistant state 0:");
expect(frozen.text).toContain("1 earlier parts omitted.");
expect(frozen.text).toContain("1 part omitted.");
expect(frozen.text).toContain("incomplete; do not assume success");
});
it("keeps the newest useful Assistant state ahead of later empty native messages", () => {
Expand Down Expand Up @@ -170,3 +170,72 @@ it("fails without usable public history or with corrupt binding metadata", () =>
}),
).toThrow();
});

it.each(["blank", "omitted", "mixed"])(
"keeps the latest useful text, tool and attachment before thirteen %s parts",
(kind) => {
const record = fixture();
record.turns[0]!.messages.push({
id: "msg_state",
role: "assistant",
parentId: "msg_user_0",
createdAt: 2,
completedAt: 3,
parts: [
{
id: "prt_state",
type: "text",
text: "Latest result: tests pass; deployment pending.",
truncated: false,
},
{
id: "prt_tool",
type: "tool",
callId: "call_verify",
name: "test",
status: "completed",
input: "{}",
output: "12 tests passed",
error: null,
startedAt: 2,
completedAt: 3,
truncated: false,
},
{
id: "prt_file",
type: "file",
name: "test-report.txt",
mime: "text/plain",
},
...Array.from({ length: 13 }, (_, index) =>
kind === "omitted" || (kind === "mixed" && index % 2 === 0)
? {
id: `prt_empty_${index}`,
type: "omitted" as const,
nativeType: "step-finish",
}
: {
id: `prt_empty_${index}`,
type: "text" as const,
text: " \n\t ",
truncated: false,
},
),
],
});
record.messageCount = 2;
const frozen = buildAssistantContinuationBrief(record);
expect(frozen.text).toContain(
"Latest result: tests pass; deployment pending.",
);
expect(frozen.text).toContain("Recorded tool test (completed)");
expect(frozen.text).toContain("12 tests passed");
expect(frozen.text).toContain(
"Attachment test-report.txt (text/plain); contents omitted.",
);
expect(frozen.text).toContain("13 parts omitted.");
expect(frozen.text).not.toContain("earlier parts omitted");
expect(frozen.estimatedTokens).toBeLessThanOrEqual(6000);
expect(frozen).toEqual(buildAssistantContinuationBrief(record));
},
);
17 changes: 9 additions & 8 deletions packages/harness/src/core/assistant-continuation-brief.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
import type {
AssistantRecord,
AssistantRecordBinding,
AssistantRecordPart,
} from "../shared/assistant-record.js";
import { validateAssistantRecord } from "./assistant-record.js";
import {
Expand All @@ -25,6 +26,9 @@ const clamp = (value: string, limit: number) =>
value.length <= limit
? value
: `${value.slice(0, limit)}… [excerpt truncated]`;
const usefulPart = (part: AssistantRecordPart) =>
part.type !== "omitted" &&
(part.type !== "text" || part.text.trim().length > 0);

/** Freeze a bounded public reconstruction. It carries context, never an instruction to replay work. */
export function buildAssistantContinuationBrief(
Expand All @@ -45,7 +49,7 @@ export function buildAssistantContinuationBrief(
const messages = turn.messages.map((message) => {
const selected =
message.role === "assistant"
? message.parts.slice(-12)
? message.parts.filter(usefulPart).slice(-12)
: message.parts.slice(0, 12);
const parts = selected.map((part) => {
switch (part.type) {
Expand All @@ -59,18 +63,15 @@ export function buildAssistantContinuationBrief(
return `Omitted ${part.nativeType} part.`;
}
});
if (message.parts.length > 12) {
const omitted = `${message.parts.length - 12} ${message.role === "assistant" ? "earlier" : "further"} parts omitted.`;
if (message.parts.length > selected.length) {
const count = message.parts.length - selected.length;
const omitted = `${count} ${message.role === "assistant" ? "" : "further "}part${count === 1 ? "" : "s"} omitted.`;
if (message.role === "assistant") parts.unshift(omitted);
else parts.push(omitted);
}
return {
text: `${message.role === "user" ? "User" : "Assistant"} ${message.id}:\n${parts.join("\n")}`,
useful: message.parts.some(
(part) =>
part.type !== "omitted" &&
(part.type !== "text" || part.text.trim().length > 0),
),
useful: selected.some(usefulPart),
};
});
return {
Expand Down
Loading