diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 3e23864e..617ff6e1 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -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) }); diff --git a/packages/agent/src/transports/AppTransport.ts b/packages/agent/src/transports/AppTransport.ts index 60924bfd..9ef1e8ce 100644 --- a/packages/agent/src/transports/AppTransport.ts +++ b/packages/agent/src/transports/AppTransport.ts @@ -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 | undefined; diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index 9c80795b..9c0c8a36 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -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 diff --git a/packages/ai/src/agent/agent-loop.ts b/packages/ai/src/agent/agent-loop.ts index 876e5e93..db2470d1 100644 --- a/packages/ai/src/agent/agent-loop.ts +++ b/packages/ai/src/agent/agent-loop.ts @@ -223,6 +223,7 @@ async function executeToolCalls( output: typeof resultOrError === "string" ? resultOrError : resultOrError.output, details: typeof resultOrError === "string" ? ({} as T) : resultOrError.details, isError, + timestamp: Date.now(), }; results.push(toolResultMessage); diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 4a3605c1..aa979734 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -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", diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 60dfb883..c2d20769 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -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 { diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index 2a4ecd44..db84f225 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -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 { diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 3edbd634..ca6197fc 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -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 { diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 4ce68cad..da5ff586 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -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 { diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 24f55aa8..2afcdaba 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -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 { @@ -115,6 +117,7 @@ export interface ToolResultMessage { output: string; details?: TDetails; isError: boolean; + timestamp: number; // Unix timestamp in milliseconds } export type Message = UserMessage | AssistantMessage | ToolResultMessage; diff --git a/packages/ai/test/abort.test.ts b/packages/ai/test/abort.test.ts index 3ec95022..5164b99f 100644 --- a/packages/ai/test/abort.test.ts +++ b/packages/ai/test/abort.test.ts @@ -9,6 +9,7 @@ async function testAbortSignal(llm: Model, 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(llm: Model, 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(llm: Model, 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 }); diff --git a/packages/ai/test/agent.test.ts b/packages/ai/test/agent.test.ts index e98468fe..f4e6222c 100644 --- a/packages/ai/test/agent.test.ts +++ b/packages/ai/test/agent.test.ts @@ -27,6 +27,7 @@ async function calculateTest(model: Model, 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(model: Model, 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 diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index 722191da..0d0a8a54 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -8,6 +8,7 @@ async function testEmptyMessage(llm: Model, options: Opt const emptyMessage: UserMessage = { role: "user", content: [], + timestamp: Date.now(), }; const context: Context = { @@ -34,6 +35,7 @@ async function testEmptyStringMessage(llm: Model, option { role: "user", content: "", + timestamp: Date.now(), }, ], }; @@ -58,6 +60,7 @@ async function testWhitespaceOnlyMessage(llm: Model, opt { role: "user", content: " \n\t ", + timestamp: Date.now(), }, ], }; @@ -92,6 +95,7 @@ async function testEmptyAssistantMessage(llm: Model, 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(llm: Model, opt { role: "user", content: "Hello, how are you?", + timestamp: Date.now(), }, emptyAssistant, { role: "user", content: "Please respond this time.", + timestamp: Date.now(), }, ], }; diff --git a/packages/ai/test/handoff.test.ts b/packages/ai/test/handoff.test.ts index 0690bf36..b38110a3 100644 --- a/packages/ai/test/handoff.test.ts +++ b/packages/ai/test/handoff.test.ts @@ -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( { 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( 3) What was the temperature? 4) What capital city was mentioned? Please include the specific numbers and names.`, + timestamp: Date.now(), }); const context: Context = { diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index af5a1ef2..5a7f58e5 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -32,7 +32,7 @@ const calculatorTool: Tool = { async function basicTextGeneration(model: Model, options?: OptionsForApi) { 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(model: Model, 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(model: Model, options?: Op { role: "user", content: "Calculate 15 + 27 using the calculator tool.", + timestamp: Date.now(), }, ], tools: [calculatorTool], @@ -141,7 +142,7 @@ async function handleStreaming(model: Model, 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(model: Model, 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(model: Model, options?: Optio }, imageContent, ], + timestamp: Date.now(), }, ], }; @@ -251,6 +254,7 @@ async function multiTurn(model: Model, 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(model: Model, options?: Options toolName: block.name, output: `${result}`, isError: false, + timestamp: Date.now(), }); } } diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index fbf92c2e..53dbc602 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -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 diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index 14c3bd48..1ca80e46 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -22,6 +22,7 @@ async function testEmojiInToolResults(llm: Model, option { role: "user", content: "Use the test tool", + timestamp: Date.now(), }, { role: "assistant", @@ -44,6 +45,7 @@ async function testEmojiInToolResults(llm: Model, 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(llm: Model, option - Mathematical symbols: ∑∫∂√ - Special quotes: "curly" 'quotes'`, isError: false, + timestamp: Date.now(), }; context.messages.push(toolResult); @@ -80,6 +83,7 @@ async function testEmojiInToolResults(llm: Model, 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(llm: Model, opt { role: "user", content: "Use the linkedin tool to get comments", + timestamp: Date.now(), }, { role: "assistant", @@ -119,6 +124,7 @@ async function testRealWorldLinkedInData(llm: Model, 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(llm: Model, opt { role: "user", content: "Use the test tool", + timestamp: Date.now(), }, { role: "assistant", @@ -197,6 +206,7 @@ async function testUnpairedHighSurrogate(llm: Model, 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(llm: Model, 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(llm: Model, opt context.messages.push({ role: "user", content: "What did the tool return?", + timestamp: Date.now(), }); // This should not throw a surrogate pair error diff --git a/packages/web-ui/src/agent/agent.ts b/packages/web-ui/src/agent/agent.ts index 6230b70a..cdcebe42 100644 --- a/packages/web-ui/src/agent/agent.ts +++ b/packages/web-ui/src/agent/agent.ts @@ -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) }); diff --git a/packages/web-ui/src/agent/transports/AppTransport.ts b/packages/web-ui/src/agent/transports/AppTransport.ts index e07889f7..810f78c1 100644 --- a/packages/web-ui/src/agent/transports/AppTransport.ts +++ b/packages/web-ui/src/agent/transports/AppTransport.ts @@ -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 | undefined; diff --git a/packages/web-ui/src/components/Messages.ts b/packages/web-ui/src/components/Messages.ts index 59d64029..3cf70861 100644 --- a/packages/web-ui/src/components/Messages.ts +++ b/packages/web-ui/src/components/Messages.ts @@ -229,7 +229,14 @@ export class ToolMessage extends LitElement { // Render tool content (renderer handles errors and styling) const result: ToolResultMessageType | 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, diff --git a/packages/web-ui/src/components/ProviderKeyInput.ts b/packages/web-ui/src/components/ProviderKeyInput.ts index e569c1bd..e74ec0d8 100644 --- a/packages/web-ui/src/components/ProviderKeyInput.ts +++ b/packages/web-ui/src/components/ProviderKeyInput.ts @@ -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, { diff --git a/packages/web-ui/src/dialogs/SettingsDialog.ts b/packages/web-ui/src/dialogs/SettingsDialog.ts index 1ab617f3..c43f36c7 100644 --- a/packages/web-ui/src/dialogs/SettingsDialog.ts +++ b/packages/web-ui/src/dialogs/SettingsDialog.ts @@ -208,13 +208,6 @@ export class SettingsDialog extends LitElement { )} - - -
-

- ${i18n("Settings are stored locally in your browser")} -

-
`, })} diff --git a/packages/web-ui/src/storage/stores/sessions-store.ts b/packages/web-ui/src/storage/stores/sessions-store.ts index 0dd8f60b..6e6de8a3 100644 --- a/packages/web-ui/src/storage/stores/sessions-store.ts +++ b/packages/web-ui/src/storage/stores/sessions-store.ts @@ -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 { + async saveSession( + id: string, + state: AgentState, + metadata: SessionMetadata | undefined, + title?: string, + ): Promise { // 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, diff --git a/packages/web-ui/src/tools/extract-document.ts b/packages/web-ui/src/tools/extract-document.ts index 61f3a5a7..476aab93 100644 --- a/packages/web-ui/src/tools/extract-document.ts +++ b/packages/web-ui/src/tools/extract-document.ts @@ -100,7 +100,10 @@ export function createExtractDocumentTool(): AgentTool