diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 1f7e321..236feb6 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -3010,7 +3010,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 128000, + contextWindow: 264000, maxTokens: 64000, } satisfies Model<"openai-responses">, "gpt-5.2-codex": { @@ -3033,7 +3033,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 272000, + contextWindow: 400000, maxTokens: 128000, } satisfies Model<"openai-responses">, "gpt-5.3-codex": { @@ -6486,6 +6486,23 @@ export const MODELS = { contextWindow: 204800, maxTokens: 131072, } satisfies Model<"anthropic-messages">, + "nemotron-3-super-free": { + id: "nemotron-3-super-free", + name: "Nemotron 3 Super Free", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, }, "opencode-go": { "glm-5": { @@ -7230,13 +7247,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.25, - output: 0.39999999999999997, - cacheRead: 0, + input: 0.26, + output: 0.38, + cacheRead: 0.13, cacheWrite: 0, }, contextWindow: 163840, - maxTokens: 65536, + maxTokens: 4096, } satisfies Model<"openai-completions">, "deepseek/deepseek-v3.2-exp": { id: "deepseek/deepseek-v3.2-exp", @@ -8360,6 +8377,23 @@ export const MODELS = { contextWindow: 256000, maxTokens: 4096, } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-super-120b-a12b:free": { + id: "nvidia/nemotron-3-super-120b-a12b:free", + name: "NVIDIA: Nemotron 3 Super (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: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, "nvidia/nemotron-nano-12b-v2-vl:free": { id: "nvidia/nemotron-nano-12b-v2-vl:free", name: "NVIDIA: Nemotron Nano 12B 2 VL (free)", @@ -9346,6 +9380,40 @@ export const MODELS = { contextWindow: 200000, maxTokens: 4096, } satisfies Model<"openai-completions">, + "openrouter/healer-alpha": { + id: "openrouter/healer-alpha", + name: "Healer Alpha", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "openrouter/hunter-alpha": { + id: "openrouter/hunter-alpha", + name: "Hunter Alpha", + 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: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, "prime-intellect/intellect-3": { id: "prime-intellect/intellect-3", name: "Prime Intellect: INTELLECT-3", @@ -9848,13 +9916,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.15, - output: 1.2, + input: 0.0975, + output: 0.78, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 128000, - maxTokens: 4096, + contextWindow: 131072, + maxTokens: 32768, } satisfies Model<"openai-completions">, "qwen/qwen3-vl-235b-a22b-instruct": { id: "qwen/qwen3-vl-235b-a22b-instruct", @@ -9882,8 +9950,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0, - output: 0, + input: 0.26, + output: 2.6, cacheRead: 0, cacheWrite: 0, }, @@ -9916,8 +9984,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0, - output: 0, + input: 0.13, + output: 1.56, cacheRead: 0, cacheWrite: 0, }, @@ -10649,13 +10717,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09999999999999999, - output: 0.3, + input: 0.29, + output: 0.59, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 40960, - maxTokens: 16384, + contextWindow: 131072, + maxTokens: 40960, } satisfies Model<"anthropic-messages">, "alibaba/qwen3-235b-a22b-thinking": { id: "alibaba/qwen3-235b-a22b-thinking", @@ -10700,13 +10768,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.07, - output: 0.27, + input: 0.15, + output: 0.6, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 160000, - maxTokens: 32768, + contextWindow: 262144, + maxTokens: 8192, } satisfies Model<"anthropic-messages">, "alibaba/qwen3-coder-next": { id: "alibaba/qwen3-coder-next", @@ -10742,6 +10810,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 65536, } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-max": { + id: "alibaba/qwen3-max", + name: "Qwen3 Max", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 1.2, + output: 6, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, "alibaba/qwen3-max-preview": { id: "alibaba/qwen3-max-preview", name: "Qwen3 Max Preview", @@ -10889,8 +10974,8 @@ export const MODELS = { cost: { input: 3, output: 15, - cacheRead: 0, - cacheWrite: 0, + cacheRead: 0.3, + cacheWrite: 3.75, }, contextWindow: 200000, maxTokens: 8192, @@ -11099,6 +11184,23 @@ export const MODELS = { contextWindow: 256000, maxTokens: 8000, } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-r1": { + id: "deepseek/deepseek-r1", + name: "DeepSeek-R1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.35, + output: 5.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, "deepseek/deepseek-v3": { id: "deepseek/deepseek-v3", name: "DeepSeek V3 0324", @@ -11125,13 +11227,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.21, - output: 0.7899999999999999, + input: 0.5, + output: 1.5, cacheRead: 0, cacheWrite: 0, }, contextWindow: 163840, - maxTokens: 128000, + maxTokens: 16384, } satisfies Model<"anthropic-messages">, "deepseek/deepseek-v3.1-terminus": { id: "deepseek/deepseek-v3.1-terminus", @@ -11184,6 +11286,40 @@ export const MODELS = { contextWindow: 128000, maxTokens: 64000, } satisfies Model<"anthropic-messages">, + "google/gemini-2.0-flash": { + id: "google/gemini-2.0-flash", + name: "Gemini 2.0 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "google/gemini-2.0-flash-lite": { + id: "google/gemini-2.0-flash-lite", + name: "Gemini 2.0 Flash Lite", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, "google/gemini-2.5-flash": { id: "google/gemini-2.5-flash", name: "Gemini 2.5 Flash", @@ -11191,11 +11327,11 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, - input: ["text"], + input: ["text", "image"], cost: { input: 0.3, output: 2.5, - cacheRead: 0, + cacheRead: 0.03, cacheWrite: 0, }, contextWindow: 1000000, @@ -11259,11 +11395,11 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, - input: ["text"], + input: ["text", "image"], cost: { input: 1.25, output: 10, - cacheRead: 0, + cacheRead: 0.125, cacheWrite: 0, }, contextWindow: 1048576, @@ -11284,7 +11420,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 1000000, - maxTokens: 64000, + maxTokens: 65000, } satisfies Model<"anthropic-messages">, "google/gemini-3-pro-preview": { id: "google/gemini-3-pro-preview", @@ -11386,7 +11522,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 8192, + maxTokens: 100000, } satisfies Model<"anthropic-messages">, "meituan/longcat-flash-thinking": { id: "meituan/longcat-flash-thinking", @@ -11414,13 +11550,13 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.39999999999999997, - output: 0.39999999999999997, + input: 0.72, + output: 0.72, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 131072, - maxTokens: 16384, + contextWindow: 128000, + maxTokens: 8192, } satisfies Model<"anthropic-messages">, "meta/llama-3.1-8b": { id: "meta/llama-3.1-8b", @@ -11431,12 +11567,12 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.03, - output: 0.049999999999999996, + input: 0.09999999999999999, + output: 0.09999999999999999, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 131072, + contextWindow: 128000, maxTokens: 16384, } satisfies Model<"anthropic-messages">, "meta/llama-3.2-11b": { @@ -11499,12 +11635,12 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.15, - output: 0.6, + input: 0.24, + output: 0.9700000000000001, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 131072, + contextWindow: 128000, maxTokens: 8192, } satisfies Model<"anthropic-messages">, "meta/llama-4-scout": { @@ -11516,12 +11652,12 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.08, - output: 0.3, + input: 0.16999999999999998, + output: 0.66, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 131072, + contextWindow: 128000, maxTokens: 8192, } satisfies Model<"anthropic-messages">, "minimax/minimax-m2": { @@ -11552,8 +11688,8 @@ export const MODELS = { cost: { input: 0.3, output: 1.2, - cacheRead: 0.15, - cacheWrite: 0, + cacheRead: 0.03, + cacheWrite: 0.375, }, contextWindow: 204800, maxTokens: 131072, @@ -11788,14 +11924,31 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.5, - output: 2, + input: 0.6, + output: 2.5, cacheRead: 0, cacheWrite: 0, }, contextWindow: 131072, maxTokens: 16384, } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2-0905": { + id: "moonshotai/kimi-k2-0905", + name: "Kimi K2 0905", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, "moonshotai/kimi-k2-thinking": { id: "moonshotai/kimi-k2-thinking", name: "Kimi K2 Thinking", @@ -11805,13 +11958,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.47, - output: 2, - cacheRead: 0.14100000000000001, + input: 0.6, + output: 2.5, + cacheRead: 0.15, cacheWrite: 0, }, - contextWindow: 216144, - maxTokens: 216144, + contextWindow: 262114, + maxTokens: 262114, } satisfies Model<"anthropic-messages">, "moonshotai/kimi-k2-thinking-turbo": { id: "moonshotai/kimi-k2-thinking-turbo", @@ -11856,13 +12009,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.5, - output: 2.8, - cacheRead: 0, + input: 0.6, + output: 3, + cacheRead: 0.09999999999999999, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 256000, + contextWindow: 262114, + maxTokens: 262114, } satisfies Model<"anthropic-messages">, "nvidia/nemotron-nano-12b-v2-vl": { id: "nvidia/nemotron-nano-12b-v2-vl", @@ -11890,8 +12043,8 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.04, - output: 0.16, + input: 0.06, + output: 0.22999999999999998, cacheRead: 0, cacheWrite: 0, }, @@ -11960,7 +12113,7 @@ export const MODELS = { cost: { input: 0.09999999999999999, output: 0.39999999999999997, - cacheRead: 0.03, + cacheRead: 0.024999999999999998, cacheWrite: 0, }, contextWindow: 1047576, @@ -12011,7 +12164,7 @@ export const MODELS = { cost: { input: 1.25, output: 10, - cacheRead: 0.13, + cacheRead: 0.125, cacheWrite: 0, }, contextWindow: 400000, @@ -12041,11 +12194,11 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, - input: ["text", "image"], + input: ["text"], cost: { input: 1.25, output: 10, - cacheRead: 0.13, + cacheRead: 0.125, cacheWrite: 0, }, contextWindow: 400000, @@ -12062,7 +12215,7 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.03, + cacheRead: 0.024999999999999998, cacheWrite: 0, }, contextWindow: 400000, @@ -12079,7 +12232,7 @@ export const MODELS = { cost: { input: 0.049999999999999996, output: 0.39999999999999997, - cacheRead: 0.01, + cacheRead: 0.005, cacheWrite: 0, }, contextWindow: 400000, @@ -12113,7 +12266,7 @@ export const MODELS = { cost: { input: 1.25, output: 10, - cacheRead: 0.13, + cacheRead: 0.125, cacheWrite: 0, }, contextWindow: 400000, @@ -12164,7 +12317,7 @@ export const MODELS = { cost: { input: 1.25, output: 10, - cacheRead: 0.13, + cacheRead: 0.125, cacheWrite: 0, }, contextWindow: 128000, @@ -12181,7 +12334,7 @@ export const MODELS = { cost: { input: 1.25, output: 10, - cacheRead: 0.13, + cacheRead: 0.125, cacheWrite: 0, }, contextWindow: 400000, @@ -12198,7 +12351,7 @@ export const MODELS = { cost: { input: 1.75, output: 14, - cacheRead: 0.18, + cacheRead: 0.175, cacheWrite: 0, }, contextWindow: 400000, @@ -12303,7 +12456,7 @@ export const MODELS = { cacheRead: 0.25, cacheWrite: 0, }, - contextWindow: 200000, + contextWindow: 1050000, maxTokens: 128000, } satisfies Model<"anthropic-messages">, "openai/gpt-5.4-pro": { @@ -12320,26 +12473,9 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 200000, + contextWindow: 1050000, maxTokens: 128000, } satisfies Model<"anthropic-messages">, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "gpt-oss-120b", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, "openai/gpt-oss-20b": { id: "openai/gpt-oss-20b", name: "gpt-oss-20b", @@ -12757,9 +12893,9 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09, - output: 0.29, - cacheRead: 0, + input: 0.09999999999999999, + output: 0.3, + cacheRead: 0.02, cacheWrite: 0, }, contextWindow: 262144, @@ -12776,11 +12912,11 @@ export const MODELS = { cost: { input: 0.6, output: 2.2, - cacheRead: 0, + cacheRead: 0.11, cacheWrite: 0, }, - contextWindow: 131072, - maxTokens: 131072, + contextWindow: 128000, + maxTokens: 96000, } satisfies Model<"anthropic-messages">, "zai/glm-4.5-air": { id: "zai/glm-4.5-air", @@ -12805,16 +12941,16 @@ export const MODELS = { api: "anthropic-messages", provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, + reasoning: false, input: ["text", "image"], cost: { input: 0.6, output: 1.7999999999999998, - cacheRead: 0, + cacheRead: 0.11, cacheWrite: 0, }, - contextWindow: 65536, - maxTokens: 16384, + contextWindow: 66000, + maxTokens: 16000, } satisfies Model<"anthropic-messages">, "zai/glm-4.6": { id: "zai/glm-4.6", @@ -12876,12 +13012,12 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.43, - output: 1.75, - cacheRead: 0.08, + input: 0.6, + output: 2.2, + cacheRead: 0, cacheWrite: 0, }, - contextWindow: 202752, + contextWindow: 200000, maxTokens: 120000, } satisfies Model<"anthropic-messages">, "zai/glm-4.7-flash": { @@ -12927,9 +13063,9 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.7999999999999999, - output: 2.56, - cacheRead: 0.16, + input: 1, + output: 3.1999999999999997, + cacheRead: 0.19999999999999998, cacheWrite: 0, }, contextWindow: 202800, diff --git a/packages/coding-agent/src/core/gateway/internal-types.ts b/packages/coding-agent/src/core/gateway/internal-types.ts index 40c1732..88274d4 100644 --- a/packages/coding-agent/src/core/gateway/internal-types.ts +++ b/packages/coding-agent/src/core/gateway/internal-types.ts @@ -55,7 +55,29 @@ export type GatewayEvent = } | { type: "message_complete"; sessionKey: string; text: string } | { type: "error"; sessionKey: string; error: string } - | { type: "aborted"; sessionKey: string }; + | { type: "aborted"; sessionKey: string } + | { + type: "structured_part"; + sessionKey: string; + partType: "teamActivity"; + payload: { + teamId: string; + status: string; + members: Array<{ id: string; name: string; role?: string; status: string; message?: string }>; + }; + } + | { + type: "structured_part"; + sessionKey: string; + partType: "media"; + payload: { url: string; mimeType?: string }; + } + | { + type: "structured_part"; + sessionKey: string; + partType: "error"; + payload: { code: string; message: string }; + }; export interface ManagedGatewaySession { sessionKey: string; diff --git a/packages/coding-agent/src/core/gateway/runtime.ts b/packages/coding-agent/src/core/gateway/runtime.ts index e8e8b3d..0a7fb9b 100644 --- a/packages/coding-agent/src/core/gateway/runtime.ts +++ b/packages/coding-agent/src/core/gateway/runtime.ts @@ -7,6 +7,7 @@ import { import { rm } from "node:fs/promises"; import { join } from "node:path"; import { URL } from "node:url"; +import type { AgentMessage } from "@mariozechner/companion-agent-core"; import type { AgentSession, AgentSessionEvent } from "../agent-session.js"; import type { Settings } from "../settings-manager.js"; import { extractMessageText, getLastAssistantText } from "./helpers.js"; @@ -29,6 +30,7 @@ import type { ModelInfo, } from "./types.js"; import { + createGatewayStructuredPartListener, createVercelStreamListener, errorVercelStream, extractUserText, @@ -265,7 +267,7 @@ export class GatewayRuntime { sessionKey: string, listener: (event: GatewayEvent) => void, ): Promise<() => void> { - const managedSession = await this.requireExistingSession(sessionKey); + const managedSession = await this.ensureSession(sessionKey); managedSession.listeners.add(listener); listener({ type: "hello", @@ -567,6 +569,7 @@ export class GatewayRuntime { sessionKey: managedSession.sessionKey, text: extractMessageText(event.message), }); + this.emitStructuredParts(managedSession, event.message); return; } if (event.message.role === "toolResult") { @@ -654,6 +657,76 @@ export class GatewayRuntime { }); } + private emitStructuredParts( + managedSession: ManagedGatewaySession, + message: AgentMessage, + ): void { + const content = message.content; + if (!Array.isArray(content)) return; + + for (const part of content) { + if (typeof part !== "object" || part === null) continue; + const p = part as Record; + + if (p.type === "teamActivity") { + const teamId = typeof p.teamId === "string" ? p.teamId : ""; + const status = typeof p.status === "string" ? p.status : "running"; + if (!teamId) continue; + const rawMembers = Array.isArray(p.members) ? p.members : []; + const members = rawMembers + .filter( + (m): m is Record => + typeof m === "object" && m !== null, + ) + .map((m) => ({ + id: typeof m.id === "string" ? m.id : "", + name: typeof m.name === "string" ? m.name : "Teammate", + ...(typeof m.role === "string" ? { role: m.role } : {}), + status: typeof m.status === "string" ? m.status : "running", + ...(typeof m.message === "string" ? { message: m.message } : {}), + })) + .filter((m) => m.id.length > 0); + this.emit(managedSession, { + type: "structured_part", + sessionKey: managedSession.sessionKey, + partType: "teamActivity", + payload: { teamId, status, members }, + }); + continue; + } + + if (p.type === "image") { + const url = typeof p.url === "string" ? p.url : ""; + if (!url) continue; + this.emit(managedSession, { + type: "structured_part", + sessionKey: managedSession.sessionKey, + partType: "media", + payload: { + url, + ...(typeof p.mimeType === "string" ? { mimeType: p.mimeType } : {}), + }, + }); + continue; + } + + if (p.type === "error") { + const errorMessage = typeof p.message === "string" ? p.message : ""; + if (!errorMessage) continue; + this.emit(managedSession, { + type: "structured_part", + sessionKey: managedSession.sessionKey, + partType: "error", + payload: { + code: typeof p.code === "string" ? p.code : "unknown", + message: errorMessage, + }, + }); + continue; + } + } + } + private createSessionState( managedSession: ManagedGatewaySession, ): GatewaySessionState { @@ -740,7 +813,28 @@ export class GatewayRuntime { request: IncomingMessage, response: ServerResponse, ): Promise { + const origin = request.headers.origin; + if (origin) { + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader( + "Access-Control-Allow-Methods", + "GET, POST, PUT, PATCH, DELETE, OPTIONS", + ); + response.setHeader( + "Access-Control-Allow-Headers", + "Content-Type, Authorization", + ); + response.setHeader("Access-Control-Allow-Credentials", "true"); + } + const method = request.method ?? "GET"; + + if (method === "OPTIONS") { + response.writeHead(204); + response.end(); + return; + } + const url = new URL( request.url ?? "/", `http://${request.headers.host ?? `${this.config.bind}:${this.config.port}`}`, @@ -919,7 +1013,7 @@ export class GatewayRuntime { const action = sessionMatch[2]; if (!action && method === "GET") { - const session = await this.requireExistingSession(sessionKey); + const session = await this.ensureSession(sessionKey); this.writeJson(response, 200, { session: this.createSnapshot(session) }); return; } @@ -1106,7 +1200,10 @@ export class GatewayRuntime { response.write("\n"); const listener = createVercelStreamListener(response); + const structuredPartListener = + createGatewayStructuredPartListener(response); let unsubscribe: (() => void) | undefined; + let unsubscribeStructured: (() => void) | undefined; let streamingActive = false; const stopStreaming = () => { @@ -1114,6 +1211,8 @@ export class GatewayRuntime { streamingActive = false; unsubscribe?.(); unsubscribe = undefined; + unsubscribeStructured?.(); + unsubscribeStructured = undefined; }; // Clean up on client disconnect @@ -1135,6 +1234,10 @@ export class GatewayRuntime { onStart: () => { if (clientDisconnected || streamingActive) return; unsubscribe = managedSession.session.subscribe(listener); + managedSession.listeners.add(structuredPartListener); + unsubscribeStructured = () => { + managedSession.listeners.delete(structuredPartListener); + }; streamingActive = true; }, onFinish: () => { @@ -1283,7 +1386,7 @@ export class GatewayRuntime { provider: string, modelId: string, ): Promise<{ ok: true; model: { provider: string; modelId: string } }> { - const managed = await this.requireExistingSession(sessionKey); + const managed = await this.ensureSession(sessionKey); const found = managed.session.modelRegistry.find(provider, modelId); if (!found) { throw new HttpError(404, `Model not found: ${provider}/${modelId}`); @@ -1389,7 +1492,8 @@ export class GatewayRuntime { } private getCompanionChannelsSettings(): CompanionChannelsSettings { - const globalSettings = this.primarySession.settingsManager.getGlobalSettings(); + const globalSettings = + this.primarySession.settingsManager.getGlobalSettings(); const projectSettings = this.primarySession.settingsManager.getProjectSettings(); const mergedSettings = mergeRecords( @@ -1397,7 +1501,9 @@ export class GatewayRuntime { isRecord(projectSettings) ? projectSettings : {}, ); const piChannels = mergedSettings["companion-channels"]; - return isRecord(piChannels) ? (piChannels as CompanionChannelsSettings) : {}; + return isRecord(piChannels) + ? (piChannels as CompanionChannelsSettings) + : {}; } private buildSlackChannelStatus( @@ -1419,7 +1525,8 @@ export class GatewayRuntime { if (hasConfig) { if (!adapter) { - error = 'Slack requires `companion-channels.adapters.slack = { "type": "slack" }`.'; + error = + 'Slack requires `companion-channels.adapters.slack = { "type": "slack" }`.'; } else if (adapterType !== "slack") { error = 'Slack adapter type must be "slack".'; } else if (!appToken) { @@ -1468,7 +1575,8 @@ export class GatewayRuntime { } else if (adapterType !== "telegram") { error = 'Telegram adapter type must be "telegram".'; } else if (!botToken) { - error = "Telegram requires companion-channels.adapters.telegram.botToken."; + error = + "Telegram requires companion-channels.adapters.telegram.botToken."; } else if (!pollingEnabled) { error = "Telegram requires companion-channels.adapters.telegram.polling = true."; diff --git a/packages/coding-agent/src/core/gateway/session-state.ts b/packages/coding-agent/src/core/gateway/session-state.ts index a2534cc..78e48db 100644 --- a/packages/coding-agent/src/core/gateway/session-state.ts +++ b/packages/coding-agent/src/core/gateway/session-state.ts @@ -85,6 +85,41 @@ export function messageContentToHistoryParts(msg: AgentMessage): HistoryPart[] { args: toolCall.arguments, state: "call", }); + } else if (contentPart.type === "teamActivity") { + const activity = contentPart as { + type: "teamActivity"; + teamId: string; + status: string; + members?: Array<{ id: string; name: string; role?: string; status: string; message?: string }>; + }; + parts.push({ + type: "teamActivity", + teamId: activity.teamId, + status: activity.status, + members: Array.isArray(activity.members) ? activity.members : [], + }); + } else if (contentPart.type === "image") { + const image = contentPart as { + type: "image"; + url: string; + mimeType?: string; + }; + parts.push({ + type: "media", + url: image.url, + mimeType: image.mimeType, + }); + } else if (contentPart.type === "error") { + const error = contentPart as { + type: "error"; + code?: string; + message: string; + }; + parts.push({ + type: "error", + code: typeof error.code === "string" ? error.code : "unknown", + message: error.message, + }); } } return parts; diff --git a/packages/coding-agent/src/core/gateway/types.ts b/packages/coding-agent/src/core/gateway/types.ts index 77c8091..938bdd4 100644 --- a/packages/coding-agent/src/core/gateway/types.ts +++ b/packages/coding-agent/src/core/gateway/types.ts @@ -77,7 +77,15 @@ export type HistoryPart = args: unknown; state: string; result?: unknown; - }; + } + | { + type: "teamActivity"; + teamId: string; + status: string; + members: Array<{ id: string; name: string; role?: string; status: string; message?: string }>; + } + | { type: "media"; url: string; mimeType?: string } + | { type: "error"; code: string; message: string }; export interface ChannelStatus { id: string; diff --git a/packages/coding-agent/src/core/gateway/vercel-ai-stream.ts b/packages/coding-agent/src/core/gateway/vercel-ai-stream.ts index 3fcaab7..a1a7262 100644 --- a/packages/coding-agent/src/core/gateway/vercel-ai-stream.ts +++ b/packages/coding-agent/src/core/gateway/vercel-ai-stream.ts @@ -1,6 +1,7 @@ import { randomUUID } from "node:crypto"; import type { ServerResponse } from "node:http"; import type { AgentSessionEvent } from "../agent-session.js"; +import type { GatewayEvent } from "./internal-types.js"; type TextStreamState = { started: boolean; @@ -324,3 +325,22 @@ export function errorVercelStream( writeChunk(response, "[DONE]"); response.end(); } + +/** + * Create a GatewayEvent listener that forwards `structured_part` events to the + * response as custom SSE chunks. Returns the listener function so the caller + * can subscribe it to managedSession.listeners and unsubscribe on cleanup. + */ +export function createGatewayStructuredPartListener( + response: ServerResponse, +): (event: GatewayEvent) => void { + return (event: GatewayEvent) => { + if (response.writableEnded) return; + if (event.type !== "structured_part") return; + writeChunk(response, { + type: "structured-part", + partType: event.partType, + payload: event.payload, + }); + }; +}