feat: project agent messages for chat surface
This commit is contained in:
@@ -0,0 +1,80 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { AgentMessage } from "../../demo/agent/events.js";
|
||||||
|
import { projectAgentMessage } from "./agentChatProjection.js";
|
||||||
|
|
||||||
|
describe("agentChatProjection", () => {
|
||||||
|
it("projects text parts", () => {
|
||||||
|
const message: AgentMessage = {
|
||||||
|
id: "m1",
|
||||||
|
role: "assistant",
|
||||||
|
parts: [{ type: "text", text: "Live target is ready." }],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(projectAgentMessage(message)).toMatchObject({
|
||||||
|
id: "m1",
|
||||||
|
from: "assistant",
|
||||||
|
parts: [{ kind: "text", text: "Live target is ready." }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects workflow start as an expanded workflow operation", () => {
|
||||||
|
const message: AgentMessage = {
|
||||||
|
id: "m1",
|
||||||
|
role: "assistant",
|
||||||
|
parts: [{ type: "tool-call", call: { id: "call-1", name: "startPreparedReportRun", input: { mode: "live" } } }],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(projectAgentMessage(message).parts[0]).toMatchObject({
|
||||||
|
kind: "tool",
|
||||||
|
label: "Workflow operation",
|
||||||
|
name: "startPreparedReportRun",
|
||||||
|
state: "pending",
|
||||||
|
defaultOpen: true,
|
||||||
|
input: { mode: "live" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects ordinary tool results as collapsed tool records", () => {
|
||||||
|
const message: AgentMessage = {
|
||||||
|
id: "m1",
|
||||||
|
role: "assistant",
|
||||||
|
parts: [{ type: "tool-result", result: { callId: "call-1", name: "readRunTrace", status: "success", output: { frames: 4 } } }],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(projectAgentMessage(message).parts[0]).toMatchObject({
|
||||||
|
kind: "tool",
|
||||||
|
label: "Tool result",
|
||||||
|
name: "readRunTrace",
|
||||||
|
state: "success",
|
||||||
|
defaultOpen: false,
|
||||||
|
output: { frames: 4 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects approval requests with their contract", () => {
|
||||||
|
const message: AgentMessage = {
|
||||||
|
id: "m1",
|
||||||
|
role: "assistant",
|
||||||
|
parts: [{
|
||||||
|
type: "approval-request",
|
||||||
|
callId: "call-1",
|
||||||
|
name: "resumeIssueReview",
|
||||||
|
prompt: "Submit resume request?",
|
||||||
|
contract: {
|
||||||
|
kind: "issue_review",
|
||||||
|
outcomes: ["submitted", "cancelled"],
|
||||||
|
resumeSchema: { type: "object" },
|
||||||
|
resumePayloadPreview: { selected_issue_ids: ["risk-1"] },
|
||||||
|
runId: "run_1",
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(projectAgentMessage(message).parts[0]).toMatchObject({
|
||||||
|
kind: "approval",
|
||||||
|
name: "resumeIssueReview",
|
||||||
|
prompt: "Submit resume request?",
|
||||||
|
contract: { kind: "issue_review", runId: "run_1" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import type { AgentApprovalContract, AgentMessage, AgentMessagePart } from "../../demo/agent/events.js";
|
||||||
|
import type { MessageFrom, ToolState } from "./ChatPrimitives.js";
|
||||||
|
|
||||||
|
export type ProjectedChatPart =
|
||||||
|
| { readonly kind: "text"; readonly text: string }
|
||||||
|
| {
|
||||||
|
readonly kind: "tool";
|
||||||
|
readonly label: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly state: ToolState;
|
||||||
|
readonly defaultOpen: boolean;
|
||||||
|
readonly input?: unknown;
|
||||||
|
readonly output?: unknown;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly kind: "approval";
|
||||||
|
readonly name: string;
|
||||||
|
readonly prompt: string;
|
||||||
|
readonly contract?: AgentApprovalContract | undefined;
|
||||||
|
}
|
||||||
|
| { readonly kind: "error"; readonly message: string };
|
||||||
|
|
||||||
|
export type ProjectedChatMessage = {
|
||||||
|
readonly id: string;
|
||||||
|
readonly from: MessageFrom;
|
||||||
|
readonly parts: ReadonlyArray<ProjectedChatPart>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toolStateFromResult = (status: AgentMessagePart & { readonly type: "tool-result" }): ToolState =>
|
||||||
|
status.result.status === "failure" ? "error" : "success";
|
||||||
|
|
||||||
|
const projectPart = (part: AgentMessagePart): ProjectedChatPart | null => {
|
||||||
|
switch (part.type) {
|
||||||
|
case "text":
|
||||||
|
return { kind: "text", text: part.text };
|
||||||
|
case "tool-call":
|
||||||
|
return {
|
||||||
|
kind: "tool",
|
||||||
|
label: part.call.name === "startPreparedReportRun" ? "Workflow operation" : "Tool call",
|
||||||
|
name: part.call.name,
|
||||||
|
state: "pending",
|
||||||
|
defaultOpen: part.call.name === "startPreparedReportRun",
|
||||||
|
input: part.call.input,
|
||||||
|
};
|
||||||
|
case "tool-result":
|
||||||
|
return {
|
||||||
|
kind: "tool",
|
||||||
|
label: "Tool result",
|
||||||
|
name: part.result.name,
|
||||||
|
state: toolStateFromResult(part),
|
||||||
|
defaultOpen: false,
|
||||||
|
output: part.result.output,
|
||||||
|
};
|
||||||
|
case "presentation-action":
|
||||||
|
return {
|
||||||
|
kind: "tool",
|
||||||
|
label: "Presentation action",
|
||||||
|
name: part.action.type,
|
||||||
|
state: "success",
|
||||||
|
defaultOpen: false,
|
||||||
|
output: part.action,
|
||||||
|
};
|
||||||
|
case "approval-request":
|
||||||
|
return {
|
||||||
|
kind: "approval",
|
||||||
|
name: part.name,
|
||||||
|
prompt: part.prompt,
|
||||||
|
contract: part.contract,
|
||||||
|
};
|
||||||
|
case "error":
|
||||||
|
return { kind: "error", message: part.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const projectAgentMessage = (message: AgentMessage): ProjectedChatMessage => ({
|
||||||
|
id: message.id,
|
||||||
|
from: message.role === "user" ? "user" : "assistant",
|
||||||
|
parts: message.parts.flatMap((part) => {
|
||||||
|
const projected = projectPart(part);
|
||||||
|
return projected ? [projected] : [];
|
||||||
|
}),
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user