diff --git a/packages/docs/src/content/docs/reference/config-and-env.md b/packages/docs/src/content/docs/reference/config-and-env.md index 4f2971ae30..3bf7d1fec0 100644 --- a/packages/docs/src/content/docs/reference/config-and-env.md +++ b/packages/docs/src/content/docs/reference/config-and-env.md @@ -30,6 +30,7 @@ related: | `AI_REASONING_LEVEL` | No | Main-agent reasoning override: `none`, `low`, `medium`, `high`, or `xhigh`. Unset by default. An explicit profile reasoning level takes precedence when routing is enabled. | | `AI_FAST_MODEL` | No | Faster model for lightweight tasks and routing/classification passes before the main turn begins. Defaults to `openai/gpt-6-luna`. | | `AI_GUARDIAN_MODEL` | No | Model for Guardian action review. Defaults to `openai/gpt-6-luna`. | +| `AI_DECISION_MODEL` | No | Decision model for passive reply routing in subscribed threads. Uses the AI SDK decision API. Defaults to `typesafe-ai/jev`. | | `AI_HANDOFF_MODEL` | No | Deprecated profile setting. Creates `handoff`. Defaults to `anthropic/claude-opus-5.5` with high reasoning. | | `AI_MODEL_PROFILES` | No | Deprecated JSON map of profile names to model IDs for env-only setup. Names must match `^[a-z][a-z0-9_-]*$`. | | `AI_EMBEDDING_MODEL` | No | Embedding model for plugin-owned vector retrieval. Defaults to `openai/text-embedding-3-small`; memory v1 stores fixed 1536-dimensional vectors. | @@ -270,6 +271,7 @@ const app = await createApp({ reasoningLevel: "high", }, }, + decisionModelId: "typesafe-ai/jev", fastModelId: "openai/gpt-6-luna", guardianModelId: "openai/gpt-6-luna", embeddingModelId: "openai/text-embedding-3-small", diff --git a/packages/junior-evals/package.json b/packages/junior-evals/package.json index 3bbec4641e..f7fabb0969 100644 --- a/packages/junior-evals/package.json +++ b/packages/junior-evals/package.json @@ -23,7 +23,7 @@ "@sentry/junior-plugin-api": "workspace:*", "@sentry/junior-sentry": "workspace:*", "@sentry/junior-testing": "workspace:*", - "ai": "^6.0.190", + "ai": "^7.0.153", "chat": "4.29.0", "hono": "^4.12.27", "msw": "^2.14.6", diff --git a/packages/junior-evals/src/fixture/web.ts b/packages/junior-evals/src/fixture/web.ts index 8b63e1ed0e..349628daaf 100644 --- a/packages/junior-evals/src/fixture/web.ts +++ b/packages/junior-evals/src/fixture/web.ts @@ -36,7 +36,7 @@ export interface WebSearchResult { url: string; } -const SEARCH_REQUEST_URL = "https://ai-gateway.vercel.sh/v3/ai/language-model"; +const SEARCH_REQUEST_URL = "https://ai-gateway.vercel.sh/v4/ai/language-model"; const SEARCH_TOOL_ID = "gateway.parallel_search"; let searchResults: WebSearchResult[] = []; diff --git a/packages/junior-evals/src/recording-rules.ts b/packages/junior-evals/src/recording-rules.ts index c622bc4eb7..bcc05342b9 100644 --- a/packages/junior-evals/src/recording-rules.ts +++ b/packages/junior-evals/src/recording-rules.ts @@ -46,7 +46,7 @@ const RECORDING_RULES: RecordingRule[] = [ // image generation and web search before the proxy. name: "model", match: { method: "POST", url: "https://ai-gateway.vercel.sh/" }, - // AI SDK gateway requests, such as `/v3/ai/language-model`, name the + // AI SDK gateway requests, such as `/v4/ai/language-model`, name the // model in a header, not in the body. keyHeaders: [ "ai-language-model-id", diff --git a/packages/junior/package.json b/packages/junior/package.json index b1d38716db..3285686e19 100644 --- a/packages/junior/package.json +++ b/packages/junior/package.json @@ -69,7 +69,7 @@ }, "dependencies": { "@agentclientprotocol/sdk": "1.3.0", - "@ai-sdk/gateway": "^3.0.119", + "@ai-sdk/gateway": "^4.0.126", "@chat-adapter/slack": "4.29.0", "@chat-adapter/state-memory": "4.29.0", "@chat-adapter/state-redis": "4.29.0", @@ -91,7 +91,7 @@ "@vercel/oidc": "^3.4.1", "@vercel/queue": "^0.2.0", "@vercel/sandbox": "2.8.0", - "ai": "^6.0.190", + "ai": "^7.0.153", "better-auth": "^1.6.22", "chat": "4.29.0", "commander": "^14.0.3", diff --git a/packages/junior/src/chat/app/services.ts b/packages/junior/src/chat/app/services.ts index e15d13d323..f255617adf 100644 --- a/packages/junior/src/chat/app/services.ts +++ b/packages/junior/src/chat/app/services.ts @@ -1,4 +1,4 @@ -import { completeObject, completeText } from "@/chat/pi/client"; +import { completeDecision, completeText } from "@/chat/pi/client"; import { executeAgentRun as executeAgentRunImpl } from "@/chat/agent"; import type { SandboxEgressTracePropagationConfig } from "@/chat/sandbox/egress/tracing"; import { @@ -86,8 +86,8 @@ export function createJuniorRuntimeServices( executeTurn: async (run, saveResult) => await executeTurn(agentRunner, run, saveResult), subscribedReplyPolicy: createSubscribedReplyPolicy({ - completeObject: - overrides.subscribedReplyPolicy?.completeObject ?? completeObject, + completeDecision: + overrides.subscribedReplyPolicy?.completeDecision ?? completeDecision, }), visionContext, }; diff --git a/packages/junior/src/chat/config.ts b/packages/junior/src/chat/config.ts index 4c1b8612b1..2346155423 100644 --- a/packages/junior/src/chat/config.ts +++ b/packages/junior/src/chat/config.ts @@ -77,6 +77,8 @@ const DEFAULT_ASSISTANT_LOADING_MESSAGES = [ ] as const; export interface BotModelConfig { + /** Decision model used by the passive reply router. */ + decisionModelId?: string; /** Model used for embeddings. */ embeddingModelId?: string; /** Model used for low-cost structured and utility calls. */ @@ -94,6 +96,7 @@ export interface BotModelConfig { export interface BotConfig { contextWindowTokens: number; crossActorMidRunMode: CrossActorMidRunMode; + decisionModelId: string; defaultProfile: ModelProfile; embeddingModelId: string; fastModelId: string; @@ -261,6 +264,7 @@ const DEFAULT_HANDOFF_MODEL_ID = resolveGatewayModel( DEFAULT_MODEL_PROFILES.handoff.modelId, ).id; const DEFAULT_WEB_SEARCH_MODEL_ID = resolveGatewayModel("openai/gpt-6-luna").id; +const DEFAULT_DECISION_MODEL_ID = "typesafe-ai/jev"; const DEFAULT_EMBEDDING_MODEL_ID = "openai/text-embedding-3-small"; const DEFAULT_IMAGE_GENERATION_MODEL_ID = "google/gemini-3-pro-image"; const DEFAULT_VISION_MODEL_ID = resolveGatewayModel("openai/gpt-5.6-sol").id; @@ -502,6 +506,8 @@ function readBotConfig( reasoningLevel === undefined ? undefined : parseTurnReasoningLevel(reasoningLevel), + decisionModelId: + toOptionalTrimmed(env.AI_DECISION_MODEL) ?? DEFAULT_DECISION_MODEL_ID, fastModelId, guardianModelId, imageGenerationModelId: @@ -825,6 +831,13 @@ export interface SlackReactionConfig { /** Apply model overrides from createApp(). */ export function setBotModelConfig(config: BotModelConfig): void { + if (config.decisionModelId !== undefined) { + botConfig.decisionModelId = requireModelId( + "decisionModelId", + config.decisionModelId, + toOptionalTrimmed, + ); + } if (config.embeddingModelId !== undefined) { botConfig.embeddingModelId = requireModelId( "embeddingModelId", diff --git a/packages/junior/src/chat/pi/client.ts b/packages/junior/src/chat/pi/client.ts index cc9b093c94..fdcd5a5a9b 100644 --- a/packages/junior/src/chat/pi/client.ts +++ b/packages/junior/src/chat/pi/client.ts @@ -21,8 +21,13 @@ export { } from "@/chat/pi/gateway-auth"; import { embedMany, + experimental_decide, + Experimental_DecisionRefusalError, generateObject, NoObjectGeneratedError, + type Experimental_DecisionQuestion, + type Experimental_DecisionResult, + type Experimental_DecisionState, type GenerateObjectResult, type LanguageModelUsage, } from "ai"; @@ -179,6 +184,7 @@ export const GEN_AI_SERVER_ADDRESS = "ai-gateway.vercel.sh"; export const GEN_AI_SERVER_PORT = 443; const GEN_AI_OPERATION_CHAT = "chat" as const; const GEN_AI_OPERATION_EMBEDDINGS = "embeddings" as const; +const GEN_AI_OPERATION_DECIDE = "decide" as const; // AI Gateway input rates in USD per million tokens. Keep estimates // model-specific so unlisted configured models remain explicitly unknown. const EMBEDDING_INPUT_COST_USD_PER_MILLION_TOKENS: Readonly< @@ -194,6 +200,25 @@ const EMBEDDING_INPUT_COST_USD_PER_MILLION_TOKENS: Readonly< "voyage/voyage-3.5": 0.06, "voyage/voyage-3.5-lite": 0.02, }; +// Decision models bill input tokens only. +const DECISION_INPUT_COST_USD_PER_MILLION_TOKENS: Readonly< + Record +> = { + "openai/gpt-6-luna-decisions": 0.1, + "typesafe-ai/jev": 0.042, +}; + +function decisionCostUsd( + modelId: string, + inputTokens: number | undefined, +): number | undefined { + const costPerMillionTokens = + DECISION_INPUT_COST_USD_PER_MILLION_TOKENS[modelId]; + return costPerMillionTokens === undefined || inputTokens === undefined + ? undefined + : (inputTokens * costPerMillionTokens) / 1_000_000; +} + function embeddingCostUsd( modelId: string, inputTokens: number, @@ -552,6 +577,80 @@ export async function completeObject(params: { }; } +/** Answer typed decision questions with an AI Gateway decision model, such as Jev. */ +export async function completeDecision< + const TQuestions extends Record, +>(params: { + modelId: string; + state: Experimental_DecisionState; + questions: TQuestions; + signal?: AbortSignal; + metadata?: Record; + promptName?: string; +}): Promise<{ + answers: Experimental_DecisionResult["answers"]; + costUsd?: number; +}> { + const credential = await resolveGatewayCredential(); + const provider = createGatewayProvider( + credential ? { apiKey: credential.token } : {}, + ); + try { + const result = await withSpan( + `${GEN_AI_OPERATION_DECIDE} ${params.modelId}`, + "gen_ai.decide", + logContextFromMetadata(params.modelId, params.metadata), + async (setSpanAttributes) => { + const result = await experimental_decide({ + model: provider.decisionModel(params.modelId), + state: params.state, + questions: params.questions, + ...(params.signal !== undefined + ? { abortSignal: params.signal } + : undefined), + }); + const costUsd = decisionCostUsd( + params.modelId, + result.usage.inputTokens, + ); + setSpanAttributes( + extractGenAiUsageAttributes({ + ...result.usage, + ...(costUsd !== undefined + ? { cost: { input: costUsd, total: costUsd } } + : undefined), + }), + ); + return { answers: result.answers, costUsd }; + }, + { + "gen_ai.provider.name": GEN_AI_PROVIDER_NAME, + "gen_ai.operation.name": GEN_AI_OPERATION_DECIDE, + "gen_ai.request.model": params.modelId, + ...(params.promptName + ? { "gen_ai.prompt.name": params.promptName } + : undefined), + "server.address": GEN_AI_SERVER_ADDRESS, + "server.port": GEN_AI_SERVER_PORT, + "gen_ai.provider.auth_mode": credential?.mode ?? "api_key", + }, + ); + return { + answers: result.answers, + ...(result.costUsd !== undefined + ? { costUsd: result.costUsd } + : undefined), + }; + } catch (error) { + throw createProviderError(error, { + ...(Experimental_DecisionRefusalError.isInstance(error) + ? { kind: "invalid_response" } + : undefined), + modelId: params.modelId, + }); + } +} + /** Generate text embeddings through the host-owned AI Gateway provider. */ export async function embedTexts(params: { modelId: string; diff --git a/packages/junior/src/chat/services/subscribed-decision.ts b/packages/junior/src/chat/services/subscribed-decision.ts index fa09050158..f396d33026 100644 --- a/packages/junior/src/chat/services/subscribed-decision.ts +++ b/packages/junior/src/chat/services/subscribed-decision.ts @@ -1,8 +1,11 @@ -import { z } from "zod"; import { isExperimentalFeatureEnabled } from "@/chat/experimental"; import { estimateTextTokens } from "@/chat/services/context-budget"; import { isProviderRetryError } from "@/chat/services/provider-error"; -import { buildSubscribedReplyRouterPolicy } from "@/chat/services/subscribed-reply-router-policy"; +import { + buildSubscribedReplyRouterQuestions, + type SubscribedReplyRoute, + type SubscribedReplyRouterQuestions, +} from "@/chat/services/subscribed-reply-router-policy"; /** Why the subscribed-thread router chose to reply or stay silent. */ export const SubscribedReplyReason = { @@ -61,29 +64,7 @@ interface RouterEvidence { omittedEntries: number; } -const replyDecisionSchema = z - .object({ - should_reply: z - .boolean() - .describe("Whether Junior should respond to this thread message."), - should_unsubscribe: z - .boolean() - .describe( - "Whether Junior should unsubscribe from this thread because the user clearly asked it to stop participating.", - ), - confidence: z - .number() - .min(0) - .max(1) - .describe("Classifier confidence from 0 to 1."), - reason: z - .string() - .describe("Short reason for the decision; use an empty string if none."), - }) - .strict(); - const ROUTER_CONFIDENCE_THRESHOLD = 0.7; -const ROUTER_CLASSIFIER_MAX_TOKENS = 400; const MAX_MESSAGE_TRANSCRIPT_TOKENS = 10_000; const MAX_MESSAGE_ENTRY_TOKENS = 2_000; const RECENT_ASSISTANT_ENTRY_LIMIT = 40; @@ -437,21 +418,23 @@ export function getSubscribedReplyPreflightDecision(args: { }; } -/** Decide whether to reply to a message in a subscribed thread using an LLM classifier. */ +/** Decide whether to reply to a message in a subscribed thread using a decision model. */ export async function decideSubscribedThreadReply(args: { botUserName: string; modelId: string; input: SubscribedDecisionInput; - completeObject: (args: { + completeDecision: (args: { modelId: string; - schema: typeof replyDecisionSchema; - maxTokens: number; - temperature: number; - system: string; - prompt: string; + state: string; + questions: SubscribedReplyRouterQuestions; metadata: Record; - promptName?: string; - }) => Promise<{ costUsd?: number; object: unknown }>; + promptName: string; + }) => Promise<{ + answers: { + route: { choice: string; probabilities?: Record }; + }; + costUsd?: number; + }>; logClassifierFailure: ( error: unknown, input: SubscribedDecisionInput, @@ -523,13 +506,10 @@ export async function decideSubscribedThreadReply(args: { } try { - const result = await args.completeObject({ + const result = await args.completeDecision({ modelId: args.modelId, - schema: replyDecisionSchema, - maxTokens: ROUTER_CLASSIFIER_MAX_TOKENS, - temperature: 0, - system: buildSubscribedReplyRouterPolicy(args.botUserName), - prompt: buildRouterPrompt(rawText, evidence), + state: buildRouterPrompt(rawText, evidence), + questions: buildSubscribedReplyRouterQuestions(args.botUserName), metadata: { modelId: args.modelId, threadId: args.input.context.threadId ?? "", @@ -539,61 +519,11 @@ export async function decideSubscribedThreadReply(args: { }, promptName: "junior.passive_reply_route", }); - - const parsed = replyDecisionSchema.parse(result.object); - const reason = parsed.reason?.trim() || "classifier"; - if (parsed.should_unsubscribe) { - if (parsed.confidence < ROUTER_CONFIDENCE_THRESHOLD) { - return { - ...(result.costUsd !== undefined - ? { costUsd: result.costUsd } - : undefined), - shouldReply: false, - reason: SubscribedReplyReason.LowConfidence, - reasonDetail: `${parsed.confidence.toFixed(2)}: ${reason}`, - }; - } - - return { - ...(result.costUsd !== undefined - ? { costUsd: result.costUsd } - : undefined), - shouldReply: false, - shouldUnsubscribe: true, - reason: SubscribedReplyReason.ThreadOptOut, - reasonDetail: reason, - }; - } - - if (!parsed.should_reply) { - return { - ...(result.costUsd !== undefined - ? { costUsd: result.costUsd } - : undefined), - shouldReply: false, - reason: SubscribedReplyReason.SideConversation, - reasonDetail: reason, - }; - } - - if (parsed.confidence < ROUTER_CONFIDENCE_THRESHOLD) { - return { - ...(result.costUsd !== undefined - ? { costUsd: result.costUsd } - : undefined), - shouldReply: false, - reason: SubscribedReplyReason.LowConfidence, - reasonDetail: `${parsed.confidence.toFixed(2)}: ${reason}`, - }; - } - return { ...(result.costUsd !== undefined ? { costUsd: result.costUsd } : undefined), - shouldReply: true, - reason: SubscribedReplyReason.Classifier, - reasonDetail: reason, + ...routeDecisionResult(result.answers.route), }; } catch (error) { if (isProviderRetryError(error)) { @@ -606,3 +536,47 @@ export async function decideSubscribedThreadReply(args: { }; } } + +function isSubscribedReplyRoute(value: string): value is SubscribedReplyRoute { + return value === "reply" || value === "stay_quiet" || value === "unsubscribe"; +} + +function routeDecisionResult(answer: { + choice: string; + probabilities?: Record; +}): SubscribedDecisionResult { + const route = answer.choice; + if (!isSubscribedReplyRoute(route)) { + throw new Error(`Decision model returned unknown route: ${route}`); + } + // A provider without probabilities still returns its most likely choice. + const probability = answer.probabilities?.[route] ?? 1; + const detail = `${route} p=${probability.toFixed(2)}`; + if (route === "stay_quiet") { + return { + shouldReply: false, + reason: SubscribedReplyReason.SideConversation, + reasonDetail: detail, + }; + } + if (probability < ROUTER_CONFIDENCE_THRESHOLD) { + return { + shouldReply: false, + reason: SubscribedReplyReason.LowConfidence, + reasonDetail: detail, + }; + } + if (route === "unsubscribe") { + return { + shouldReply: false, + shouldUnsubscribe: true, + reason: SubscribedReplyReason.ThreadOptOut, + reasonDetail: detail, + }; + } + return { + shouldReply: true, + reason: SubscribedReplyReason.Classifier, + reasonDetail: detail, + }; +} diff --git a/packages/junior/src/chat/services/subscribed-reply-policy.ts b/packages/junior/src/chat/services/subscribed-reply-policy.ts index 6526c5c754..5a8ef18ca9 100644 --- a/packages/junior/src/chat/services/subscribed-reply-policy.ts +++ b/packages/junior/src/chat/services/subscribed-reply-policy.ts @@ -4,10 +4,11 @@ import { decideSubscribedThreadReply, type SubscribedDecisionInput, } from "@/chat/services/subscribed-decision"; -import type { completeObject } from "@/chat/pi/client"; export interface SubscribedReplyPolicyDeps { - completeObject: typeof completeObject; + completeDecision: Parameters< + typeof decideSubscribedThreadReply + >[0]["completeDecision"]; } export interface SubscribedReplyDecision { @@ -27,9 +28,9 @@ export function createSubscribedReplyPolicy( return async (args) => { const decision = await decideSubscribedThreadReply({ botUserName: botConfig.userName, - modelId: botConfig.fastModelId, + modelId: botConfig.decisionModelId, input: args, - completeObject: deps.completeObject, + completeDecision: deps.completeDecision, logClassifierFailure: (error) => { logWarn("subscribed_message.classifier.failed", { "exception.message": @@ -42,7 +43,9 @@ export function createSubscribedReplyPolicy( ? `${decision.reason}:${decision.reasonDetail}` : decision.reason; return { - ...(decision.costUsd !== undefined ? { costUsd: decision.costUsd } : undefined), + ...(decision.costUsd !== undefined + ? { costUsd: decision.costUsd } + : undefined), shouldReply: decision.shouldReply, shouldUnsubscribe: decision.shouldUnsubscribe, reason, diff --git a/packages/junior/src/chat/services/subscribed-reply-router-policy.ts b/packages/junior/src/chat/services/subscribed-reply-router-policy.ts index 10d758f226..108f6401c3 100644 --- a/packages/junior/src/chat/services/subscribed-reply-router-policy.ts +++ b/packages/junior/src/chat/services/subscribed-reply-router-policy.ts @@ -1,3 +1,6 @@ +/** One passive reply route that the decision model can choose. */ +export type SubscribedReplyRoute = "reply" | "stay_quiet" | "unsubscribe"; + /** * Passive subscribed-thread reply router policy. * @@ -9,11 +12,9 @@ * This router judges one inbound Slack message from bounded visible * user/assistant thread evidence and decides whether Junior should reply. */ -export function buildSubscribedReplyRouterPolicy( - assistantName: string, -): string { +function buildSubscribedReplyRouterPolicy(assistantName: string): string { return `You are judging whether the Slack assistant named ${assistantName} should reply to one latest message in a subscribed thread. -Assess conversation floor ownership and whether the latest message continues work ${assistantName} was already doing. Derive should_reply and should_unsubscribe only after that assessment. Your primary objective is to re-engage when the thread is still talking to ${assistantName}, while staying quiet for human-to-human side conversation. +Assess conversation floor ownership and whether the latest message continues work ${assistantName} was already doing. Choose the route only after that assessment. Your primary objective is to re-engage when the thread is still talking to ${assistantName}, while staying quiet for human-to-human side conversation. # Evidence Handling - Core-supplied routing signals and the assistant name are authoritative facts. @@ -30,24 +31,44 @@ Assess conversation floor ownership and whether the latest message continues wor - If one or more humans spoke after ${assistantName}, the floor is human-to-human unless the latest message turns back to ${assistantName} or clearly continues work ${assistantName} was already handling. - Shared topic vocabulary alone does not give ${assistantName} the floor. - Explicit @mentions of another person usually mean the floor is not with ${assistantName}, unless the message also clearly asks ${assistantName} to act. +- A message that starts by addressing another person by name is aimed at that person, not at ${assistantName}. +- After one human addresses another human, later messages from either of them stay in that human exchange until one of them clearly turns back to ${assistantName}. # Reply Policy -- should_reply=true when the latest message is explicitly aimed at ${assistantName}. -- should_reply=true when the latest message is a natural follow-up to ${assistantName}'s prior question, answer, analysis, plan, or requested next step. +- route=reply when the latest message is explicitly aimed at ${assistantName}. +- route=reply when the latest message is a natural follow-up to ${assistantName}'s prior question, answer, analysis, plan, or requested next step. - Natural follow-ups include questions, corrections, added constraints, choices, requests to act, and topic-specific continuations, even without words like "you" or "${assistantName}". -- Terse continuations such as "which one?", "why?", "what about auth?", "is that the right approach?", or "can you check on this?" can be should_reply=true when they continue ${assistantName}'s immediately preceding contribution. -- Attachment-only messages can be should_reply=true when the thread context makes them a continuation of work ${assistantName} was doing. -- should_reply=false for acknowledgments, reactions, status chatter, and team coordination that do not ask ${assistantName} to continue. -- should_reply=false when the latest message is plainly human-to-human, even if it discusses the same project. +- Terse continuations such as "which one?", "why?", "what about auth?", "is that the right approach?", or "can you check on this?" can be route=reply when they continue ${assistantName}'s immediately preceding contribution. +- Attachment-only messages can be route=reply when the thread context makes them a continuation of work ${assistantName} was doing. +- route=stay_quiet for acknowledgments, reactions, status chatter, and team coordination that do not ask ${assistantName} to continue. +- route=stay_quiet when the latest message is plainly human-to-human, even if it discusses the same project. # Unsubscribe Policy -- should_unsubscribe=true when the latest message clearly tells ${assistantName} to stop or leave the thread. -- If should_unsubscribe=true, should_reply must be false. +- route=unsubscribe when the latest message clearly tells ${assistantName} to stop or leave the thread. - Ordinary disagreement, correction, or "not yet" is not an unsubscribe. # Decision Policy -- Prefer should_reply=true for clear continuations of ${assistantName}'s work. -- Prefer should_reply=false for clear side conversation. -- Use low confidence only for genuinely ambiguous floor ownership. Do not lower confidence merely because the assistant was addressed implicitly. -- Keep the reason to one concise sentence under 160 characters grounded in floor ownership or continuity.`; +- Prefer route=reply for clear continuations of ${assistantName}'s work. +- Prefer route=stay_quiet for clear side conversation. +- Spread probability across routes only for genuinely ambiguous floor ownership. Do not lower the reply probability merely because the assistant was addressed implicitly.`; +} + +/** Build the decision question for one passive subscribed-thread message. */ +export function buildSubscribedReplyRouterQuestions(assistantName: string) { + return { + route: { + type: "choice", + instructions: buildSubscribedReplyRouterPolicy(assistantName), + criteria: { + reply: `The latest message is aimed at ${assistantName}. It addresses ${assistantName}, or it continues ${assistantName}'s latest answer, question, or plan and no human-to-human exchange started after it.`, + stay_quiet: `The latest message is aimed at another person or at the room. It addresses someone else by name or @mention, continues a human-to-human exchange, only shares topic words with earlier ${assistantName} messages, or is an acknowledgment or status chatter.`, + unsubscribe: `The latest message clearly tells ${assistantName} to stop or leave the thread.`, + } satisfies Record, + }, + } as const; } + +/** Decision questions sent by the passive subscribed-thread reply router. */ +export type SubscribedReplyRouterQuestions = ReturnType< + typeof buildSubscribedReplyRouterQuestions +>; diff --git a/packages/junior/tests/fixtures/conversation-work.ts b/packages/junior/tests/fixtures/conversation-work.ts index 8cdfe2ec66..2e00e975fa 100644 --- a/packages/junior/tests/fixtures/conversation-work.ts +++ b/packages/junior/tests/fixtures/conversation-work.ts @@ -462,13 +462,8 @@ export async function createConversationWorkSlackHarness( services: { agentRunner, subscribedReplyPolicy: { - completeObject: async ({ schema }) => ({ - object: schema.parse({ - should_reply: false, - should_unsubscribe: false, - confidence: 1, - reason: "side_conversation", - }), + completeDecision: async () => ({ + answers: { route: { choice: "stay_quiet" } }, }), }, visionContext: { diff --git a/packages/junior/tests/fixtures/turn-router-model.ts b/packages/junior/tests/fixtures/turn-router-model.ts index 5312bbdf29..c8e19e481c 100644 --- a/packages/junior/tests/fixtures/turn-router-model.ts +++ b/packages/junior/tests/fixtures/turn-router-model.ts @@ -4,7 +4,7 @@ import { mswServer } from "../msw/server"; /** Return a fixed route decision from the model service. */ export function mockTurnRouterModel(): void { mswServer.use( - http.post("https://ai-gateway.vercel.sh/v3/ai/language-model", () => + http.post("https://ai-gateway.vercel.sh/v4/ai/language-model", () => HttpResponse.json({ content: [ { diff --git a/packages/junior/tests/integration/slack/watched-message-behavior.test.ts b/packages/junior/tests/integration/slack/watched-message-behavior.test.ts index a22de9832b..905627ca5e 100644 --- a/packages/junior/tests/integration/slack/watched-message-behavior.test.ts +++ b/packages/junior/tests/integration/slack/watched-message-behavior.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { TurnInputCommitLostError } from "@/chat/runtime/turn"; import type { JuniorRuntimeServiceOverrides } from "@/chat/app/services"; +import type { SubscribedReplyRoute } from "@/chat/services/subscribed-reply-router-policy"; import { createProviderError } from "@/chat/services/provider-error"; import type { AgentRun } from "@/chat/agent/types"; import { createTestChatRuntime } from "../../fixtures/chat-runtime"; @@ -52,6 +53,14 @@ function toPostedText(value: unknown): string { return String(value); } +function routeDecision(route: SubscribedReplyRoute, probability = 1) { + return { + answers: { + route: { choice: route, probabilities: { [route]: probability } }, + }, + }; +} + function replyForRun(selectText: (run: AgentRun) => string) { return createModelAgentRunnerForRun((run) => createModelStream([{ type: "text", text: selectText(run) }]), @@ -65,16 +74,9 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createTestChatRuntime({ services: { subscribedReplyPolicy: { - completeObject: async (params: { prompt?: unknown }) => { - classifierCalls.push(String(params.prompt)); - return { - object: { - should_reply: false, - confidence: 0, - reason: "side conversation", - }, - text: '{"should_reply":false,"confidence":0,"reason":"side conversation"}', - } as never; + completeDecision: async (params: { state: string }) => { + classifierCalls.push(params.state); + return routeDecision("stay_quiet"); }, }, agentRunner: neverRunAgentRunner(), @@ -108,7 +110,7 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createTestChatRuntime({ services: { subscribedReplyPolicy: { - completeObject: async () => { + completeDecision: async () => { throw providerError; }, }, @@ -142,17 +144,9 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createTestChatRuntime({ services: { subscribedReplyPolicy: { - completeObject: async (params: { prompt?: unknown }) => { - classifierCalls.push(String(params.prompt)); - return { - object: { - should_reply: true, - should_unsubscribe: false, - confidence: 1, - reason: "explicit ask", - }, - text: '{"should_reply":true,"should_unsubscribe":false,"confidence":1,"reason":"explicit ask"}', - } as never; + completeDecision: async (params: { state: string }) => { + classifierCalls.push(params.state); + return routeDecision("reply"); }, }, agentRunner: replyForRun((run) => { @@ -190,16 +184,9 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createTestChatRuntime({ services: { subscribedReplyPolicy: { - completeObject: async () => { + completeDecision: async () => { classifierCalled = true; - return { - object: { - should_reply: true, - confidence: 1, - reason: "direct mention asking junior for status", - }, - text: '{"should_reply":true,"confidence":1,"reason":"direct mention asking junior for status"}', - } as never; + return routeDecision("reply"); }, }, agentRunner: replyForRun((run) => { @@ -237,7 +224,7 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createTestChatRuntime({ services: { subscribedReplyPolicy: { - completeObject: async () => { + completeDecision: async () => { classifierCalled = true; throw new Error( "classifier should be bypassed for queued explicit mentions", @@ -308,18 +295,9 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createTestChatRuntime({ services: { subscribedReplyPolicy: { - completeObject: async () => { + completeDecision: async () => { classifierCalled = true; - return { - object: { - should_reply: false, - should_unsubscribe: true, - confidence: 1, - reason: - "user explicitly asked junior to stop participating in the thread", - }, - text: '{"should_reply":false,"should_unsubscribe":true,"confidence":1,"reason":"user explicitly asked junior to stop participating in the thread"}', - } as never; + return routeDecision("unsubscribe"); }, }, agentRunner: replyForRun((run) => { @@ -422,7 +400,7 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createRuntime({ services: { subscribedReplyPolicy: { - completeObject: async () => { + completeDecision: async () => { classifierCalled = true; throw new Error( "classifier should be bypassed for acknowledgments", @@ -458,16 +436,9 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createRuntime({ services: { subscribedReplyPolicy: { - completeObject: async () => { + completeDecision: async () => { classifierCalled = true; - return { - object: { - should_reply: false, - confidence: 0.95, - reason: "attachment acknowledgment", - }, - text: '{"should_reply":false,"confidence":0.95,"reason":"attachment acknowledgment"}', - } as never; + return routeDecision("stay_quiet", 0.95); }, }, agentRunner: neverRunAgentRunner(), @@ -505,16 +476,9 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createRuntime({ services: { subscribedReplyPolicy: { - completeObject: async () => { + completeDecision: async () => { classifierCalled = true; - return { - object: { - should_reply: false, - confidence: 0.95, - reason: "passive attachment", - }, - text: '{"should_reply":false,"confidence":0.95,"reason":"passive attachment"}', - } as never; + return routeDecision("stay_quiet", 0.95); }, }, agentRunner: neverRunAgentRunner(), @@ -552,18 +516,11 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createRuntime({ services: { subscribedReplyPolicy: { - completeObject: async (args) => { + completeDecision: async (args) => { classifierCalled = true; - expect(args.prompt).toContain("Deploy failed"); - expect(args.prompt).toContain("Service: checkout"); - return { - object: { - should_reply: false, - confidence: 0.95, - reason: "passive legacy attachment", - }, - text: '{"should_reply":false,"confidence":0.95,"reason":"passive legacy attachment"}', - } as never; + expect(args.state).toContain("Deploy failed"); + expect(args.state).toContain("Service: checkout"); + return routeDecision("stay_quiet", 0.95); }, }, agentRunner: neverRunAgentRunner(), @@ -607,16 +564,9 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createRuntime({ services: { subscribedReplyPolicy: { - completeObject: async () => { + completeDecision: async () => { classifierCalled = true; - return { - object: { - should_reply: false, - confidence: 0.95, - reason: "human side conversation", - }, - text: '{"should_reply":false,"confidence":0.95,"reason":"human side conversation"}', - } as never; + return routeDecision("stay_quiet", 0.95); }, }, agentRunner: replyForRun((run) => { @@ -665,16 +615,9 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createRuntime({ services: { subscribedReplyPolicy: { - completeObject: async () => { + completeDecision: async () => { classifierCalled = true; - return { - object: { - should_reply: false, - confidence: 0.95, - reason: "attachment follow-up", - }, - text: '{"should_reply":false,"confidence":0.95,"reason":"attachment follow-up"}', - } as never; + return routeDecision("stay_quiet", 0.95); }, }, agentRunner: replyForRun((run) => { @@ -730,7 +673,7 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createRuntime({ services: { subscribedReplyPolicy: { - completeObject: async () => { + completeDecision: async () => { classifierCalled = true; throw new Error( "classifier should be bypassed for messages addressed to another bot", @@ -790,16 +733,9 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createRuntime({ services: { subscribedReplyPolicy: { - completeObject: async () => { + completeDecision: async () => { classifierCalled = true; - return { - object: { - should_reply: true, - confidence: 0.95, - reason: "follow-up directed at assistant's previous response", - }, - text: '{"should_reply":true,"confidence":0.95,"reason":"follow-up directed at assistant\'s previous response"}', - } as never; + return routeDecision("reply", 0.95); }, }, agentRunner: replyForRun((run) => { @@ -849,16 +785,9 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createRuntime({ services: { subscribedReplyPolicy: { - completeObject: async () => { + completeDecision: async () => { classifierCalled = true; - return { - object: { - should_reply: true, - confidence: 0.65, - reason: "brief clarification after assistant reply", - }, - text: '{"should_reply":true,"confidence":0.65,"reason":"brief clarification after assistant reply"}', - } as never; + return routeDecision("reply", 0.65); }, }, agentRunner: replyForRun((run) => { @@ -970,15 +899,7 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createRuntime({ services: { subscribedReplyPolicy: { - completeObject: async () => - ({ - object: { - should_reply: false, - confidence: 0.9, - reason: "side conversation", - }, - text: '{"should_reply":false,"confidence":0.9,"reason":"side conversation"}', - }) as never, + completeDecision: async () => routeDecision("stay_quiet", 0.9), }, }, }); @@ -1007,16 +928,7 @@ describe("Slack behavior: subscribed messages", () => { const { slackRuntime } = createRuntime({ services: { subscribedReplyPolicy: { - completeObject: async () => - ({ - object: { - should_reply: false, - should_unsubscribe: true, - confidence: 1, - reason: "explicit stop", - }, - text: '{"should_reply":false,"should_unsubscribe":true,"confidence":1,"reason":"explicit stop"}', - }) as never, + completeDecision: async () => routeDecision("unsubscribe"), }, }, }); diff --git a/packages/junior/tests/unit/routing/subscribed-decision.test.ts b/packages/junior/tests/unit/routing/subscribed-decision.test.ts index 1db091f6aa..7ee0e8c976 100644 --- a/packages/junior/tests/unit/routing/subscribed-decision.test.ts +++ b/packages/junior/tests/unit/routing/subscribed-decision.test.ts @@ -1,4 +1,3 @@ -import { strictProviderSchemaProblems } from "@sentry/junior-testing/structured-output"; import { afterEach, describe, expect, it, vi } from "vitest"; import { setExperimentalFeatures } from "@/chat/experimental"; import { @@ -21,25 +20,14 @@ function makeInput( }; } -function classify( - object: { - should_reply: boolean; - should_unsubscribe: boolean; - confidence: number; - reason: string; - }, - input = makeInput(), - completeObject: Parameters< - typeof decideSubscribedThreadReply - >[0]["completeObject"] = vi.fn(async () => ({ costUsd: 0.00023, object })), -) { - return decideSubscribedThreadReply({ - botUserName: "junior", - modelId: "router-model", - input, - completeObject, - logClassifierFailure: vi.fn(), - }); +type CompleteDecision = Parameters< + typeof decideSubscribedThreadReply +>[0]["completeDecision"]; + +function routeAnswer(choice: string, probability: number) { + return { + answers: { route: { choice, probabilities: { [choice]: probability } } }, + }; } describe("subscribed reply decision", () => { @@ -52,7 +40,7 @@ describe("subscribed reply decision", () => { it("skips non-mention replies when passive-routing is off", async () => { setExperimentalFeatures(undefined); - const completeObject = vi.fn(); + const completeDecision = vi.fn(); await expect( decideSubscribedThreadReply({ @@ -63,7 +51,7 @@ describe("subscribed reply decision", () => { text: "what did you just say?", isExplicitMention: false, }), - completeObject, + completeDecision, logClassifierFailure: vi.fn(), }), ).resolves.toEqual({ @@ -71,12 +59,12 @@ describe("subscribed reply decision", () => { reason: SubscribedReplyReason.PassiveDisabled, reasonDetail: "passive-routing", }); - expect(completeObject).not.toHaveBeenCalled(); + expect(completeDecision).not.toHaveBeenCalled(); }); it("still replies to explicit mentions when passive routing is off", async () => { setExperimentalFeatures(undefined); - const completeObject = vi.fn(); + const completeDecision = vi.fn(); await expect( decideSubscribedThreadReply({ @@ -87,14 +75,14 @@ describe("subscribed reply decision", () => { text: "please continue", isExplicitMention: true, }), - completeObject, + completeDecision, logClassifierFailure: vi.fn(), }), ).resolves.toEqual({ shouldReply: true, reason: SubscribedReplyReason.ExplicitMention, }); - expect(completeObject).not.toHaveBeenCalled(); + expect(completeDecision).not.toHaveBeenCalled(); }); it.each([ @@ -105,7 +93,7 @@ describe("subscribed reply decision", () => { "ok enough for now !stop thanks", "<@U0APP> keep going unless I say !stop later", ])("unsubscribes for %s without the classifier", async (text) => { - const completeObject = vi.fn(); + const completeDecision = vi.fn(); await expect( decideSubscribedThreadReply({ @@ -116,7 +104,7 @@ describe("subscribed reply decision", () => { text, isExplicitMention: text.startsWith("<@U0APP>"), }), - completeObject, + completeDecision, logClassifierFailure: vi.fn(), }), ).resolves.toEqual({ @@ -125,7 +113,7 @@ describe("subscribed reply decision", () => { reason: SubscribedReplyReason.ThreadOptOut, reasonDetail: "!stop", }); - expect(completeObject).not.toHaveBeenCalled(); + expect(completeDecision).not.toHaveBeenCalled(); }); it.each([ @@ -146,7 +134,7 @@ describe("subscribed reply decision", () => { isExplicitMention: false, }, ])("unsubscribes when the whole message is $name", async (fixture) => { - const completeObject = vi.fn(); + const completeDecision = vi.fn(); await expect( decideSubscribedThreadReply({ @@ -157,7 +145,7 @@ describe("subscribed reply decision", () => { text: fixture.text, isExplicitMention: fixture.isExplicitMention ?? true, }), - completeObject, + completeDecision, logClassifierFailure: vi.fn(), }), ).resolves.toEqual({ @@ -166,7 +154,7 @@ describe("subscribed reply decision", () => { reason: SubscribedReplyReason.ThreadOptOut, reasonDetail: "stop", }); - expect(completeObject).not.toHaveBeenCalled(); + expect(completeDecision).not.toHaveBeenCalled(); }); it.each([ @@ -176,7 +164,7 @@ describe("subscribed reply decision", () => { "stop spamming", "please continue with the PR checks", ])("does not treat %s as stop", async (text) => { - const completeObject = vi.fn(); + const completeDecision = vi.fn(); await expect( decideSubscribedThreadReply({ @@ -187,14 +175,14 @@ describe("subscribed reply decision", () => { text, isExplicitMention: true, }), - completeObject, + completeDecision, logClassifierFailure: vi.fn(), }), ).resolves.toEqual({ shouldReply: true, reason: SubscribedReplyReason.ExplicitMention, }); - expect(completeObject).not.toHaveBeenCalled(); + expect(completeDecision).not.toHaveBeenCalled(); }); it.each([ @@ -241,89 +229,68 @@ describe("subscribed reply decision", () => { it.each([ { - name: "a negative decision", - object: { - should_reply: false, - should_unsubscribe: false, - confidence: 0.95, - reason: "status chatter", - }, + name: "a stay-quiet decision", + choice: "stay_quiet", + probability: 0.95, expected: { costUsd: 0.00023, shouldReply: false, reason: SubscribedReplyReason.SideConversation, - reasonDetail: "status chatter", + reasonDetail: "stay_quiet p=0.95", }, }, { name: "a confident unsubscribe", - object: { - should_reply: false, - should_unsubscribe: true, - confidence: 0.95, - reason: "stop participating", - }, + choice: "unsubscribe", + probability: 0.95, expected: { costUsd: 0.00023, shouldReply: false, shouldUnsubscribe: true, reason: SubscribedReplyReason.ThreadOptOut, - reasonDetail: "stop participating", + reasonDetail: "unsubscribe p=0.95", }, }, { name: "a low-confidence reply", - object: { - should_reply: true, - should_unsubscribe: false, - confidence: 0.65, - reason: "maybe follow-up", - }, + choice: "reply", + probability: 0.65, expected: { costUsd: 0.00023, shouldReply: false, reason: SubscribedReplyReason.LowConfidence, - reasonDetail: "0.65: maybe follow-up", + reasonDetail: "reply p=0.65", }, }, { name: "a confident reply", - object: { - should_reply: true, - should_unsubscribe: false, - confidence: 0.95, - reason: "direct question", - }, + choice: "reply", + probability: 0.95, expected: { costUsd: 0.00023, shouldReply: true, reason: SubscribedReplyReason.Classifier, - reasonDetail: "direct question", + reasonDetail: "reply p=0.95", }, }, ])("maps $name onto the runtime decision contract", async (fixture) => { - const completeObject = vi.fn< - Parameters[0]["completeObject"] - >(async () => ({ costUsd: 0.00023, object: fixture.object })); - await expect( - classify(fixture.object, makeInput(), completeObject), + decideSubscribedThreadReply({ + botUserName: "junior", + modelId: "router-model", + input: makeInput(), + completeDecision: vi.fn(async () => ({ + ...routeAnswer(fixture.choice, fixture.probability), + costUsd: 0.00023, + })), + logClassifierFailure: vi.fn(), + }), ).resolves.toEqual(fixture.expected); - expect( - strictProviderSchemaProblems(completeObject.mock.calls[0]![0].schema), - ).toEqual([]); }); it("projects guardian-style user/assistant evidence without tool lines", async () => { - const completeObject = vi.fn( - async (_request: { prompt: string; system: string }) => ({ - object: { - should_reply: true, - should_unsubscribe: false, - confidence: 0.72, - reason: "natural continuation", - }, - }), + const completeDecision = vi.fn(async () => + routeAnswer("reply", 0.72), ); await expect( @@ -345,45 +312,44 @@ describe("subscribed reply decision", () => { "", ].join("\n"), }), - completeObject, + completeDecision, logClassifierFailure: vi.fn(), }), ).resolves.toEqual({ shouldReply: true, reason: SubscribedReplyReason.Classifier, - reasonDetail: "natural continuation", + reasonDetail: "reply p=0.72", }); - expect(completeObject).toHaveBeenCalledWith( + expect(completeDecision).toHaveBeenCalledWith( expect.objectContaining({ - maxTokens: 400, + modelId: "router-model", promptName: "junior.passive_reply_route", - prompt: expect.stringContaining(">>> TRANSCRIPT START"), - system: expect.stringContaining("# Evidence Handling"), + state: expect.stringContaining(">>> TRANSCRIPT START"), }), ); - const call = completeObject.mock.calls[0]?.[0]; - expect(call?.prompt).toContain( - "user David: investigate the passive router", - ); - expect(call?.prompt).toContain( + const call = completeDecision.mock.calls[0]?.[0]; + expect(call?.state).toContain("user David: investigate the passive router"); + expect(call?.state).toContain( "assistant junior: the confidence gate looks too strict", ); - expect(call?.prompt).toContain(">>> LATEST MESSAGE START"); - expect(call?.prompt).not.toContain("must-not-reach-router"); - expect(call?.system).toContain("Conversation Floor"); - expect(call?.system).toContain("untrusted evidence"); + expect(call?.state).toContain(">>> LATEST MESSAGE START"); + expect(call?.state).not.toContain("must-not-reach-router"); + const question = call?.questions.route; + expect(question?.type).toBe("choice"); + expect(Object.keys(question?.criteria ?? {})).toEqual([ + "reply", + "stay_quiet", + "unsubscribe", + ]); + expect(question?.instructions).toContain("Conversation Floor"); + expect(question?.instructions).toContain("untrusted evidence"); }); it("keeps first and recent user anchors with recent assistant evidence", async () => { - const completeObject = vi.fn(async (_request: { prompt: string }) => ({ - object: { - should_reply: false, - should_unsubscribe: false, - confidence: 0.9, - reason: "human side conversation", - }, - })); + const completeDecision = vi.fn(async () => + routeAnswer("stay_quiet", 0.9), + ); const middle = Array.from({ length: 12 }, (_, index) => { const filler = "x".repeat(8_000); return `[user] Person ${index}: ${index}-${filler}`; @@ -400,20 +366,20 @@ describe("subscribed reply decision", () => { "[user] David: latest request", ].join("\n"), }), - completeObject, + completeDecision, logClassifierFailure: vi.fn(), }); - const prompt = completeObject.mock.calls[0]?.[0].prompt; - expect(prompt).toBeDefined(); - expect(prompt).toContain("user David: original request"); - expect(prompt).toContain("assistant junior: latest answer"); - expect(prompt).toContain("user David: latest request"); - expect(prompt).toContain("Omitted earlier transcript entries:"); - expect(prompt).not.toContain("user Person 0:"); + const state = completeDecision.mock.calls[0]?.[0].state; + expect(state).toBeDefined(); + expect(state).toContain("user David: original request"); + expect(state).toContain("assistant junior: latest answer"); + expect(state).toContain("user David: latest request"); + expect(state).toContain("Omitted earlier transcript entries:"); + expect(state).not.toContain("user Person 0:"); }); - it("fails closed when the classifier result is invalid", async () => { + it("fails closed when the decision model returns an unknown route", async () => { const logClassifierFailure = vi.fn(); const input = makeInput(); @@ -422,9 +388,9 @@ describe("subscribed reply decision", () => { botUserName: "junior", modelId: "router-model", input, - completeObject: vi.fn(async () => ({ - object: { should_reply: "yes" }, - })), + completeDecision: vi.fn(async () => + routeAnswer("maybe", 1), + ), logClassifierFailure, }), ).resolves.toEqual({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb545f8cf1..b35721286c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,7 +39,7 @@ overrides: '@earendil-works/pi-agent-core>@earendil-works/pi-ai': 0.85.1 '@astrojs/compiler-binding-wasm32-wasi>@napi-rs/wasm-runtime': 1.2.0 '@bruits/satteri-wasm32-wasi>@napi-rs/wasm-runtime': 1.2.0 - ai: 6.0.190 + ai: 7.0.153 astro>vite: 8.0.14 '@swc/core': 1.15.33 linkify-it: 5.0.0 @@ -188,17 +188,17 @@ importers: specifier: 1.3.0 version: 1.3.0(zod@4.5.4) '@ai-sdk/gateway': - specifier: ^3.0.119 - version: 3.0.119(zod@4.5.4) + specifier: ^4.0.126 + version: 4.0.126(zod@4.5.4) '@chat-adapter/slack': specifier: 4.29.0 - version: 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + version: 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) '@chat-adapter/state-memory': specifier: 4.29.0 - version: 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + version: 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) '@chat-adapter/state-redis': specifier: 4.29.0 - version: 4.29.0(@opentelemetry/api@1.9.1)(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + version: 4.29.0(@opentelemetry/api@1.9.1)(ai@7.0.153(zod@4.5.4))(zod@4.5.4) '@earendil-works/pi-agent-core': specifier: 'catalog:' version: 0.84.1(@modelcontextprotocol/sdk@1.31.0(zod@4.5.4))(zod@4.5.4) @@ -254,14 +254,14 @@ importers: specifier: 2.8.0 version: 2.8.0 ai: - specifier: 6.0.190 - version: 6.0.190(zod@4.5.4) + specifier: 7.0.153 + version: 7.0.153(zod@4.5.4) better-auth: specifier: ^1.6.22 version: 1.6.22(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(pg@8.21.0))(pg@8.21.0)(vitest@4.1.7) chat: specifier: 4.29.0 - version: 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + version: 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) commander: specifier: ^14.0.3 version: 14.0.3 @@ -457,7 +457,7 @@ importers: devDependencies: '@chat-adapter/slack': specifier: 4.29.0 - version: 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + version: 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) '@earendil-works/pi-ai': specifier: 'catalog:' version: 0.85.1(zod@4.5.4) @@ -483,11 +483,11 @@ importers: specifier: workspace:* version: file:packages/junior-testing ai: - specifier: 6.0.190 - version: 6.0.190(zod@4.5.4) + specifier: 7.0.153 + version: 7.0.153(zod@4.5.4) chat: specifier: 4.29.0 - version: 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + version: 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) hono: specifier: ^4.12.27 version: 4.12.27 @@ -508,7 +508,7 @@ importers: version: 4.1.7(msw@2.14.6(typescript@7.0.2))(tsx@4.22.3) vitest-evals: specifier: 0.16.1 - version: 0.16.1(ai@6.0.190(zod@4.5.4))(tinyrainbow@3.1.0)(vitest@4.1.7(msw@2.14.6(typescript@7.0.2))(tsx@4.22.3))(zod@4.5.4) + version: 0.16.1(ai@7.0.153(zod@4.5.4))(tinyrainbow@3.1.0)(vitest@4.1.7(msw@2.14.6(typescript@7.0.2))(tsx@4.22.3))(zod@4.5.4) zod: specifier: 'catalog:' version: 4.5.4 @@ -787,21 +787,21 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 - '@ai-sdk/gateway@3.0.119': - resolution: {integrity: sha512-VAhfRWC+JexZakkVfmjaJKaTj00x7/UHdE8kMWL3NhuQAlf8oXtg9r4dfvFZrByXxchGRBvYE3biEUyibkg0xg==} - engines: {node: '>=18'} + '@ai-sdk/gateway@4.0.126': + resolution: {integrity: sha512-gOrvnfrWxslTws5puMe+CvEDjZ0rxbpQYLZgdngQl1g/z3mRc5sllcflVAhy2xJSZacrWtsXwua+NJoc/IKZDw==} + engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@4.0.27': - resolution: {integrity: sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw==} - engines: {node: '>=18'} + '@ai-sdk/provider-utils@5.0.68': + resolution: {integrity: sha512-SASuC1DqSdWoYyYfpxbgPKm9VCnMP1VTahWY7WEYzi2QDmFdbw4runqmH7NKC33iB/INo+XbaN2hH3dXHSNQZw==} + engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider@3.0.10': - resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==} - engines: {node: '>=18'} + '@ai-sdk/provider@4.0.34': + resolution: {integrity: sha512-Ip7ZQBq/FiGpM1mmOVseTtX9PBjnzo0bLC41WTgBSBBRUBz6LhNFGbYaNUzI6hPm6ll1oNSPOAZc/q/z+3QZEQ==} + engines: {node: '>=22'} '@andrewbranch/untar.js@1.0.4': resolution: {integrity: sha512-pVXSwPsLuw8IGLo2Di0EaOfsk+ntVvpkk942J/sHYIkwvtKUakEcPh7HBgZ6tuimgzKSEHgCvO4XgQ05DEbwDw==} @@ -4642,6 +4642,9 @@ packages: '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + '@workflow/serde@4.1.0': + resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==} + '@workflow/serde@4.1.0-beta.2': resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==} @@ -4758,9 +4761,9 @@ packages: agent-browser@0.27.0: resolution: {integrity: sha512-mmHzVsYFVA6nshNNGJzg83aVMgKpf4h98ytY3pvtJB1Cot0ZyA2bfnkbSngGD56Azkj+GlhVH6qx9DfKOVE0yg==} - ai@6.0.190: - resolution: {integrity: sha512-T+ixHbWZ6jmHRREpVVJTkFyWJeCekCdzLPan7lp1F32jG5OUw4+odlVYjtMRXVzogU+pWzpMmXdRiHUmdL/q0w==} - engines: {node: '>=18'} + ai@7.0.153: + resolution: {integrity: sha512-Ib8v374jWn9FXsjADXodRaP7fsMutlmPsbTLT5gYBDyadFcF9XxfYX1CSSxgSvE1r06MmOeYVwvQjFb3CcBi1Q==} + engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -5115,7 +5118,7 @@ packages: resolution: {integrity: sha512-KdPfzaie5ivYytyRICTERg5xT+LeCbYefokvNAqTHe92eqkFaoTMXXkSitikxJVWhZIb2YoXF1b9UZHyzSzKzw==} engines: {node: '>=20'} peerDependencies: - ai: 6.0.190 + ai: 7.0.153 zod: ^3.0.0 || ^4.0.0 peerDependenciesMeta: ai: @@ -8822,7 +8825,7 @@ packages: resolution: {integrity: sha512-MuPVetClAOn55ewgajxGxZcQDFWUftrQ19/rZQn8zXvaxHri+ElgEb5W0OIMRQwSvF5TBYIsE5rWdQB/qrWJkg==} hasBin: true peerDependencies: - ai: 6.0.190 + ai: 7.0.153 tinyrainbow: '>=2 <4' vitest: '>=4 <5' zod: '>=3 <5' @@ -9152,21 +9155,23 @@ snapshots: dependencies: zod: 4.5.4 - '@ai-sdk/gateway@3.0.119(zod@4.5.4)': + '@ai-sdk/gateway@4.0.126(zod@4.5.4)': dependencies: - '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@4.5.4) + '@ai-sdk/provider': 4.0.34 + '@ai-sdk/provider-utils': 5.0.68(zod@4.5.4) '@vercel/oidc': 3.2.0 zod: 4.5.4 - '@ai-sdk/provider-utils@4.0.27(zod@4.5.4)': + '@ai-sdk/provider-utils@5.0.68(zod@4.5.4)': dependencies: - '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider': 4.0.34 '@standard-schema/spec': 1.1.0 + '@workflow/serde': 4.1.0 eventsource-parser: 3.0.8 + undici: 7.29.0 zod: 4.5.4 - '@ai-sdk/provider@3.0.10': + '@ai-sdk/provider@4.0.34': dependencies: json-schema: 0.4.0 @@ -9927,20 +9932,20 @@ snapshots: dependencies: fontkitten: 1.0.3 - '@chat-adapter/shared@4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4)': + '@chat-adapter/shared@4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4)': dependencies: - chat: 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + chat: 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) transitivePeerDependencies: - ai - supports-color - zod - '@chat-adapter/slack@4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4)': + '@chat-adapter/slack@4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4)': dependencies: - '@chat-adapter/shared': 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + '@chat-adapter/shared': 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) '@slack/socket-mode': 2.0.7 '@slack/web-api': 7.16.0 - chat: 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + chat: 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) transitivePeerDependencies: - ai - bufferutil @@ -9949,17 +9954,17 @@ snapshots: - utf-8-validate - zod - '@chat-adapter/state-memory@4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4)': + '@chat-adapter/state-memory@4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4)': dependencies: - chat: 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + chat: 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) transitivePeerDependencies: - ai - supports-color - zod - '@chat-adapter/state-redis@4.29.0(@opentelemetry/api@1.9.1)(ai@6.0.190(zod@4.5.4))(zod@4.5.4)': + '@chat-adapter/state-redis@4.29.0(@opentelemetry/api@1.9.1)(ai@7.0.153(zod@4.5.4))(zod@4.5.4)': dependencies: - chat: 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + chat: 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) redis: 5.12.1(@opentelemetry/api@1.9.1) transitivePeerDependencies: - '@node-rs/xxhash' @@ -12313,10 +12318,10 @@ snapshots: '@sentry/junior@file:packages/junior(@types/node@25.9.1)': dependencies: '@agentclientprotocol/sdk': 1.3.0(zod@4.5.4) - '@ai-sdk/gateway': 3.0.119(zod@4.5.4) - '@chat-adapter/slack': 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) - '@chat-adapter/state-memory': 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) - '@chat-adapter/state-redis': 4.29.0(@opentelemetry/api@1.9.1)(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + '@ai-sdk/gateway': 4.0.126(zod@4.5.4) + '@chat-adapter/slack': 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) + '@chat-adapter/state-memory': 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) + '@chat-adapter/state-redis': 4.29.0(@opentelemetry/api@1.9.1)(ai@7.0.153(zod@4.5.4))(zod@4.5.4) '@earendil-works/pi-agent-core': 0.84.1(@modelcontextprotocol/sdk@1.31.0(zod@4.5.4))(zod@4.5.4) '@earendil-works/pi-ai': 0.84.1(@modelcontextprotocol/sdk@1.31.0(zod@4.5.4))(zod@4.5.4) '@hono/zod-validator': 0.9.0(hono@4.12.27)(zod@4.5.4) @@ -12335,9 +12340,9 @@ snapshots: '@vercel/oidc': 3.4.1 '@vercel/queue': 0.2.0 '@vercel/sandbox': 2.8.0 - ai: 6.0.190(zod@4.5.4) + ai: 7.0.153(zod@4.5.4) better-auth: 1.6.22(@opentelemetry/api@1.9.1)(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(pg@8.21.0))(pg@8.21.0) - chat: 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + chat: 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) commander: 14.0.3 diff: 8.0.4 drizzle-orm: 0.45.2(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(pg@8.21.0) @@ -12407,10 +12412,10 @@ snapshots: '@sentry/junior@file:packages/junior(@types/node@25.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@agentclientprotocol/sdk': 1.3.0(zod@4.5.4) - '@ai-sdk/gateway': 3.0.119(zod@4.5.4) - '@chat-adapter/slack': 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) - '@chat-adapter/state-memory': 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) - '@chat-adapter/state-redis': 4.29.0(@opentelemetry/api@1.9.1)(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + '@ai-sdk/gateway': 4.0.126(zod@4.5.4) + '@chat-adapter/slack': 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) + '@chat-adapter/state-memory': 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) + '@chat-adapter/state-redis': 4.29.0(@opentelemetry/api@1.9.1)(ai@7.0.153(zod@4.5.4))(zod@4.5.4) '@earendil-works/pi-agent-core': 0.84.1(@modelcontextprotocol/sdk@1.31.0(zod@4.5.4))(zod@4.5.4) '@earendil-works/pi-ai': 0.84.1(@modelcontextprotocol/sdk@1.31.0(zod@4.5.4))(zod@4.5.4) '@hono/zod-validator': 0.9.0(hono@4.12.27)(zod@4.5.4) @@ -12429,9 +12434,9 @@ snapshots: '@vercel/oidc': 3.4.1 '@vercel/queue': 0.2.0 '@vercel/sandbox': 2.8.0 - ai: 6.0.190(zod@4.5.4) + ai: 7.0.153(zod@4.5.4) better-auth: 1.6.22(@opentelemetry/api@1.9.1)(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(pg@8.21.0))(pg@8.21.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - chat: 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + chat: 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) commander: 14.0.3 diff: 8.0.4 drizzle-orm: 0.45.2(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(pg@8.21.0) @@ -12501,10 +12506,10 @@ snapshots: '@sentry/junior@file:packages/junior(@types/node@25.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.7)': dependencies: '@agentclientprotocol/sdk': 1.3.0(zod@4.5.4) - '@ai-sdk/gateway': 3.0.119(zod@4.5.4) - '@chat-adapter/slack': 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) - '@chat-adapter/state-memory': 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) - '@chat-adapter/state-redis': 4.29.0(@opentelemetry/api@1.9.1)(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + '@ai-sdk/gateway': 4.0.126(zod@4.5.4) + '@chat-adapter/slack': 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) + '@chat-adapter/state-memory': 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) + '@chat-adapter/state-redis': 4.29.0(@opentelemetry/api@1.9.1)(ai@7.0.153(zod@4.5.4))(zod@4.5.4) '@earendil-works/pi-agent-core': 0.84.1(@modelcontextprotocol/sdk@1.31.0(zod@4.5.4))(zod@4.5.4) '@earendil-works/pi-ai': 0.84.1(@modelcontextprotocol/sdk@1.31.0(zod@4.5.4))(zod@4.5.4) '@hono/zod-validator': 0.9.0(hono@4.12.27)(zod@4.5.4) @@ -12523,9 +12528,9 @@ snapshots: '@vercel/oidc': 3.4.1 '@vercel/queue': 0.2.0 '@vercel/sandbox': 2.8.0 - ai: 6.0.190(zod@4.5.4) + ai: 7.0.153(zod@4.5.4) better-auth: 1.6.22(@opentelemetry/api@1.9.1)(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(pg@8.21.0))(pg@8.21.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.7) - chat: 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + chat: 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) commander: 14.0.3 diff: 8.0.4 drizzle-orm: 0.45.2(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(pg@8.21.0) @@ -12595,10 +12600,10 @@ snapshots: '@sentry/junior@file:packages/junior(vitest@4.1.7(msw@2.14.6(typescript@7.0.2))(tsx@4.22.3))': dependencies: '@agentclientprotocol/sdk': 1.3.0(zod@4.5.4) - '@ai-sdk/gateway': 3.0.119(zod@4.5.4) - '@chat-adapter/slack': 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) - '@chat-adapter/state-memory': 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) - '@chat-adapter/state-redis': 4.29.0(@opentelemetry/api@1.9.1)(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + '@ai-sdk/gateway': 4.0.126(zod@4.5.4) + '@chat-adapter/slack': 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) + '@chat-adapter/state-memory': 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) + '@chat-adapter/state-redis': 4.29.0(@opentelemetry/api@1.9.1)(ai@7.0.153(zod@4.5.4))(zod@4.5.4) '@earendil-works/pi-agent-core': 0.84.1(@modelcontextprotocol/sdk@1.31.0(zod@4.5.4))(zod@4.5.4) '@earendil-works/pi-ai': 0.84.1(@modelcontextprotocol/sdk@1.31.0(zod@4.5.4))(zod@4.5.4) '@hono/zod-validator': 0.9.0(hono@4.12.27)(zod@4.5.4) @@ -12617,9 +12622,9 @@ snapshots: '@vercel/oidc': 3.4.1 '@vercel/queue': 0.2.0 '@vercel/sandbox': 2.8.0 - ai: 6.0.190(zod@4.5.4) + ai: 7.0.153(zod@4.5.4) better-auth: 1.6.22(@opentelemetry/api@1.9.1)(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(pg@8.21.0))(pg@8.21.0)(vitest@4.1.7(msw@2.14.6(typescript@7.0.2))(tsx@4.22.3)) - chat: 4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4) + chat: 4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4) commander: 14.0.3 diff: 8.0.4 drizzle-orm: 0.45.2(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(pg@8.21.0) @@ -13721,6 +13726,8 @@ snapshots: '@vscode/l10n@0.0.18': {} + '@workflow/serde@4.1.0': {} + '@workflow/serde@4.1.0-beta.2': {} '@yuku-core/binding-android-arm64@0.19.0': @@ -13798,12 +13805,11 @@ snapshots: agent-browser@0.27.0: {} - ai@6.0.190(zod@4.5.4): + ai@7.0.153(zod@4.5.4): dependencies: - '@ai-sdk/gateway': 3.0.119(zod@4.5.4) - '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@4.5.4) - '@opentelemetry/api': 1.9.1 + '@ai-sdk/gateway': 4.0.126(zod@4.5.4) + '@ai-sdk/provider': 4.0.34 + '@ai-sdk/provider-utils': 5.0.68(zod@4.5.4) zod: 4.5.4 ajv-draft-04@1.0.0(ajv@8.20.0): @@ -14289,7 +14295,7 @@ snapshots: character-reference-invalid@2.0.1: {} - chat@4.29.0(ai@6.0.190(zod@4.5.4))(zod@4.5.4): + chat@4.29.0(ai@7.0.153(zod@4.5.4))(zod@4.5.4): dependencies: '@workflow/serde': 4.1.0-beta.2 mdast-util-to-string: 4.0.0 @@ -14299,7 +14305,7 @@ snapshots: remend: 1.3.0 unified: 11.0.5 optionalDependencies: - ai: 6.0.190(zod@4.5.4) + ai: 7.0.153(zod@4.5.4) zod: 4.5.4 transitivePeerDependencies: - supports-color @@ -18757,14 +18763,14 @@ snapshots: optionalDependencies: vite: 8.0.14(esbuild@0.28.1)(tsx@4.22.3) - vitest-evals@0.16.1(ai@6.0.190(zod@4.5.4))(tinyrainbow@3.1.0)(vitest@4.1.7(msw@2.14.6(typescript@7.0.2))(tsx@4.22.3))(zod@4.5.4): + vitest-evals@0.16.1(ai@7.0.153(zod@4.5.4))(tinyrainbow@3.1.0)(vitest@4.1.7(msw@2.14.6(typescript@7.0.2))(tsx@4.22.3))(zod@4.5.4): dependencies: '@vitest-evals/core': 0.16.1 '@vitest-evals/report-ui': 0.16.1 tinyrainbow: 3.1.0 vitest: 4.1.7(msw@2.14.6(typescript@7.0.2))(tsx@4.22.3) optionalDependencies: - ai: 6.0.190(zod@4.5.4) + ai: 7.0.153(zod@4.5.4) zod: 4.5.4 vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(jiti@2.7.0)(msw@2.14.6(@types/node@25.9.1)(typescript@7.0.2))(tsx@4.22.3)(yaml@2.9.0): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 052e75b821..2b58e49fc0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -18,7 +18,7 @@ overrides: "@earendil-works/pi-agent-core>@earendil-works/pi-ai": 0.85.1 "@astrojs/compiler-binding-wasm32-wasi>@napi-rs/wasm-runtime": 1.2.0 "@bruits/satteri-wasm32-wasi>@napi-rs/wasm-runtime": 1.2.0 - ai: 6.0.190 + ai: 7.0.153 "astro>vite": 8.0.14 "@swc/core": 1.15.33 linkify-it: 5.0.0 diff --git a/scripts/check-test-architecture.mjs b/scripts/check-test-architecture.mjs index 409e4108f1..5a23f548d6 100644 --- a/scripts/check-test-architecture.mjs +++ b/scripts/check-test-architecture.mjs @@ -108,7 +108,7 @@ const AGENT_TEST_RULES = [ message: "tests must not fake the model; run the real agent through the agent test fixture", pattern: - /\bvi\.(?:doMock|mock)\(\s*["'](?:@earendil-works\/pi-agent-core|@\/chat\/pi\/client)["']|\bclass\s+MockAgent\b|\b(?:createFauxCore|fauxAssistantMessage)\b|\b(?:completeObject|completeText)\s*:|\bhttp\.\w+\(\s*["'`]https:\/\/ai-gateway\.vercel\.sh/g, + /\bvi\.(?:doMock|mock)\(\s*["'](?:@earendil-works\/pi-agent-core|@\/chat\/pi\/client)["']|\bclass\s+MockAgent\b|\b(?:createFauxCore|fauxAssistantMessage)\b|\b(?:completeDecision|completeObject|completeText)\s*:|\bhttp\.\w+\(\s*["'`]https:\/\/ai-gateway\.vercel\.sh/g, }, { id: "runtime-config-mutation", diff --git a/scripts/file-length-exceptions.mjs b/scripts/file-length-exceptions.mjs index 341e79eb9e..3987b1d8ba 100644 --- a/scripts/file-length-exceptions.mjs +++ b/scripts/file-length-exceptions.mjs @@ -56,8 +56,6 @@ export const fileLengthExceptions = { "Existing broad sandbox egress suite; split by behavior.", "packages/junior/tests/integration/slack-schedule-tools.test.ts": "Existing broad Slack scheduler suite; split by tool.", - "packages/junior/tests/integration/slack/watched-message-behavior.test.ts": - "Existing broad watched-message suite; split by behavior.", "packages/junior/tests/unit/api/conversation-events.test.ts": "Existing broad conversation events suite; split by behavior.", "packages/junior/tests/unit/plugins/agent-hooks.test.ts":