238 lines
7.9 KiB
TypeScript
238 lines
7.9 KiB
TypeScript
/**
|
|
* Shared domain types for Lam Agent.
|
|
*
|
|
* These are deliberately split into two families:
|
|
*
|
|
* - `ApiMessage` and friends mirror exactly what Ollama's `/api/chat` accepts.
|
|
* - `Session`, `ChatTurn` and friends are Lam's own storage model.
|
|
*
|
|
* Keeping them separate matters because the storage model is derived from the
|
|
* API model, not identical to it: the tool-calling messages that Ollama needs
|
|
* mid-turn are not persisted, so a reloaded session replays cleanly as plain
|
|
* user/assistant pairs.
|
|
*/
|
|
|
|
export type ChatRole = "system" | "user" | "assistant" | "tool";
|
|
|
|
export interface ToolCall {
|
|
function: {
|
|
name: string;
|
|
arguments: Record<string, unknown>;
|
|
};
|
|
}
|
|
|
|
export interface ToolDefinition {
|
|
type: "function";
|
|
function: {
|
|
name: string;
|
|
description: string;
|
|
parameters: Record<string, unknown>;
|
|
};
|
|
}
|
|
|
|
/** A message in the shape Ollama's `/api/chat` endpoint expects. */
|
|
export interface ApiMessage {
|
|
role: ChatRole;
|
|
content: string;
|
|
tool_calls?: ToolCall[];
|
|
/** Only set on `role: "tool"` messages, naming the tool that produced this content. */
|
|
tool_name?: string;
|
|
/**
|
|
* Base64 image data, for vision models.
|
|
*
|
|
* Ollama only reads these on a `user` message (`gpt-oss` style tool roles have
|
|
* no images). They are the one genuinely large thing in a request, so they are
|
|
* attached to the question they belong to and never replayed later, the same
|
|
* way tool output is not replayed.
|
|
*/
|
|
images?: string[];
|
|
}
|
|
|
|
/**
|
|
* A file the user attached to a question.
|
|
*
|
|
* Only the path is stored. A screenshot runs to megabytes, and
|
|
* `MAX_STORED_CHARS_PER_TURN` exists precisely so one turn cannot dominate
|
|
* LocalStorage, so the bytes stay on disk and are read when the turn is sent.
|
|
*/
|
|
export interface Attachment {
|
|
/** Absolute path to the image on disk. */
|
|
path: string;
|
|
/** Sent to Ollama next to the base64 payload. */
|
|
mime: string;
|
|
/**
|
|
* Pixel dimensions, read from the file's header.
|
|
*
|
|
* Kept because the transcript has to lay the preview out long after the file
|
|
* was read, and Vicinae sizes an image's container to the request rather than
|
|
* to the image, which leaves dead space unless the request has the right
|
|
* aspect ratio. Absent for a format whose header we do not parse.
|
|
*/
|
|
width?: number;
|
|
height?: number;
|
|
}
|
|
|
|
/**
|
|
* An attachment that has been read and is ready to send, held in the view while
|
|
* the user types their question.
|
|
*
|
|
* The base64 payload lives here and nowhere else. It is needed for exactly one
|
|
* request, so it is deliberately not part of `Attachment` and never reaches
|
|
* storage.
|
|
*/
|
|
export interface PendingAttachment extends Attachment {
|
|
/** Base64 of the image, with no `data:` URL prefix. Ollama wants it bare. */
|
|
base64: string;
|
|
}
|
|
/** What a tool actually returned during a turn, kept for display and debugging. */
|
|
export interface ToolCallRecord {
|
|
name: string;
|
|
arguments: Record<string, unknown>;
|
|
result: string;
|
|
isError: boolean;
|
|
/**
|
|
* Set when the call was refused because the turn had already made it twice.
|
|
*
|
|
* Stored rather than inferred from the message text, so the transcript can say
|
|
* what happened in its own words, and so a repeat that ended a turn stays
|
|
* visible even when the user has hidden tool calls from the transcript.
|
|
*/
|
|
repeated?: boolean;
|
|
}
|
|
|
|
/** One question and its answer. The unit the transcript is rendered from. */
|
|
export interface ChatTurn {
|
|
id: string;
|
|
prompt: string;
|
|
response: string;
|
|
thinking: string;
|
|
model: string;
|
|
createdAt: number;
|
|
/** Set when the turn failed, so a stored session can show what went wrong. */
|
|
error?: string;
|
|
toolCalls: ToolCallRecord[];
|
|
/** Images the user attached, kept for the transcript. The bytes are not stored. */
|
|
attachments: Attachment[];
|
|
}
|
|
|
|
export interface Session {
|
|
id: string;
|
|
title: string;
|
|
model: string;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
turns: ChatTurn[];
|
|
}
|
|
|
|
/** The lightweight record kept in the session index, so listing sessions never loads full transcripts. */
|
|
export interface SessionSummary {
|
|
id: string;
|
|
title: string;
|
|
model: string;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
turnCount: number;
|
|
preview: string;
|
|
/**
|
|
* Every image path any turn in this conversation refers to.
|
|
*
|
|
* Kept here rather than derived from the turns for one reason: the orphan
|
|
* sweep needs to know which snips are still spoken for, and loading every
|
|
* transcript to find out would mean a hundred JSON parses on every turn.
|
|
*
|
|
* Absent on entries written before this field existed, which is deliberately
|
|
* not the same as an empty array: an absent value means "unknown", and the
|
|
* sweep resolves it by reading that one transcript rather than by assuming the
|
|
* conversation has no screenshots and deleting them.
|
|
*/
|
|
attachmentPaths?: string[];
|
|
}
|
|
|
|
/**
|
|
* Whether the model in use can read images, as far as the capability check knows.
|
|
*
|
|
* `"unknown"` is deliberately distinct from `"no"`: a failed request is not
|
|
* evidence that a model cannot see, and reporting it as one would be a false
|
|
* claim about the user's setup. Lives here rather than in `vision.ts` so that
|
|
* modules which must stay free of Vicinae imports can name it.
|
|
*/
|
|
export type VisionSupport = "yes" | "no" | "unknown";
|
|
|
|
/**
|
|
* The tool running right now, and how stale it is getting.
|
|
*
|
|
* Lives here rather than beside the chat state machine so that `thread.ts` can
|
|
* carry it into the transcript, which is the only place it is now rendered.
|
|
*/
|
|
export interface ToolActivity {
|
|
/** Human-readable label, e.g. "Searching the web". */
|
|
label: string;
|
|
/**
|
|
* How many times this turn has now started this *identical* call.
|
|
*
|
|
* Counted by the agent over name and arguments together, so two different
|
|
* searches are two tools doing their job rather than a repeat.
|
|
*/
|
|
count: number;
|
|
}
|
|
|
|
/** A model as reported by `GET /api/tags`. */
|
|
export interface ModelInfo {
|
|
name: string;
|
|
parameterSize?: string;
|
|
/**
|
|
* How much disk the model takes, in bytes.
|
|
*
|
|
* Meaningless for a cloud model proxied through a local daemon: that entry is
|
|
* a few hundred bytes of manifest, so the caller shows nothing rather than
|
|
* "326 B", which is what `ollama list` does too.
|
|
*/
|
|
size?: number;
|
|
/**
|
|
* What the model says it can do, e.g. `["tools", "vision"]`.
|
|
*
|
|
* Only newer servers send this. Absent means unknown, never "no": reporting a
|
|
* missing field as a lack of vision would be a false claim about the user's
|
|
* setup.
|
|
*/
|
|
capabilities?: string[];
|
|
/**
|
|
* Set when this entry is a proxy rather than weights on disk, naming the server
|
|
* that actually runs it (`https://ollama.com`). Deleting one of these drops a
|
|
* reference; it does not free the gigabytes the size column implies.
|
|
*/
|
|
remoteHost?: string;
|
|
}
|
|
|
|
/**
|
|
* One rendered line of the conversation.
|
|
*
|
|
* A turn is stored as a question/answer pair, but the transcript is displayed as
|
|
* a flat sequence of messages so each side can carry its own icon and weight.
|
|
*/
|
|
export interface ThreadMessage {
|
|
id: string;
|
|
role: "user" | "assistant";
|
|
/** Full text, used for the detail panel and for copying. */
|
|
text: string;
|
|
createdAt: number;
|
|
isStreaming: boolean;
|
|
thinking: string;
|
|
toolCalls: ToolCallRecord[];
|
|
error?: string;
|
|
/** Activity line shown while this message is streaming, e.g. "Searching the web". */
|
|
status?: string;
|
|
/**
|
|
* The tool this message is waiting on, when there is one.
|
|
*
|
|
* Separate from {@link ThreadMessage.status} because the two are rendered from
|
|
* different triggers: the status is a stand-in used only while there is no text
|
|
* to read, while a running tool is worth saying whether or not tokens have
|
|
* started arriving. Carrying both is what lets one line be printed for one
|
|
* fact, rather than the same sentence appearing twice at the top of a turn.
|
|
*/
|
|
activity?: ToolActivity;
|
|
/** Images attached to this question. Only ever non-empty on a user message. */
|
|
attachments: Attachment[];
|
|
}
|