Skip to content
Merged
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
140 changes: 140 additions & 0 deletions features/pitch/application/agentProgress.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
applyAgentProgress,
ensureAgentProgressPlaceholder,
reconcileAgentProgress,
type PitchAgentMessage
} from "./agentProgress";

const userMessage: PitchAgentMessage = {
id: "user-1",
role: "user",
content: "Improve the deck"
};

test("replaces a run placeholder with cumulative reasoning summary progress", () => {
const pending = ensureAgentProgressPlaceholder([userMessage], "run-1");
const partial = applyAgentProgress(pending, "run-1", {
type: "reasoning.summary",
text: "Inspecting the deck",
done: false
});
const complete = applyAgentProgress(partial, "run-1", {
type: "reasoning.summary",
text: "Inspecting the deck before choosing a layout.",
done: true
});

assert.deepEqual(complete, [
userMessage,
{
id: "reasoning:run-1",
role: "reasoning",
content: "Inspecting the deck before choosing a layout.",
done: true
}
]);
});

test("keeps each commentary message distinct and updates it by provider id", () => {
const first = applyAgentProgress([userMessage], "run-1", {
type: "assistant.commentary",
messageId: "message-1",
text: "I found the current layout.",
done: true
});
const secondPartial = applyAgentProgress(first, "run-1", {
type: "assistant.commentary",
messageId: "message-2",
text: "Next I’m checking",
done: false
});
const secondComplete = applyAgentProgress(secondPartial, "run-1", {
type: "assistant.commentary",
messageId: "message-2",
text: "Next I’m checking the available slide presets.",
done: true
});

assert.deepEqual(secondComplete.slice(1), [
{
id: "commentary:run-1:message-1",
role: "commentary",
content: "I found the current layout.",
done: true
},
{
id: "commentary:run-1:message-2",
role: "commentary",
content: "Next I’m checking the available slide presets.",
done: true
}
]);
});

test("scopes transient progress ids to the active run", () => {
const firstRun = applyAgentProgress([userMessage], "run-1", {
type: "reasoning.summary",
text: "Inspecting the first request.",
done: true
});
const secondRun = applyAgentProgress(firstRun, "run-2", {
type: "reasoning.summary",
text: "Inspecting the follow-up request.",
done: false
});

assert.deepEqual(
secondRun.filter(({ role }) => role === "reasoning").map(({ id }) => id),
["reasoning:run-1", "reasoning:run-2"]
);
});

test("keeps current-run progress immediately before the durable terminal answer", () => {
const progress = applyAgentProgress([userMessage], "run-1", {
type: "assistant.commentary",
messageId: "message-1",
text: "I checked the available slide layouts.",
done: true
});
const durableMessages: PitchAgentMessage[] = [
userMessage,
{
id: "assistant-1",
role: "assistant",
content: "Added the requested slide."
}
];

assert.deepEqual(
reconcileAgentProgress(progress, "run-1", durableMessages),
[
userMessage,
{
id: "commentary:run-1:message-1",
role: "commentary",
content: "I checked the available slide layouts.",
done: true
},
durableMessages[1]
]
);
});

test("does not retain an empty progress placeholder after completion", () => {
const pending = ensureAgentProgressPlaceholder([userMessage], "run-1");
const durableMessages: PitchAgentMessage[] = [
userMessage,
{
id: "assistant-1",
role: "assistant",
content: "No deck change was needed."
}
];

assert.deepEqual(
reconcileAgentProgress(pending, "run-1", durableMessages),
durableMessages
);
});
120 changes: 120 additions & 0 deletions features/pitch/application/agentProgress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import type { AgentActivity } from "@/features/pitch/domain/agentRun";

export type PitchAgentMessage =
| {
id: string;
role: "user" | "assistant";
content: string;
}
| {
id: string;
role: "reasoning" | "commentary";
content: string;
done: boolean;
};

