feat(giellie): shared types and SSE codec
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0eb9653359
commit
d611efe77a
3 changed files with 109 additions and 0 deletions
29
src/lib/giellie-sse.test.ts
Normal file
29
src/lib/giellie-sse.test.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { encodeEvent, createEventParser, type GielieEvent } from "./giellie-sse";
|
||||
|
||||
describe("giellie SSE codec", () => {
|
||||
it("round-trips events", () => {
|
||||
const events: GielieEvent[] = [
|
||||
{ type: "text", delta: "hel" },
|
||||
{ type: "tool_start", id: "t1", name: "orders.get", input: { id: "x" } },
|
||||
{ type: "done" },
|
||||
];
|
||||
const parser = createEventParser();
|
||||
const out = parser.push(events.map(encodeEvent).join(""));
|
||||
expect(out).toEqual(events);
|
||||
});
|
||||
|
||||
it("buffers a frame split across chunks", () => {
|
||||
const frame = encodeEvent({ type: "text", delta: "split me" });
|
||||
const parser = createEventParser();
|
||||
expect(parser.push(frame.slice(0, 10))).toEqual([]);
|
||||
expect(parser.push(frame.slice(10))).toEqual([{ type: "text", delta: "split me" }]);
|
||||
});
|
||||
|
||||
it("ignores malformed frames instead of throwing", () => {
|
||||
const parser = createEventParser();
|
||||
expect(parser.push("data: {not json\n\n" + encodeEvent({ type: "done" }))).toEqual([
|
||||
{ type: "done" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
39
src/lib/giellie-sse.ts
Normal file
39
src/lib/giellie-sse.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import type { PendingAction } from "./giellie-types";
|
||||
|
||||
/** Events streamed from the server to the chat UI. */
|
||||
export type GielieEvent =
|
||||
| { type: "text"; delta: string }
|
||||
| { type: "tool_start"; id: string; name: string; input: Record<string, unknown> }
|
||||
| { type: "tool_result"; id: string; name: string; isError: boolean; preview: string }
|
||||
| { type: "approval_required"; pending: PendingAction }
|
||||
| { type: "title"; title: string }
|
||||
| { type: "error"; message: string }
|
||||
| { type: "done" };
|
||||
|
||||
export function encodeEvent(event: GielieEvent): string {
|
||||
return `data: ${JSON.stringify(event)}\n\n`;
|
||||
}
|
||||
|
||||
/** Incremental SSE parser: feed decoded text chunks, get complete events. */
|
||||
export function createEventParser() {
|
||||
let buffer = "";
|
||||
return {
|
||||
push(chunk: string): GielieEvent[] {
|
||||
buffer += chunk;
|
||||
const events: GielieEvent[] = [];
|
||||
let idx: number;
|
||||
while ((idx = buffer.indexOf("\n\n")) !== -1) {
|
||||
const frame = buffer.slice(0, idx);
|
||||
buffer = buffer.slice(idx + 2);
|
||||
const line = frame.split("\n").find((l) => l.startsWith("data: "));
|
||||
if (!line) continue;
|
||||
try {
|
||||
events.push(JSON.parse(line.slice(6)) as GielieEvent);
|
||||
} catch {
|
||||
// Malformed frame — drop it; the stream keeps going.
|
||||
}
|
||||
}
|
||||
return events;
|
||||
},
|
||||
};
|
||||
}
|
||||
41
src/lib/giellie-types.ts
Normal file
41
src/lib/giellie-types.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* Shared Giellie types (server + client). Message blocks follow the
|
||||
* Anthropic content-block shape but are kept loose (`Block`) because
|
||||
* MiniMax also returns `thinking` blocks that must round-trip untouched.
|
||||
*/
|
||||
export type Block = { type: string; [key: string]: unknown };
|
||||
|
||||
export interface StoredMessage {
|
||||
role: "user" | "assistant";
|
||||
content: Block[];
|
||||
ts: string;
|
||||
}
|
||||
|
||||
export interface PendingToolUse {
|
||||
id: string;
|
||||
/** Dotted MCP name, e.g. `orders.update`. */
|
||||
name: string;
|
||||
input: Record<string, unknown>;
|
||||
/** Email/PO sends — the trust toggle never auto-approves these. */
|
||||
confirmAlways: boolean;
|
||||
}
|
||||
|
||||
export interface PendingAction {
|
||||
tool_uses: PendingToolUse[];
|
||||
/** tool_result blocks already produced this turn (read tools). */
|
||||
completed: Block[];
|
||||
/** Every tool_use id of the assistant turn, in original order. */
|
||||
order: string[];
|
||||
}
|
||||
|
||||
export interface GielieChatSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface GielieChat extends GielieChatSummary {
|
||||
messages: StoredMessage[];
|
||||
pending_action: PendingAction | null;
|
||||
attachments: string[];
|
||||
}
|
||||
Loading…
Reference in a new issue