Add timestamp to messages

This commit is contained in:
Mario Zechner 2025-10-26 00:43:43 +02:00
parent ef09efaac9
commit 55dc0b6e08
24 changed files with 388 additions and 220 deletions

View file

@ -159,6 +159,7 @@ export class Agent {
role: "user",
content,
attachments: attachments?.length ? attachments : undefined,
timestamp: Date.now(),
};
this.abortController = new AbortController();
@ -260,6 +261,7 @@ export class Agent {
},
stopReason: this.abortController?.signal.aborted ? "aborted" : "error",
errorMessage: err?.message || String(err),
timestamp: Date.now(),
};
this.appendMessage(msg as AppMessage);
this.patch({ error: err?.message || String(err) });

View file

@ -46,6 +46,7 @@ function streamSimpleProxy(
cacheWrite: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
timestamp: Date.now(),
};
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;

View file

@ -86,7 +86,7 @@ describe("Agent", () => {
expect(agent.state.tools).toBe(tools);
// Test replaceMessages
const messages = [{ role: "user" as const, content: "Hello" }];
const messages = [{ role: "user" as const, content: "Hello", timestamp: Date.now() }];
agent.replaceMessages(messages);
expect(agent.state.messages).toEqual(messages);
expect(agent.state.messages).not.toBe(messages); // Should be a copy
@ -107,7 +107,7 @@ describe("Agent", () => {
transport: new ProviderTransport(),
});
const message = { role: "user" as const, content: "Queued message" };
const message = { role: "user" as const, content: "Queued message", timestamp: Date.now() };
await agent.queueMessage(message);
// The message is queued but not yet in state.messages

View file

@ -223,6 +223,7 @@ async function executeToolCalls<T>(
output: typeof resultOrError === "string" ? resultOrError : resultOrError.output,
details: typeof resultOrError === "string" ? ({} as T) : resultOrError.details,
isError,
timestamp: Date.now(),
};
results.push(toolResultMessage);

View file

@ -5,6 +5,23 @@ import type { Model } from "./types.js";
export const MODELS = {
anthropic: {
"claude-opus-4-0": {
id: "claude-opus-4-0",
name: "Claude Opus 4 (latest)",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 15,
output: 75,
cacheRead: 1.5,
cacheWrite: 18.75,
},
contextWindow: 200000,
maxTokens: 32000,
} satisfies Model<"anthropic-messages">,
"claude-3-5-sonnet-20241022": {
id: "claude-3-5-sonnet-20241022",
name: "Claude Sonnet 3.5 v2",
@ -22,6 +39,40 @@ export const MODELS = {
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"anthropic-messages">,
"claude-opus-4-1": {
id: "claude-opus-4-1",
name: "Claude Opus 4.1 (latest)",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 15,
output: 75,
cacheRead: 1.5,
cacheWrite: 18.75,
},
contextWindow: 200000,
maxTokens: 32000,
} satisfies Model<"anthropic-messages">,
"claude-haiku-4-5": {
id: "claude-haiku-4-5",
name: "Claude Haiku 4.5 (latest)",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 1,
output: 5,
cacheRead: 0.1,
cacheWrite: 1.25,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-3-5-sonnet-20240620": {
id: "claude-3-5-sonnet-20240620",
name: "Claude Sonnet 3.5",
@ -39,6 +90,23 @@ export const MODELS = {
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"anthropic-messages">,
"claude-3-5-haiku-latest": {
id: "claude-3-5-haiku-latest",
name: "Claude Haiku 3.5 (latest)",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.8,
output: 4,
cacheRead: 0.08,
cacheWrite: 1,
},
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"anthropic-messages">,
"claude-3-opus-20240229": {
id: "claude-3-opus-20240229",
name: "Claude Opus 3",
@ -56,6 +124,23 @@ export const MODELS = {
contextWindow: 200000,
maxTokens: 4096,
} satisfies Model<"anthropic-messages">,
"claude-sonnet-4-5": {
id: "claude-sonnet-4-5",
name: "Claude Sonnet 4.5 (latest)",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-sonnet-4-5-20250929": {
id: "claude-sonnet-4-5-20250929",
name: "Claude Sonnet 4.5",
@ -158,6 +243,40 @@ export const MODELS = {
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-3-7-sonnet-latest": {
id: "claude-3-7-sonnet-latest",
name: "Claude Sonnet 3.7 (latest)",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-sonnet-4-0": {
id: "claude-sonnet-4-0",
name: "Claude Sonnet 4 (latest)",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-opus-4-1-20250805": {
id: "claude-opus-4-1-20250805",
name: "Claude Opus 4.1",
@ -209,125 +328,6 @@ export const MODELS = {
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-sonnet-4-0": {
id: "claude-sonnet-4-0",
name: "Claude Sonnet 4",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-3-7-sonnet-latest": {
id: "claude-3-7-sonnet-latest",
name: "Claude Sonnet 3.7",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-sonnet-4-5": {
id: "claude-sonnet-4-5",
name: "Claude Sonnet 4.5",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-3-5-haiku-latest": {
id: "claude-3-5-haiku-latest",
name: "Claude Haiku 3.5",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.8,
output: 4,
cacheRead: 0.08,
cacheWrite: 1,
},
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"anthropic-messages">,
"claude-haiku-4-5": {
id: "claude-haiku-4-5",
name: "Claude Haiku 4.5",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 1,
output: 5,
cacheRead: 0.1,
cacheWrite: 1.25,
},
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"anthropic-messages">,
"claude-opus-4-1": {
id: "claude-opus-4-1",
name: "Claude Opus 4.1",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 15,
output: 75,
cacheRead: 1.5,
cacheWrite: 18.75,
},
contextWindow: 200000,
maxTokens: 32000,
} satisfies Model<"anthropic-messages">,
"claude-opus-4-0": {
id: "claude-opus-4-0",
name: "Claude Opus 4",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text", "image"],
cost: {
input: 15,
output: 75,
cacheRead: 1.5,
cacheWrite: 18.75,
},
contextWindow: 200000,
maxTokens: 32000,
} satisfies Model<"anthropic-messages">,
},
google: {
"gemini-2.5-flash-preview-05-20": {
@ -1804,6 +1804,23 @@ export const MODELS = {
} satisfies Model<"anthropic-messages">,
},
openrouter: {
"minimax/minimax-m2:free": {
id: "minimax/minimax-m2:free",
name: "MiniMax: MiniMax M2 (free)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 204800,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"openrouter/andromeda-alpha": {
id: "openrouter/andromeda-alpha",
name: "Andromeda Alpha",
@ -1974,6 +1991,23 @@ export const MODELS = {
contextWindow: 202752,
maxTokens: 202752,
} satisfies Model<"openai-completions">,
"z-ai/glm-4.6:exacto": {
id: "z-ai/glm-4.6:exacto",
name: "Z.AI: GLM 4.6 (exacto)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0.6,
output: 1.9,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 202752,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"deepseek/deepseek-v3.2-exp": {
id: "deepseek/deepseek-v3.2-exp",
name: "DeepSeek: DeepSeek V3.2 Exp",
@ -2014,7 +2048,7 @@ export const MODELS = {
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.3,
@ -2076,6 +2110,23 @@ export const MODELS = {
contextWindow: 163840,
maxTokens: 163840,
} satisfies Model<"openai-completions">,
"deepseek/deepseek-v3.1-terminus:exacto": {
id: "deepseek/deepseek-v3.1-terminus:exacto",
name: "DeepSeek: DeepSeek V3.1 Terminus (exacto)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0.27,
output: 1,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"alibaba/tongyi-deepresearch-30b-a3b:free": {
id: "alibaba/tongyi-deepresearch-30b-a3b:free",
name: "Tongyi DeepResearch 30B A3B (free)",
@ -2136,13 +2187,13 @@ export const MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.14,
input: 0.15,
output: 1.2,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 4096,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"qwen/qwen3-next-80b-a3b-instruct": {
id: "qwen/qwen3-next-80b-a3b-instruct",
@ -2263,6 +2314,23 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"moonshotai/kimi-k2-0905:exacto": {
id: "moonshotai/kimi-k2-0905:exacto",
name: "MoonshotAI: Kimi K2 0905 (exacto)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.6,
output: 2.5,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"deepcogito/cogito-v2-preview-llama-70b": {
id: "deepcogito/cogito-v2-preview-llama-70b",
name: "Deep Cogito: Cogito V2 Preview Llama 70B",
@ -2545,7 +2613,7 @@ export const MODELS = {
input: ["text"],
cost: {
input: 0.35,
output: 1.5,
output: 1.55,
cacheRead: 0,
cacheWrite: 0,
},
@ -2654,6 +2722,23 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"qwen/qwen3-coder:exacto": {
id: "qwen/qwen3-coder:exacto",
name: "Qwen: Qwen3 Coder 480B A35B (exacto)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0.38,
output: 1.53,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"qwen/qwen3-235b-a22b-2507": {
id: "qwen/qwen3-235b-a22b-2507",
name: "Qwen: Qwen3 235B A22B Instruct 2507",
@ -2824,9 +2909,9 @@ export const MODELS = {
contextWindow: 40000,
maxTokens: 40000,
} satisfies Model<"openai-completions">,
"mistralai/magistral-medium-2506": {
id: "mistralai/magistral-medium-2506",
name: "Mistral: Magistral Medium 2506",
"mistralai/magistral-medium-2506:thinking": {
id: "mistralai/magistral-medium-2506:thinking",
name: "Mistral: Magistral Medium 2506 (thinking)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
@ -2841,9 +2926,9 @@ export const MODELS = {
contextWindow: 40960,
maxTokens: 40000,
} satisfies Model<"openai-completions">,
"mistralai/magistral-medium-2506:thinking": {
id: "mistralai/magistral-medium-2506:thinking",
name: "Mistral: Magistral Medium 2506 (thinking)",
"mistralai/magistral-medium-2506": {
id: "mistralai/magistral-medium-2506",
name: "Mistral: Magistral Medium 2506",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
@ -3028,6 +3113,23 @@ export const MODELS = {
contextWindow: 40960,
maxTokens: 40960,
} satisfies Model<"openai-completions">,
"qwen/qwen3-8b": {
id: "qwen/qwen3-8b",
name: "Qwen: Qwen3 8B",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0.035,
output: 0.13799999999999998,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 20000,
} satisfies Model<"openai-completions">,
"qwen/qwen3-14b": {
id: "qwen/qwen3-14b",
name: "Qwen: Qwen3 14B",
@ -3212,8 +3314,8 @@ export const MODELS = {
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
contextWindow: 96000,
maxTokens: 96000,
} satisfies Model<"openai-completions">,
"mistralai/mistral-small-3.1-24b-instruct": {
id: "mistralai/mistral-small-3.1-24b-instruct",
@ -3603,7 +3705,7 @@ export const MODELS = {
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
contextWindow: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"qwen/qwen-2.5-7b-instruct": {
@ -3621,7 +3723,7 @@ export const MODELS = {
cacheWrite: 0,
},
contextWindow: 32768,
maxTokens: 16384,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"nvidia/llama-3.1-nemotron-70b-instruct": {
id: "nvidia/llama-3.1-nemotron-70b-instruct",
@ -3708,23 +3810,6 @@ export const MODELS = {
contextWindow: 32768,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"cohere/command-r-plus-08-2024": {
id: "cohere/command-r-plus-08-2024",
name: "Cohere: Command R+ (08-2024)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 2.5,
output: 10,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4000,
} satisfies Model<"openai-completions">,
"cohere/command-r-08-2024": {
id: "cohere/command-r-08-2024",
name: "Cohere: Command R (08-2024)",
@ -3742,6 +3827,23 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 4000,
} satisfies Model<"openai-completions">,
"cohere/command-r-plus-08-2024": {
id: "cohere/command-r-plus-08-2024",
name: "Cohere: Command R+ (08-2024)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 2.5,
output: 10,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4000,
} satisfies Model<"openai-completions">,
"sao10k/l3.1-euryale-70b": {
id: "sao10k/l3.1-euryale-70b",
name: "Sao10K: Llama 3.1 Euryale 70B v2.2",
@ -3793,23 +3895,6 @@ export const MODELS = {
contextWindow: 65536,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-405b-instruct": {
id: "meta-llama/llama-3.1-405b-instruct",
name: "Meta: Llama 3.1 405B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.7999999999999999,
output: 0.7999999999999999,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 32768,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-8b-instruct": {
id: "meta-llama/llama-3.1-8b-instruct",
name: "Meta: Llama 3.1 8B Instruct",
@ -3844,6 +3929,23 @@ export const MODELS = {
contextWindow: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-405b-instruct": {
id: "meta-llama/llama-3.1-405b-instruct",
name: "Meta: Llama 3.1 405B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.7999999999999999,
output: 0.7999999999999999,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 32768,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"mistralai/mistral-nemo": {
id: "mistralai/mistral-nemo",
name: "Mistral: Mistral Nemo",
@ -3963,23 +4065,6 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3-8b-instruct": {
id: "meta-llama/llama-3-8b-instruct",
name: "Meta: Llama 3 8B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.03,
output: 0.06,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 8192,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3-70b-instruct": {
id: "meta-llama/llama-3-70b-instruct",
name: "Meta: Llama 3 70B Instruct",
@ -3997,6 +4082,23 @@ export const MODELS = {
contextWindow: 8192,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3-8b-instruct": {
id: "meta-llama/llama-3-8b-instruct",
name: "Meta: Llama 3 8B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.03,
output: 0.06,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 8192,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"mistralai/mixtral-8x22b-instruct": {
id: "mistralai/mixtral-8x22b-instruct",
name: "Mistral: Mixtral 8x22B Instruct",
@ -4031,23 +4133,6 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/mistral-small": {
id: "mistralai/mistral-small",
name: "Mistral Small",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.19999999999999998,
output: 0.6,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 32768,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/mistral-tiny": {
id: "mistralai/mistral-tiny",
name: "Mistral Tiny",
@ -4065,6 +4150,23 @@ export const MODELS = {
contextWindow: 32768,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/mistral-small": {
id: "mistralai/mistral-small",
name: "Mistral Small",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.19999999999999998,
output: 0.6,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 32768,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/mixtral-8x7b-instruct": {
id: "mistralai/mixtral-8x7b-instruct",
name: "Mistral: Mixtral 8x7B Instruct",

View file

@ -54,6 +54,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now(),
};
try {

View file

@ -59,6 +59,7 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now(),
};
try {

View file

@ -53,6 +53,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now(),
};
try {

View file

@ -62,6 +62,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now(),
};
try {

View file

@ -95,6 +95,7 @@ export type StopReason = "stop" | "length" | "toolUse" | "error" | "aborted";
export interface UserMessage {
role: "user";
content: string | (TextContent | ImageContent)[];
timestamp: number; // Unix timestamp in milliseconds
}
export interface AssistantMessage {
@ -106,6 +107,7 @@ export interface AssistantMessage {
usage: Usage;
stopReason: StopReason;
errorMessage?: string;
timestamp: number; // Unix timestamp in milliseconds
}
export interface ToolResultMessage<TDetails = any> {
@ -115,6 +117,7 @@ export interface ToolResultMessage<TDetails = any> {
output: string;
details?: TDetails;
isError: boolean;
timestamp: number; // Unix timestamp in milliseconds
}
export type Message = UserMessage | AssistantMessage | ToolResultMessage;

View file

@ -9,6 +9,7 @@ async function testAbortSignal<TApi extends Api>(llm: Model<TApi>, options: Opti
{
role: "user",
content: "What is 15 + 27? Think step by step. Then list 50 first names.",
timestamp: Date.now(),
},
],
};
@ -29,7 +30,11 @@ async function testAbortSignal<TApi extends Api>(llm: Model<TApi>, options: Opti
expect(msg.content.length).toBeGreaterThan(0);
context.messages.push(msg);
context.messages.push({ role: "user", content: "Please continue, but only generate 5 names." });
context.messages.push({
role: "user",
content: "Please continue, but only generate 5 names.",
timestamp: Date.now(),
});
const followUp = await complete(llm, context, options);
expect(followUp.stopReason).toBe("stop");
@ -42,7 +47,7 @@ async function testImmediateAbort<TApi extends Api>(llm: Model<TApi>, options: O
controller.abort();
const context: Context = {
messages: [{ role: "user", content: "Hello" }],
messages: [{ role: "user", content: "Hello", timestamp: Date.now() }],
};
const response = await complete(llm, context, { ...options, signal: controller.signal });

View file

@ -27,6 +27,7 @@ async function calculateTest<TApi extends Api>(model: Model<TApi>, options: Opti
1. Calculate 3485 * 4234 and 88823 * 3482 in parallel
2. Calculate the sum of the two results using the calculator tool
3. Output ONLY the final sum as a single integer number, nothing else.`,
timestamp: Date.now(),
};
// Calculate expected results (using integers)
@ -176,6 +177,7 @@ async function abortTest<TApi extends Api>(model: Model<TApi>, options: OptionsF
const userPrompt: UserMessage = {
role: "user",
content: "Calculate 100 * 200, then 300 * 400, then 500 * 600, then sum all three results.",
timestamp: Date.now(),
};
// Create abort controller

View file

@ -8,6 +8,7 @@ async function testEmptyMessage<TApi extends Api>(llm: Model<TApi>, options: Opt
const emptyMessage: UserMessage = {
role: "user",
content: [],
timestamp: Date.now(),
};
const context: Context = {
@ -34,6 +35,7 @@ async function testEmptyStringMessage<TApi extends Api>(llm: Model<TApi>, option
{
role: "user",
content: "",
timestamp: Date.now(),
},
],
};
@ -58,6 +60,7 @@ async function testWhitespaceOnlyMessage<TApi extends Api>(llm: Model<TApi>, opt
{
role: "user",
content: " \n\t ",
timestamp: Date.now(),
},
],
};
@ -92,6 +95,7 @@ async function testEmptyAssistantMessage<TApi extends Api>(llm: Model<TApi>, opt
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now(),
};
const context: Context = {
@ -99,11 +103,13 @@ async function testEmptyAssistantMessage<TApi extends Api>(llm: Model<TApi>, opt
{
role: "user",
content: "Hello, how are you?",
timestamp: Date.now(),
},
emptyAssistant,
{
role: "user",
content: "Please respond this time.",
timestamp: Date.now(),
},
],
};

View file

@ -49,6 +49,7 @@ const providerContexts = {
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "toolUse",
timestamp: Date.now(),
} satisfies AssistantMessage,
toolResult: {
role: "toolResult" as const,
@ -56,6 +57,7 @@ const providerContexts = {
toolName: "get_weather",
output: "Weather in Tokyo: 18°C, partly cloudy",
isError: false,
timestamp: Date.now(),
} satisfies ToolResultMessage,
facts: {
calculation: 391,
@ -98,6 +100,7 @@ const providerContexts = {
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "toolUse",
timestamp: Date.now(),
} satisfies AssistantMessage,
toolResult: {
role: "toolResult" as const,
@ -105,6 +108,7 @@ const providerContexts = {
toolName: "get_weather",
output: "Weather in Berlin: 22°C, sunny",
isError: false,
timestamp: Date.now(),
} satisfies ToolResultMessage,
facts: {
calculation: 456,
@ -146,6 +150,7 @@ const providerContexts = {
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "toolUse",
timestamp: Date.now(),
} satisfies AssistantMessage,
toolResult: {
role: "toolResult" as const,
@ -153,6 +158,7 @@ const providerContexts = {
toolName: "get_weather",
output: "Weather in London: 15°C, rainy",
isError: false,
timestamp: Date.now(),
} satisfies ToolResultMessage,
facts: {
calculation: 525,
@ -196,6 +202,7 @@ const providerContexts = {
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "toolUse",
timestamp: Date.now(),
} satisfies AssistantMessage,
toolResult: {
role: "toolResult" as const,
@ -203,6 +210,7 @@ const providerContexts = {
toolName: "get_weather",
output: "Weather in Sydney: 25°C, clear",
isError: false,
timestamp: Date.now(),
} satisfies ToolResultMessage,
facts: {
calculation: 486,
@ -239,6 +247,7 @@ const providerContexts = {
},
stopReason: "error",
errorMessage: "Request was aborted",
timestamp: Date.now(),
} satisfies AssistantMessage,
toolResult: null,
facts: {
@ -263,6 +272,7 @@ async function testProviderHandoff<TApi extends Api>(
{
role: "user",
content: "Please do some calculations, tell me about capitals, and check the weather.",
timestamp: Date.now(),
},
sourceContext.message,
];
@ -281,6 +291,7 @@ async function testProviderHandoff<TApi extends Api>(
3) What was the temperature?
4) What capital city was mentioned?
Please include the specific numbers and names.`,
timestamp: Date.now(),
});
const context: Context = {

View file

@ -32,7 +32,7 @@ const calculatorTool: Tool<typeof calculatorSchema> = {
async function basicTextGeneration<TApi extends Api>(model: Model<TApi>, options?: OptionsForApi<TApi>) {
const context: Context = {
systemPrompt: "You are a helpful assistant. Be concise.",
messages: [{ role: "user", content: "Reply with exactly: 'Hello test successful'" }],
messages: [{ role: "user", content: "Reply with exactly: 'Hello test successful'", timestamp: Date.now() }],
};
const response = await complete(model, context, options);
@ -44,7 +44,7 @@ async function basicTextGeneration<TApi extends Api>(model: Model<TApi>, options
expect(response.content.map((b) => (b.type === "text" ? b.text : "")).join("")).toContain("Hello test successful");
context.messages.push(response);
context.messages.push({ role: "user", content: "Now say 'Goodbye test successful'" });
context.messages.push({ role: "user", content: "Now say 'Goodbye test successful'", timestamp: Date.now() });
const secondResponse = await complete(model, context, options);
@ -65,6 +65,7 @@ async function handleToolCall<TApi extends Api>(model: Model<TApi>, options?: Op
{
role: "user",
content: "Calculate 15 + 27 using the calculator tool.",
timestamp: Date.now(),
},
],
tools: [calculatorTool],
@ -141,7 +142,7 @@ async function handleStreaming<TApi extends Api>(model: Model<TApi>, options?: O
let textCompleted = false;
const context: Context = {
messages: [{ role: "user", content: "Count from 1 to 3" }],
messages: [{ role: "user", content: "Count from 1 to 3", timestamp: Date.now() }],
};
const s = stream(model, context, options);
@ -174,6 +175,7 @@ async function handleThinking<TApi extends Api>(model: Model<TApi>, options?: Op
{
role: "user",
content: `Think long and hard about ${(Math.random() * 255) | 0} + 27. Think step by step. Then output the result.`,
timestamp: Date.now(),
},
],
};
@ -228,6 +230,7 @@ async function handleImage<TApi extends Api>(model: Model<TApi>, options?: Optio
},
imageContent,
],
timestamp: Date.now(),
},
],
};
@ -251,6 +254,7 @@ async function multiTurn<TApi extends Api>(model: Model<TApi>, options?: Options
{
role: "user",
content: "Think about this briefly, then calculate 42 * 17 and 453 + 434 using the calculator tool.",
timestamp: Date.now(),
},
],
tools: [calculatorTool],
@ -303,6 +307,7 @@ async function multiTurn<TApi extends Api>(model: Model<TApi>, options?: Options
toolName: block.name,
output: `${result}`,
isError: false,
timestamp: Date.now(),
});
}
}

View file

@ -33,6 +33,7 @@ describe("Tool Call Without Result Tests", () => {
context.messages.push({
role: "user",
content: "Please calculate 25 * 18 using the calculate tool.",
timestamp: Date.now(),
});
// Step 3: Get the assistant's response (should contain a tool call)
@ -54,6 +55,7 @@ describe("Tool Call Without Result Tests", () => {
context.messages.push({
role: "user",
content: "Never mind, just tell me what is 2+2?",
timestamp: Date.now(),
});
// Step 5: The fix should filter out the orphaned tool call, and the request should succeed

View file

@ -22,6 +22,7 @@ async function testEmojiInToolResults<TApi extends Api>(llm: Model<TApi>, option
{
role: "user",
content: "Use the test tool",
timestamp: Date.now(),
},
{
role: "assistant",
@ -44,6 +45,7 @@ async function testEmojiInToolResults<TApi extends Api>(llm: Model<TApi>, option
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "toolUse",
timestamp: Date.now(),
},
],
tools: [
@ -72,6 +74,7 @@ async function testEmojiInToolResults<TApi extends Api>(llm: Model<TApi>, option
- Mathematical symbols:
- Special quotes: "curly" 'quotes'`,
isError: false,
timestamp: Date.now(),
};
context.messages.push(toolResult);
@ -80,6 +83,7 @@ async function testEmojiInToolResults<TApi extends Api>(llm: Model<TApi>, option
context.messages.push({
role: "user",
content: "Summarize the tool result briefly.",
timestamp: Date.now(),
});
// This should not throw a surrogate pair error
@ -97,6 +101,7 @@ async function testRealWorldLinkedInData<TApi extends Api>(llm: Model<TApi>, opt
{
role: "user",
content: "Use the linkedin tool to get comments",
timestamp: Date.now(),
},
{
role: "assistant",
@ -119,6 +124,7 @@ async function testRealWorldLinkedInData<TApi extends Api>(llm: Model<TApi>, opt
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "toolUse",
timestamp: Date.now(),
},
],
tools: [
@ -151,6 +157,7 @@ Unanswered Comments: 2
]
}`,
isError: false,
timestamp: Date.now(),
};
context.messages.push(toolResult);
@ -158,6 +165,7 @@ Unanswered Comments: 2
context.messages.push({
role: "user",
content: "How many comments are there?",
timestamp: Date.now(),
});
// This should not throw a surrogate pair error
@ -175,6 +183,7 @@ async function testUnpairedHighSurrogate<TApi extends Api>(llm: Model<TApi>, opt
{
role: "user",
content: "Use the test tool",
timestamp: Date.now(),
},
{
role: "assistant",
@ -197,6 +206,7 @@ async function testUnpairedHighSurrogate<TApi extends Api>(llm: Model<TApi>, opt
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "toolUse",
timestamp: Date.now(),
},
],
tools: [
@ -218,6 +228,7 @@ async function testUnpairedHighSurrogate<TApi extends Api>(llm: Model<TApi>, opt
toolName: "test_tool",
output: `Text with unpaired surrogate: ${unpairedSurrogate} <- should be sanitized`,
isError: false,
timestamp: Date.now(),
};
context.messages.push(toolResult);
@ -225,6 +236,7 @@ async function testUnpairedHighSurrogate<TApi extends Api>(llm: Model<TApi>, opt
context.messages.push({
role: "user",
content: "What did the tool return?",
timestamp: Date.now(),
});
// This should not throw a surrogate pair error

View file

@ -162,6 +162,7 @@ export class Agent {
role: "user",
content,
attachments: attachments?.length ? attachments : undefined,
timestamp: Date.now(),
};
this.abortController = new AbortController();
@ -311,6 +312,7 @@ export class Agent {
},
stopReason: this.abortController?.signal.aborted ? "aborted" : "error",
errorMessage: err?.message || String(err),
timestamp: Date.now(),
};
this.appendMessage(msg as AppMessage);
this.patch({ error: err?.message || String(err) });

View file

@ -48,6 +48,7 @@ function streamSimpleProxy(
cacheWrite: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
timestamp: Date.now(),
};
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;

View file

@ -229,7 +229,14 @@ export class ToolMessage extends LitElement {
// Render tool content (renderer handles errors and styling)
const result: ToolResultMessageType<any> | undefined = this.aborted
? { role: "toolResult", isError: true, output: "", toolCallId: this.toolCall.id, toolName: this.toolCall.name }
? {
role: "toolResult",
isError: true,
output: "",
toolCallId: this.toolCall.id,
toolName: this.toolCall.name,
timestamp: Date.now(),
}
: this.result;
const renderResult = renderTool(
toolName,

View file

@ -63,7 +63,7 @@ export class ProviderKeyInput extends LitElement {
}
const context: Context = {
messages: [{ role: "user", content: "Reply with: ok" }],
messages: [{ role: "user", content: "Reply with: ok", timestamp: Date.now() }],
};
const result = await complete(model, context, {

View file

@ -208,13 +208,6 @@ export class SettingsDialog extends LitElement {
)}
</div>
</div>
<!-- Footer -->
<div class="pt-4 flex-shrink-0">
<p class="text-xs text-muted-foreground text-center">
${i18n("Settings are stored locally in your browser")}
</p>
</div>
</div>
`,
})}

View file

@ -1,3 +1,4 @@
import type { AgentState } from "../../agent/agent.js";
import { Store } from "../store.js";
import type { SessionData, SessionMetadata, StoreConfig } from "../types.js";
@ -82,7 +83,12 @@ export class SessionsStore extends Store {
}
// Alias methods for backward compatibility
async saveSession(id: string, state: any, metadata: SessionMetadata | undefined, title?: string): Promise<void> {
async saveSession(
id: string,
state: AgentState,
metadata: SessionMetadata | undefined,
title?: string,
): Promise<void> {
// If metadata is provided, use it; otherwise create it from state
const meta: SessionMetadata = metadata || {
id,
@ -90,7 +96,7 @@ export class SessionsStore extends Store {
createdAt: new Date().toISOString(),
lastModified: new Date().toISOString(),
messageCount: state.messages?.length || 0,
usage: state.usage || {
usage: {
input: 0,
output: 0,
cacheRead: 0,

View file

@ -100,7 +100,10 @@ export function createExtractDocumentTool(): AgentTool<typeof extractDocumentSch
// Extract filename from URL
const urlParts = url.split("/");
const fileName = urlParts[urlParts.length - 1]?.split("?")[0] || "document";
let fileName = urlParts[urlParts.length - 1]?.split("?")[0] || "document";
if (url.startsWith("https://arxiv.org/")) {
fileName = fileName + ".pdf";
}
// Use loadAttachment to process the document
const attachment = await loadAttachment(arrayBuffer, fileName);