/**
* Projects Heddle's two user-visible progress channels into transient UI rows.
*
* `reasoning.summary` is provider-generated reasoning summary text, while
* `assistant.commentary` is assistant-authored work narration. Neither is a
* terminal answer, and both remain separate so the panel owns their labels and
* presentation.
*/
export function applyAgentProgress(
messages: PitchAgentMessage[],
runId: string,
activity: AgentActivity
): PitchAgentMessage[] {
if (activity.type === "reasoning.summary" && activity.text) {
return upsertProgress(
removeRunPlaceholder(messages, runId),
{
id: `reasoning:${runId}`,
role: "reasoning",
content: activity.text,
done: activity.done === true
}
);
}

if (
activity.type === "assistant.commentary"
&& activity.messageId
&& activity.text
) {
return upsertProgress(
removeRunPlaceholder(messages, runId),
{
id: `commentary:${runId}:${activity.messageId}`,
role: "commentary",
content: activity.text,
done: activity.done === true
}
);
}

return messages;
}

export function ensureAgentProgressPlaceholder(
messages: PitchAgentMessage[],
runId: string
): PitchAgentMessage[] {
const id = placeholderId(runId);
return messages.some((message) => message.id === id)
? messages
: [...messages, {
id,
role: "commentary",
content: "",
done: false
}];
}

export function reconcileAgentProgress(
messages: PitchAgentMessage[],
runId: string,
durableMessages: PitchAgentMessage[]
): PitchAgentMessage[] {
const progress = messages.filter((message) => (
message.id === `reasoning:${runId}`
|| message.id.startsWith(`commentary:${runId}:`)
));
const terminalIndex = durableMessages.findLastIndex(({ role }) => (
role === "assistant"
));
if (progress.length === 0 || terminalIndex < 0) {
return durableMessages;
}

return [
...durableMessages.slice(0, terminalIndex),
...progress,
...durableMessages.slice(terminalIndex)
];
}

function removeRunPlaceholder(
messages: PitchAgentMessage[],
runId: string
): PitchAgentMessage[] {
const id = placeholderId(runId);
return messages.filter((message) => message.id !== id);
}

function upsertProgress(
messages: PitchAgentMessage[],
progress: Extract<PitchAgentMessage, { role: "reasoning" | "commentary" }>
): PitchAgentMessage[] {
const index = messages.findIndex(({ id }) => id === progress.id);
return index < 0
? [...messages, progress]
: messages.map((message, messageIndex) => (
messageIndex === index ? progress : message
));
}

function placeholderId(runId: string): string {
return `progress:${runId}`;
}
2 changes: 2 additions & 0 deletions features/pitch/domain/agentRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import type { ConversationRunProtocolEvent } from "@roackb2/heddle-remote";

export type AgentActivity = {
type: string;
messageId?: string;
text?: string;
done?: boolean;
tool?: string;
result?: { ok?: boolean };
};
Expand Down
44 changes: 44 additions & 0 deletions features/pitch/infrastructure/slidexAgentProtocol.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,50 @@ test("validates the product payload carried by Heddle's run protocol", () => {
);
});

test("preserves reasoning-summary progress as a distinct activity", () => {
const event = SlideXAgentRunProtocol.parseEvent({
kind: "activity",
runId: "run-1",
sequence: 1,
timestamp,
activity: {
type: "reasoning.summary",
text: "Inspecting the deck structure",
done: false
}
});

assert.equal(event.kind, "activity");
assert.deepEqual(event.activity, {
type: "reasoning.summary",
text: "Inspecting the deck structure",
done: false
});
});

test("preserves assistant commentary as streamed work progress", () => {
const event = SlideXAgentRunProtocol.parseEvent({
kind: "activity",
runId: "run-1",
sequence: 2,
timestamp,
activity: {
type: "assistant.commentary",
messageId: "commentary-1",
text: "I checked the current deck. Next I’m comparing the available layouts.",
done: false
}
});

assert.equal(event.kind, "activity");
assert.deepEqual(event.activity, {
type: "assistant.commentary",
messageId: "commentary-1",
text: "I checked the current deck. Next I’m comparing the available layouts.",
done: false
});
});

test("validates hydrated conversation history and active-run discovery", () => {
const state = AgentSessionStateSchema.parse({
session: createSession(),
Expand Down
2 changes: 2 additions & 0 deletions features/pitch/infrastructure/slidexAgentProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ export const AttachAgentSessionResultSchema: z.ZodType<AttachAgentSessionResult>

const AgentActivitySchema: z.ZodType<AgentActivity> = z.object({
type: z.string().min(1),
messageId: z.string().min(1).optional(),
text: z.string().optional(),
done: z.boolean().optional(),
tool: z.string().optional(),
result: z.object({
ok: z.boolean().optional()
Expand Down
Loading
Loading