feat: project durable agent conversations

This commit is contained in:
-Puter
2026-07-29 08:19:17 +05:30
parent 18eb150d7d
commit 3ffa1cfc7c
22 changed files with 800 additions and 309 deletions

View File

@@ -55,7 +55,12 @@ export const useOrganizationChatAgent = (
const projected = projectConversation(rows ?? []);
let status: AgentStatus = projected.pending ? "submitted" : "idle";
let status: AgentStatus = "idle";
if (projected.streaming) {
status = "streaming";
} else if (projected.pending) {
status = "submitted";
}
if (!organizationId || rows === undefined) {
status = organization.error ? "error" : "connecting";
} else if (projected.failedError || sendError) {

View File

@@ -20,7 +20,7 @@ describe("projectConversation", () => {
messageId: "user-1",
rawText: "Build it",
role: "user",
status: "processing",
status: "dispatching",
}),
row({ messageId: "assistant-1", role: "assistant", status: "queued" }),
]);
@@ -28,6 +28,21 @@ describe("projectConversation", () => {
expect(state.messages).toHaveLength(1);
});
test("projects partial assistant text as streaming", () => {
const state = projectConversation([
row({
messageId: "assistant-streaming",
rawText: "Working",
role: "assistant",
status: "running",
}),
]);
expect(state.streaming).toBe(true);
expect(state.messages[0]?.parts).toEqual([
{ state: "streaming", text: "Working", type: "text" },
]);
});
test("projects completed Convex rows into renderable messages", () => {
const state = projectConversation([
row({

View File

@@ -11,40 +11,67 @@ export interface ConversationRow {
readonly messageId: string;
readonly rawText: string;
readonly role: "assistant" | "user";
readonly status: "completed" | "failed" | "processing" | "queued";
readonly status:
| "aborted"
| "completed"
| "dispatching"
| "failed"
| "queued"
| "running";
}
export const projectConversation = (rows: readonly ConversationRow[]) => ({
failedError: rows.findLast(
(row) => row.role === "assistant" && row.status === "failed"
)?.error,
messages: rows
.filter((row) => row.role === "user" || row.status === "completed")
.map<ConversationMessage>((row) => ({
id: row.messageId,
parts: [
...row.attachments.map((attachment) => ({
filename: attachment.filename ?? undefined,
id: attachment.id,
mediaType: attachment.mediaType,
type: "file" as const,
url: attachment.url ?? undefined,
})),
...(row.rawText
? [
{
state: "done" as const,
text: row.rawText,
type: "text" as const,
},
]
: []),
],
role: row.role,
})),
pending: rows.some(
export const projectConversation = (rows: readonly ConversationRow[]) => {
const streaming = rows.some(
(row) =>
row.role === "assistant" &&
(row.status === "queued" || row.status === "processing")
),
});
row.status === "running" &&
row.rawText.length > 0
);
return {
failedError: rows.findLast(
(row) => row.role === "assistant" && row.status === "failed"
)?.error,
messages: rows
.filter(
(row) =>
row.role === "user" ||
row.status === "completed" ||
(row.role === "assistant" &&
row.status === "running" &&
row.rawText.length > 0)
)
.map<ConversationMessage>((row) => ({
id: row.messageId,
parts: [
...row.attachments.map((attachment) => ({
filename: attachment.filename ?? undefined,
id: attachment.id,
mediaType: attachment.mediaType,
type: "file" as const,
url: attachment.url ?? undefined,
})),
...(row.rawText
? [
{
state:
row.status === "running"
? ("streaming" as const)
: ("done" as const),
text: row.rawText,
type: "text" as const,
},
]
: []),
],
role: row.role,
})),
pending: rows.some(
(row) =>
row.role === "assistant" &&
(row.status === "queued" ||
row.status === "dispatching" ||
row.status === "running")
),
streaming,
};
};

View File

@@ -6,7 +6,5 @@ export default [
route("login", "./routes/auth/login/page.tsx"),
route("signup", "./routes/auth/signup/page.tsx"),
]),
layout("./routes/app/layout.tsx", [
index("./routes/app/workspace/page.tsx"),
]),
layout("./routes/app/layout.tsx", [index("./routes/app/workspace/page.tsx")]),
] satisfies RouteConfig;