diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7716b99d..2780b2acd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,7 @@ jobs: - run: pnpm release:check - run: pnpm skills:check - run: pnpm lint + - run: pnpm typecheck - run: pnpm test:ci - run: pnpm test:e2e:dashboard - run: pnpm --filter @sentry/junior-example build diff --git a/package.json b/package.json index c67d2092a..fabb3c34f 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "test:watch": "pnpm --filter @sentry/junior test:watch", "evals": "pnpm --filter @sentry/junior-evals evals", "evals:record": "pnpm --filter @sentry/junior-evals evals:record", - "typecheck": "pnpm --filter @sentry/junior-plugin-api typecheck && pnpm --filter @sentry/junior-scheduler typecheck && pnpm --filter @sentry/junior-memory typecheck && pnpm --filter @sentry/junior-github typecheck && pnpm --filter @sentry/junior typecheck && pnpm --filter @sentry/junior-dashboard typecheck && pnpm --filter @sentry/junior-testing typecheck && pnpm --filter @sentry/junior-example typecheck", + "typecheck": "pnpm -r run typecheck", "skills:check": "pnpm --filter @sentry/junior skills:check", "test:ci": "pnpm --filter @sentry/junior build && pnpm --filter @sentry/junior-memory build && pnpm --filter @sentry/junior-github build && pnpm --filter @sentry/junior-dashboard build && pnpm --filter @sentry/junior test:coverage && pnpm --filter @sentry/junior-memory test && pnpm --filter @sentry/junior-github test && pnpm --filter @sentry/junior-dashboard test:coverage" }, diff --git a/packages/junior-evals/package.json b/packages/junior-evals/package.json index 58b6a1ec1..e9cd3fd99 100644 --- a/packages/junior-evals/package.json +++ b/packages/junior-evals/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "test": "vitest run", + "typecheck": "tsc --noEmit", "evals": "vitest run -c vitest.evals.config.ts", "evals:record": "VITEST_EVALS_REPLAY_MODE=record vitest run -c vitest.evals.config.ts" }, diff --git a/packages/junior-evals/src/behavior-harness.ts b/packages/junior-evals/src/behavior-harness.ts index 58897ca70..3caff39b2 100644 --- a/packages/junior-evals/src/behavior-harness.ts +++ b/packages/junior-evals/src/behavior-harness.ts @@ -41,6 +41,8 @@ import { } from "@/chat/plugins/task-runner"; import type { PluginTaskQueueMessage } from "@/chat/plugins/task-message"; import { generateAssistantReply } from "@/chat/respond"; +import { completedAgentRun } from "@/chat/runtime/agent-run-outcome"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import { resumeAwaitingSlackContinuation } from "@/chat/runtime/agent-continue-runner"; import { scheduleAgentContinue } from "@/chat/services/agent-continue"; import { @@ -1485,113 +1487,115 @@ function buildRuntimeServices( } : {}), replyExecutor: { - generateAssistantReply: async (text, context) => { - replyCallCount += 1; - const mockImageGeneration = scenario.overrides?.mock_image_generation; - if (scenario.overrides?.fail_reply_call === replyCallCount) { - throw new Error(`forced reply failure on call ${replyCallCount}`); - } - const replyResult = replyResults[replyCallCount - 1]; - if (replyResult) { - if (replyResult.stream_text) { - await context?.onTextDelta?.(replyResult.stream_text); + agentRunner: { + run: async (text, context) => { + replyCallCount += 1; + const mockImageGeneration = scenario.overrides?.mock_image_generation; + if (scenario.overrides?.fail_reply_call === replyCallCount) { + throw new Error(`forced reply failure on call ${replyCallCount}`); + } + const replyResult = replyResults[replyCallCount - 1]; + if (replyResult) { + if (replyResult.stream_text) { + await context?.onTextDelta?.(replyResult.stream_text); + } + replyState.successfulCount += 1; + observations.toolInvocations.push( + ...(replyResult.tool_invocations ?? + (replyResult.tool_calls ?? []).map((tool) => ({ tool }))), + ); + return completedAgentRun({ + text: replyResult.text, + deliveryMode: "thread", + deliveryPlan: { + mode: "thread", + postThreadText: true, + attachFiles: "none", + }, + diagnostics: { + assistantMessageCount: replyResult.assistant_message_count ?? 1, + ...(replyResult.error_message + ? { errorMessage: replyResult.error_message } + : {}), + modelId: "eval-reply-result", + outcome: replyResult.outcome ?? "success", + ...(replyResult.stop_reason + ? { stopReason: replyResult.stop_reason } + : {}), + toolCalls: replyResult.tool_calls ?? [], + toolErrorCount: replyResult.tool_error_count ?? 0, + toolResultCount: replyResult.tool_result_count ?? 0, + usedPrimaryText: replyResult.used_primary_text ?? true, + }, + }); + } + const replyText = replyTexts[replyState.successfulCount]; + if (typeof replyText === "string") { + replyState.successfulCount += 1; + return completedAgentRun({ + text: replyText, + deliveryMode: "thread", + deliveryPlan: { + mode: "thread", + postThreadText: true, + attachFiles: "none", + }, + diagnostics: { + assistantMessageCount: 1, + modelId: "eval-reply-text", + outcome: "success", + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); } - replyState.successfulCount += 1; - observations.toolInvocations.push( - ...(replyResult.tool_invocations ?? - (replyResult.tool_calls ?? []).map((tool) => ({ tool }))), - ); - return { - text: replyResult.text, - deliveryMode: "thread", - deliveryPlan: { - mode: "thread", - postThreadText: true, - attachFiles: "none", - }, - diagnostics: { - assistantMessageCount: replyResult.assistant_message_count ?? 1, - ...(replyResult.error_message - ? { errorMessage: replyResult.error_message } - : {}), - modelId: "eval-reply-result", - outcome: replyResult.outcome ?? "success", - ...(replyResult.stop_reason - ? { stopReason: replyResult.stop_reason } - : {}), - toolCalls: replyResult.tool_calls ?? [], - toolErrorCount: replyResult.tool_error_count ?? 0, - toolResultCount: replyResult.tool_result_count ?? 0, - usedPrimaryText: replyResult.used_primary_text ?? true, - }, - }; - } - const replyText = replyTexts[replyState.successfulCount]; - if (typeof replyText === "string") { - replyState.successfulCount += 1; - return { - text: replyText, - deliveryMode: "thread", - deliveryPlan: { - mode: "thread", - postThreadText: true, - attachFiles: "none", - }, - diagnostics: { - assistantMessageCount: 1, - modelId: "eval-reply-text", - outcome: "success", - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }; - } - const gatewaySnapshot = snapshotEnv([ - "AI_GATEWAY_API_KEY", - "VERCEL_OIDC_TOKEN", - ]); - const baseToolOverrides: ToolHooks["toolOverrides"] = { - ...(context?.toolOverrides ?? {}), - }; - const toolOverrides = { - ...baseToolOverrides, - webFetch: createReplayWebFetchDeps(baseToolOverrides), - webSearch: createReplayWebSearchDeps(baseToolOverrides), - ...(mockImageGeneration - ? { imageGenerate: createMockImageGenerateDeps() } - : {}), - }; - if (scenario.overrides?.unset_gateway_api_key) { - delete process.env.AI_GATEWAY_API_KEY; - delete process.env.VERCEL_OIDC_TOKEN; - } - try { - const reply = await generateAssistantReply(text, { - ...context, - turnDeadlineAtMs: Math.min( - context?.turnDeadlineAtMs ?? Number.POSITIVE_INFINITY, - Date.now() + replyTimeoutMs, - ), - onToolInvocation: (invocation) => { - observations.toolInvocations.push( - toEvalToolInvocation(invocation), - ); - }, - ...(env.configuredSkillDirs.length > 0 - ? { skillDirs: env.configuredSkillDirs } + const gatewaySnapshot = snapshotEnv([ + "AI_GATEWAY_API_KEY", + "VERCEL_OIDC_TOKEN", + ]); + const baseToolOverrides: ToolHooks["toolOverrides"] = { + ...(context?.toolOverrides ?? {}), + }; + const toolOverrides = { + ...baseToolOverrides, + webFetch: createReplayWebFetchDeps(baseToolOverrides), + webSearch: createReplayWebSearchDeps(baseToolOverrides), + ...(mockImageGeneration + ? { imageGenerate: createMockImageGenerateDeps() } : {}), - toolOverrides, - }); - replyState.successfulCount += 1; - return reply; - } finally { + }; if (scenario.overrides?.unset_gateway_api_key) { - gatewaySnapshot.restore(); + delete process.env.AI_GATEWAY_API_KEY; + delete process.env.VERCEL_OIDC_TOKEN; } - } + try { + const outcome = await generateAssistantReply(text, { + ...context, + turnDeadlineAtMs: Math.min( + context?.turnDeadlineAtMs ?? Number.POSITIVE_INFINITY, + Date.now() + replyTimeoutMs, + ), + onToolInvocation: (invocation) => { + observations.toolInvocations.push( + toEvalToolInvocation(invocation), + ); + }, + ...(env.configuredSkillDirs.length > 0 + ? { skillDirs: env.configuredSkillDirs } + : {}), + toolOverrides, + }); + replyState.successfulCount += 1; + return outcome; + } finally { + if (scenario.overrides?.unset_gateway_api_key) { + gatewaySnapshot.restore(); + } + } + }, }, scheduleAgentContinue: async (request) => { await scheduleAgentContinue(request, { @@ -1637,7 +1641,7 @@ function buildRuntimeServices( async function processEvents(args: { scenario: EvalScenario; env: HarnessEnvironment; - generateAssistantReply: typeof generateAssistantReply; + agentRunner: AgentRunner; getSlackAdapter: () => FakeSlackAdapter; conversationWorkQueue: ConversationWorkQueueTestAdapter; slackRuntime: ReturnType; @@ -1647,7 +1651,7 @@ async function processEvents(args: { const { scenario, env, - generateAssistantReply, + agentRunner, getSlackAdapter, conversationWorkQueue, slackRuntime, @@ -1713,7 +1717,7 @@ async function processEvents(args: { getSlackAdapter: () => getSlackAdapter() as unknown as SlackAdapter, resumeAwaitingContinuation: async (conversationId) => await resumeAwaitingSlackContinuation(conversationId, { - generateReply: generateAssistantReply, + agentRunner, scheduleAgentContinue: async (request) => { await scheduleAgentContinue(request, { queue: conversationWorkQueue, @@ -1869,7 +1873,7 @@ async function processEvents(args: { if (!callback) { throw new Error("Scheduled eval dispatch callback was not captured."); } - await runAgentDispatchSlice(callback, { generateAssistantReply }); + await runAgentDispatchSlice(callback, { agentRunner }); } }; @@ -2024,10 +2028,9 @@ export async function runEvalScenario( observations, conversationWorkQueue, ); - const generateEvalAssistantReply = - services.replyExecutor?.generateAssistantReply; - if (!generateEvalAssistantReply) { - throw new Error("Eval reply executor was not configured."); + const evalAgentRunner = services.replyExecutor?.agentRunner; + if (!evalAgentRunner) { + throw new Error("Eval agent runner was not configured."); } const slackRuntime = createSlackRuntime({ @@ -2038,7 +2041,7 @@ export async function runEvalScenario( await processEvents({ scenario, env, - generateAssistantReply: generateEvalAssistantReply, + agentRunner: evalAgentRunner, getSlackAdapter: () => slackAdapter, conversationWorkQueue, slackRuntime, diff --git a/packages/junior/src/app.ts b/packages/junior/src/app.ts index 30daadf9b..225cf2b12 100644 --- a/packages/junior/src/app.ts +++ b/packages/junior/src/app.ts @@ -63,7 +63,7 @@ import { createProductionConversationWorkOptions, createProductionSlackWebhookServices, } from "@/chat/app/production"; -import { withSandboxTracePropagation } from "@/chat/app/services"; +import { createAgentRunner } from "@/chat/runtime/agent-runner"; import type { WaitUntilFn } from "@/handlers/types"; export { defineJuniorPlugins } from "./plugins"; @@ -584,18 +584,17 @@ export async function createApp(options?: JuniorAppOptions): Promise { } const waitUntil = options?.waitUntil ?? (await defaultWaitUntil()); + const tracePropagation = { domains: sandboxEgressTracePropagationDomains }; + const agentRunner = createAgentRunner(generateAssistantReply, { + tracePropagation, + }); const runtimeServiceOverrides = { - sandbox: { - tracePropagation: { domains: sandboxEgressTracePropagationDomains }, - }, + replyExecutor: { agentRunner }, + sandbox: { tracePropagation }, }; const slackWebhookServices = createProductionSlackWebhookServices({ services: runtimeServiceOverrides, }); - const generateReplyWithTracePropagation = withSandboxTracePropagation( - generateAssistantReply, - runtimeServiceOverrides.sandbox.tracePropagation, - ); const app = new Hono(); @@ -629,20 +628,18 @@ export async function createApp(options?: JuniorAppOptions): Promise { // because Hono matches routes top-down and `:provider` would swallow `mcp/`. app.get("/api/oauth/callback/mcp/:provider", (c) => { return mcpOauthCallbackGET(c.req.raw, c.req.param("provider"), waitUntil, { - generateReply: generateReplyWithTracePropagation, + agentRunner, }); }); app.get("/api/oauth/callback/:provider", (c) => { return oauthCallbackGET(c.req.raw, c.req.param("provider"), waitUntil, { - generateReply: generateReplyWithTracePropagation, + agentRunner, }); }); app.post("/api/internal/agent-dispatch", (c) => { - return agentDispatchPOST(c.req.raw, waitUntil, { - tracePropagation: { domains: sandboxEgressTracePropagationDomains }, - }); + return agentDispatchPOST(c.req.raw, waitUntil, { agentRunner }); }); let agentContinuePOST: @@ -658,6 +655,7 @@ export async function createApp(options?: JuniorAppOptions): Promise { conversationWorkOptions ??= options?.conversationWork ?? createProductionConversationWorkOptions({ + agentRunner, services: runtimeServiceOverrides, }); return conversationWorkOptions; diff --git a/packages/junior/src/chat/agent-dispatch/runner.ts b/packages/junior/src/chat/agent-dispatch/runner.ts index 15b7e6198..58dd25285 100644 --- a/packages/junior/src/chat/agent-dispatch/runner.ts +++ b/packages/junior/src/chat/agent-dispatch/runner.ts @@ -7,13 +7,8 @@ * state, and schedules follow-up slices when a turn needs to continue. */ import { botConfig } from "@/chat/config"; -import { - generateAssistantReply as generateAssistantReplyImpl, - type AssistantReply, - type AssistantReplyRequestContext, -} from "@/chat/respond"; -import type { AgentRunOutcome } from "@/chat/runtime/agent-run-outcome"; -import type { SandboxEgressTracePropagationConfig } from "@/chat/sandbox/egress/tracing"; +import type { AssistantReply } from "@/chat/respond"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import { logException } from "@/chat/logging"; import { buildConversationContext, @@ -64,13 +59,9 @@ import type { DispatchCallback, DispatchRecord } from "./types"; const DISPATCH_SLICE_LEASE_MS = 5 * 60 * 1000; export interface AgentDispatchRunnerDeps { - generateAssistantReply?: ( - messageText: string, - context: AssistantReplyRequestContext, - ) => Promise; + agentRunner: AgentRunner; scheduleCallback?: typeof scheduleDispatchCallback; scheduleSessionCompletedPluginTasks?: typeof scheduleSessionCompletedPluginTasks; - tracePropagation?: SandboxEgressTracePropagationConfig; } function getUserMessageId(dispatch: DispatchRecord): string { @@ -176,10 +167,8 @@ function canClaimDispatch(record: DispatchRecord, nowMs: number): boolean { /** Run one serverless slice for a core-owned agent dispatch. */ export async function runAgentDispatchSlice( callback: DispatchCallback, - deps: AgentDispatchRunnerDeps = {}, + deps: AgentDispatchRunnerDeps, ): Promise { - const generateAssistantReply = - deps.generateAssistantReply ?? generateAssistantReplyImpl; const scheduleCallback = deps.scheduleCallback ?? scheduleDispatchCallback; const scheduleCompletedTasks = deps.scheduleSessionCompletedPluginTasks ?? @@ -300,7 +289,7 @@ export async function runAgentDispatchSlice( excludeMessageId: userMessageId, }); - const outcome = await generateAssistantReply(dispatch.input, { + const outcome = await deps.agentRunner.run(dispatch.input, { authorizationFlowMode: "disabled", credentialContext: { actor: dispatch.actor, @@ -333,7 +322,6 @@ export async function runAgentDispatchSlice( sandbox: { sandboxId, sandboxDependencyProfileHash, - tracePropagation: deps.tracePropagation, }, onSandboxAcquired: async (sandbox) => { sandboxId = sandbox.sandboxId; diff --git a/packages/junior/src/chat/app/production.ts b/packages/junior/src/chat/app/production.ts index 492435842..3d699b1e5 100644 --- a/packages/junior/src/chat/app/production.ts +++ b/packages/junior/src/chat/app/production.ts @@ -1,6 +1,6 @@ import type { SlackAdapter } from "@chat-adapter/slack"; import { createSlackRuntime } from "@/chat/app/factory"; -import { withSandboxTracePropagation } from "@/chat/app/services"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import { createUserTokenStore } from "@/chat/capabilities/factory"; import { getSlackBotToken, @@ -16,7 +16,6 @@ import { getVercelConversationWorkQueue } from "@/chat/task-execution/vercel-que import type { VercelConversationWorkCallbackOptions } from "@/chat/task-execution/vercel-callback"; import { resumeAwaitingSlackContinuation } from "@/chat/runtime/agent-continue-runner"; import type { JuniorRuntimeServiceOverrides } from "@/chat/app/services"; -import { generateAssistantReply } from "@/chat/respond"; import { getConversationStore } from "@/chat/db"; import type { ConversationStore } from "@/chat/conversations/store"; @@ -94,13 +93,24 @@ export function getProductionSlackWebhookServices(): SlackWebhookServices { } /** Return the production queue callback options for conversation work. */ -export function createProductionConversationWorkOptions(options?: { +export function createProductionConversationWorkOptions(options: { + agentRunner: AgentRunner; services?: JuniorRuntimeServiceOverrides; }): VercelConversationWorkCallbackOptions { const conversationStore = getProductionConversationStore(); + const { agentRunner } = options; + // The explicit runner is authoritative for both the reply runtime and the + // resume path, so a caller cannot run them on divergent runners. + const services: JuniorRuntimeServiceOverrides = { + ...options.services, + replyExecutor: { + ...options.services?.replyExecutor, + agentRunner, + }, + }; const runtime = createSlackRuntime({ getSlackAdapter: getProductionSlackAdapter, - services: options?.services, + services, }); return { conversationStore, @@ -110,13 +120,9 @@ export function createProductionConversationWorkOptions(options?: { conversationStore, resumeAwaitingContinuation: async (conversationId) => await resumeAwaitingSlackContinuation(conversationId, { - generateReply: withSandboxTracePropagation( - generateAssistantReply, - options?.services?.sandbox?.tracePropagation, - ), + agentRunner, scheduleSessionCompletedPluginTasks: - options?.services?.replyExecutor - ?.scheduleSessionCompletedPluginTasks, + services.replyExecutor?.scheduleSessionCompletedPluginTasks, }), runtime, }), diff --git a/packages/junior/src/chat/app/services.ts b/packages/junior/src/chat/app/services.ts index 1a64b530c..6de641c9f 100644 --- a/packages/junior/src/chat/app/services.ts +++ b/packages/junior/src/chat/app/services.ts @@ -1,8 +1,5 @@ import { completeObject, completeText } from "@/chat/pi/client"; -import { - generateAssistantReply as generateAssistantReplyImpl, - type AssistantReplyRequestContext, -} from "@/chat/respond"; +import { generateAssistantReply as generateAssistantReplyImpl } from "@/chat/respond"; import type { SandboxEgressTracePropagationConfig } from "@/chat/sandbox/egress/tracing"; import { getAwaitingAgentContinueRequest, @@ -33,6 +30,7 @@ import { type VisionContextDeps, type VisionContextService, } from "@/chat/services/vision-context"; +import { createAgentRunner } from "@/chat/runtime/agent-runner"; export interface JuniorRuntimeServices { conversationMemory: ConversationMemoryService; @@ -53,22 +51,6 @@ export interface JuniorRuntimeServiceOverrides { visionContext?: Partial; } -/** Apply app-owned sandbox egress trace config unless a turn overrides it. */ -export function withSandboxTracePropagation( - generateReply: typeof generateAssistantReplyImpl, - tracePropagation?: SandboxEgressTracePropagationConfig, -): typeof generateAssistantReplyImpl { - return async (messageText: string, context: AssistantReplyRequestContext) => - await generateReply(messageText, { - ...context, - sandbox: { - ...context?.sandbox, - tracePropagation: - context?.sandbox?.tracePropagation ?? tracePropagation, - }, - }); -} - export function createJuniorRuntimeServices( overrides: JuniorRuntimeServiceOverrides = {}, ): JuniorRuntimeServices { @@ -94,12 +76,11 @@ export function createJuniorRuntimeServices( replyExecutor: { contextCompactor: overrides.replyExecutor?.contextCompactor ?? contextCompactor, - generateAssistantReply: - overrides.replyExecutor?.generateAssistantReply ?? - withSandboxTracePropagation( - generateAssistantReplyImpl, - overrides.sandbox?.tracePropagation, - ), + agentRunner: + overrides.replyExecutor?.agentRunner ?? + createAgentRunner(generateAssistantReplyImpl, { + tracePropagation: overrides.sandbox?.tracePropagation, + }), getAwaitingAgentContinueRequest: overrides.replyExecutor?.getAwaitingAgentContinueRequest ?? getAwaitingAgentContinueRequest, diff --git a/packages/junior/src/chat/local/runner.ts b/packages/junior/src/chat/local/runner.ts index a88eba903..dcf2667a1 100644 --- a/packages/junior/src/chat/local/runner.ts +++ b/packages/junior/src/chat/local/runner.ts @@ -6,12 +6,8 @@ * a local destination, and only commits assistant delivery after the CLI sink * accepts the final output. */ -import { - generateAssistantReply as generateAssistantReplyImpl, - type AssistantReply, - type AssistantReplyRequestContext, -} from "@/chat/respond"; -import type { AgentRunOutcome } from "@/chat/runtime/agent-run-outcome"; +import type { AssistantReply } from "@/chat/respond"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import { createLocalSource, localDestinationSchema, @@ -68,11 +64,8 @@ export interface LocalToolInvocation { export type LocalToolResult = ToolExecutionReport; export interface LocalAgentTurnDeps { + agentRunner: AgentRunner; deliverReply: (reply: LocalAgentReply) => Promise; - generateAssistantReply?: ( - messageText: string, - context: AssistantReplyRequestContext, - ) => Promise; now?: () => number; onStatus?: (status: string) => void | Promise; onTextDelta?: (deltaText: string) => void | Promise; @@ -183,8 +176,6 @@ export async function runLocalAgentTurn( const destination = localDestination(input.conversationId); const source = createLocalSource(destination.conversationId); - const generateAssistantReply = - deps.generateAssistantReply ?? generateAssistantReplyImpl; const now = deps.now ?? (() => Date.now()); const persisted = await getPersistedThreadState(input.conversationId); const conversation = coerceThreadConversationState(persisted); @@ -232,7 +223,7 @@ export async function runLocalAgentTurn( fallback: conversation.piMessages, }); piMessagesBeforeRun = piMessages; - const outcome = await generateAssistantReply(text, { + const outcome = await deps.agentRunner.run(text, { authorizationFlowMode: "disabled", conversationContext: buildConversationContext(conversation, { excludeMessageId: userMessageId, diff --git a/packages/junior/src/chat/runtime/agent-continue-runner.ts b/packages/junior/src/chat/runtime/agent-continue-runner.ts index 73943b588..762efc18c 100644 --- a/packages/junior/src/chat/runtime/agent-continue-runner.ts +++ b/packages/junior/src/chat/runtime/agent-continue-runner.ts @@ -60,7 +60,8 @@ import { type SlackRequester, } from "@/chat/requester"; import { getConversationWorkState } from "@/chat/task-execution/store"; -import type { AssistantReply, generateAssistantReply } from "@/chat/respond"; +import type { AssistantReply } from "@/chat/respond"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import { persistAuthPauseTurnState } from "@/chat/runtime/auth-pause-state"; import { applyPendingAuthUpdate, @@ -72,7 +73,7 @@ const AGENT_CONTINUE_LOCK_RETRY_DELAYS_MS = [250, 1_000, 2_000] as const; /** Runtime ports for agent continuation scheduling. */ export interface AgentContinueRunnerOptions { - generateReply?: typeof generateAssistantReply; + agentRunner: AgentRunner; resumeTurn?: typeof resumeSlackTurn; scheduleAgentContinue?: (request: AgentContinueRequest) => Promise; scheduleSessionCompletedPluginTasks?: (params: { @@ -267,7 +268,7 @@ async function failUnresumableContinuation(args: { */ export async function continueSlackAgentRun( payload: AgentContinueRequest, - options: AgentContinueRunnerOptions = {}, + options: AgentContinueRunnerOptions, ): Promise { const thread = parseSlackThreadId(payload.conversationId); if (!thread) { @@ -284,7 +285,7 @@ export async function continueSlackAgentRun( channelId: thread.channelId, threadTs: thread.threadTs, lockKey: payload.conversationId, - generateReply: options.generateReply, + agentRunner: options.agentRunner, scheduleSessionCompletedPluginTasks: options.scheduleSessionCompletedPluginTasks, beforeStart: async () => { @@ -583,7 +584,7 @@ async function recoverStrandedRunningSession(args: { /** Resume the first valid paused Slack session for an idle conversation. */ export async function resumeAwaitingSlackContinuation( conversationId: string, - options: AgentContinueRunnerOptions = {}, + options: AgentContinueRunnerOptions, ): Promise { const summaries = await listAgentTurnSessionSummariesForConversation(conversationId); @@ -643,7 +644,7 @@ export async function resumeAwaitingSlackContinuation( */ export async function continueSlackAgentRunWithLockRetry( payload: AgentContinueRequest, - options: AgentContinueRunnerOptions = {}, + options: AgentContinueRunnerOptions, ): Promise { const scheduleAgentContinue = options.scheduleAgentContinue ?? defaultScheduleAgentContinue; diff --git a/packages/junior/src/chat/runtime/agent-runner.ts b/packages/junior/src/chat/runtime/agent-runner.ts new file mode 100644 index 000000000..b8baaf050 --- /dev/null +++ b/packages/junior/src/chat/runtime/agent-runner.ts @@ -0,0 +1,33 @@ +import type { ReplyRequestContext } from "@/chat/respond"; +import type { AgentRunOutcome } from "@/chat/runtime/agent-run-outcome"; +import type { SandboxEgressTracePropagationConfig } from "@/chat/sandbox/egress/tracing"; + +/** Run one agent-run slice behind runtime-owned orchestration boundaries. */ +export interface AgentRunner { + run( + messageText: string, + context: ReplyRequestContext, + ): Promise; +} + +/** Adapt the Pi-facing reply generator behind the runtime-owned runner seam. */ +export function createAgentRunner( + run: AgentRunner["run"], + options?: { tracePropagation?: SandboxEgressTracePropagationConfig }, +): AgentRunner { + const tracePropagation = options?.tracePropagation; + if (!tracePropagation) { + return { run }; + } + return { + run: async (messageText, context) => + await run(messageText, { + ...context, + sandbox: { + ...context.sandbox, + tracePropagation: + context.sandbox?.tracePropagation ?? tracePropagation, + }, + }), + }; +} diff --git a/packages/junior/src/chat/runtime/reply-executor.ts b/packages/junior/src/chat/runtime/reply-executor.ts index 1ffc03340..b3072421e 100644 --- a/packages/junior/src/chat/runtime/reply-executor.ts +++ b/packages/junior/src/chat/runtime/reply-executor.ts @@ -1,10 +1,10 @@ /** * Slack reply execution boundary. * - * This module bridges prepared Slack thread state into `generateAssistantReply` + * This module bridges prepared Slack thread state into the agent runner * and commits the resulting Slack-visible delivery/state updates. It is where * queued messages, compaction, status updates, and Slack posting meet; agent - * internals stay behind the reply generator. + * internals stay behind the runner seam. */ import { THREAD_STATE_TTL_MS } from "chat"; import type { Message, SentMessage, Thread } from "chat"; @@ -31,10 +31,9 @@ import { buildSlackOutputMessage } from "@/chat/slack/output"; import { getSlackErrorObservabilityAttributes } from "@/chat/slack/errors"; import { buildSteeringPiMessage, - type AssistantReplyRequestContext, type ReplySteeringMessage, } from "@/chat/respond"; -import type { AgentRunOutcome } from "@/chat/runtime/agent-run-outcome"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import type { CredentialContext } from "@/chat/credentials/context"; import { shouldEmitDevAgentTrace } from "@/chat/runtime/dev-agent-trace"; import { @@ -277,11 +276,8 @@ async function loadPiMessagesForTurn(args: { } export interface ReplyExecutorServices { + agentRunner: AgentRunner; contextCompactor: ContextCompactor; - generateAssistantReply: ( - messageText: string, - context: AssistantReplyRequestContext, - ) => Promise; generateThreadTitle: ConversationMemoryService["generateThreadTitle"]; getAwaitingAgentContinueRequest: (args: { conversationId: string; @@ -1039,7 +1035,7 @@ export function createReplyToThread(deps: ReplyExecutorDeps) { ); } : undefined; - const outcome = await deps.services.generateAssistantReply( + const outcome = await deps.services.agentRunner.run( effectiveUserText, { credentialContext, diff --git a/packages/junior/src/chat/runtime/slack-resume.ts b/packages/junior/src/chat/runtime/slack-resume.ts index 6542d1776..2f8714f5b 100644 --- a/packages/junior/src/chat/runtime/slack-resume.ts +++ b/packages/junior/src/chat/runtime/slack-resume.ts @@ -8,11 +8,10 @@ import { botConfig } from "@/chat/config"; import type { ChannelConfigurationService } from "@/chat/configuration/types"; import { - generateAssistantReply, type AssistantReply, type AssistantReplyRequestContext, } from "@/chat/respond"; -import type { AgentRunOutcome } from "@/chat/runtime/agent-run-outcome"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import type { Source } from "@sentry/junior-plugin-api"; import { scheduleSessionCompletedPluginTasks } from "@/chat/plugins/task-runner"; import { @@ -63,11 +62,6 @@ function resolveReplyTimeoutMs(explicitTimeoutMs?: number): number | undefined { return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined; } -type ResumeGenerateReply = ( - messageText: string, - context: AssistantReplyRequestContext, -) => Promise; - async function postSlackMessageBestEffort( channelId: string, threadTs: string, @@ -144,7 +138,7 @@ interface ResumeSlackTurnArgs { replyContext?: ResumeReplyContext; lockKey?: string; initialText?: string; - generateReply?: ResumeGenerateReply; + agentRunner: AgentRunner; scheduleSessionCompletedPluginTasks?: (params: { conversationId: string; sessionId: string; @@ -374,9 +368,11 @@ export async function resumeSlackTurn( } status.start(); - const generateReply = runArgs.generateReply ?? generateAssistantReply; const replyContext = createResumeReplyContext(runArgs, status); - const replyPromise = generateReply(runArgs.messageText, replyContext); + const replyPromise = runArgs.agentRunner.run( + runArgs.messageText, + replyContext, + ); const replyTimeoutMs = resolveReplyTimeoutMs(runArgs.replyTimeoutMs); const outcome = typeof replyTimeoutMs === "number" @@ -598,7 +594,7 @@ export async function resumeAuthorizedRequest(args: { connectedText: string; replyContext?: ResumeReplyContext; lockKey?: string; - generateReply?: ResumeGenerateReply; + agentRunner: AgentRunner; onSuccess?: (reply: AssistantReply) => Promise; onFailure?: (error: unknown) => Promise; onAuthPause?: (pause: { providerDisplayName: string }) => Promise; @@ -615,7 +611,7 @@ export async function resumeAuthorizedRequest(args: { replyContext: args.replyContext, lockKey: args.lockKey, initialText: args.connectedText, - generateReply: args.generateReply, + agentRunner: args.agentRunner, onSuccess: args.onSuccess, onFailure: args.onFailure, onAuthPause: args.onAuthPause, diff --git a/packages/junior/src/cli/chat.ts b/packages/junior/src/cli/chat.ts index 3cf0382c6..8d824b73c 100644 --- a/packages/junior/src/cli/chat.ts +++ b/packages/junior/src/cli/chat.ts @@ -15,7 +15,13 @@ import * as readline from "node:readline/promises"; import { createJiti } from "jiti"; import { loadAppPluginSet } from "@/plugin-module"; import { normalizeLocalConversationId } from "@/chat/local/conversation"; -import type { LocalAgentReply, LocalToolResult } from "@/chat/local/runner"; +import type { + LocalAgentReply, + LocalAgentTurnDeps, + LocalToolResult, +} from "@/chat/local/runner"; +import { generateAssistantReply } from "@/chat/respond"; +import { createAgentRunner } from "@/chat/runtime/agent-runner"; import type { JuniorPluginSet } from "@/plugins"; export const CHAT_USAGE = "usage: junior chat\n junior chat -p "; @@ -230,32 +236,44 @@ function newRunConversationId(): string { return conversationId; } -async function runPrompt( - options: Extract, +/** Wire the shared local-turn setup so prompt and interactive runs stay identical. */ +async function prepareLocalChatRun( io: ChatIo, pluginSet: JuniorPluginSet | null | undefined, -): Promise { +) { defaultStateAdapterForLocalChat(); await configureLocalChatPlugins(pluginSet); - const conversationId = newRunConversationId(); - const { runLocalAgentTurn } = await import("@/chat/local/runner"); + const deps: LocalAgentTurnDeps = { + agentRunner: createAgentRunner(generateAssistantReply), + deliverReply: async (reply) => { + await deliverReply(io, reply); + }, + onStatus: async (status) => { + await reportStatus(io, status); + }, + onToolResult: async (result) => { + await reportToolResult(io, result); + }, + }; + return { conversationId: newRunConversationId(), runLocalAgentTurn, deps }; +} + +async function runPrompt( + options: Extract, + io: ChatIo, + pluginSet: JuniorPluginSet | null | undefined, +): Promise { + const { conversationId, runLocalAgentTurn, deps } = await prepareLocalChatRun( + io, + pluginSet, + ); const result = await runLocalAgentTurn( { conversationId, message: options.message, }, - { - deliverReply: async (reply) => { - await deliverReply(io, reply); - }, - onStatus: async (status) => { - await reportStatus(io, status); - }, - onToolResult: async (result) => { - await reportToolResult(io, result); - }, - }, + deps, ); return result.outcome === "success" ? 0 : 1; } @@ -264,11 +282,10 @@ async function runInteractive( io: ChatIo, pluginSet: JuniorPluginSet | null | undefined, ): Promise { - defaultStateAdapterForLocalChat(); - await configureLocalChatPlugins(pluginSet); - const conversationId = newRunConversationId(); - - const { runLocalAgentTurn } = await import("@/chat/local/runner"); + const { conversationId, runLocalAgentTurn, deps } = await prepareLocalChatRun( + io, + pluginSet, + ); const rl = readline.createInterface({ input: io.input, output: io.output, @@ -289,17 +306,7 @@ async function runInteractive( conversationId, message, }, - { - deliverReply: async (reply) => { - await deliverReply(io, reply); - }, - onStatus: async (status) => { - await reportStatus(io, status); - }, - onToolResult: async (result) => { - await reportToolResult(io, result); - }, - }, + deps, ); } catch (error) { if (error instanceof ChatOutputError) { diff --git a/packages/junior/src/handlers/agent-dispatch.ts b/packages/junior/src/handlers/agent-dispatch.ts index 33afe96a9..407bfbcc8 100644 --- a/packages/junior/src/handlers/agent-dispatch.ts +++ b/packages/junior/src/handlers/agent-dispatch.ts @@ -1,18 +1,18 @@ import { logException } from "@/chat/logging"; import { runAgentDispatchSlice } from "@/chat/agent-dispatch/runner"; import { verifyDispatchCallbackRequest } from "@/chat/agent-dispatch/signing"; -import type { SandboxEgressTracePropagationConfig } from "@/chat/sandbox/egress/tracing"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import type { WaitUntilFn } from "@/handlers/types"; interface AgentDispatchHandlerOptions { - tracePropagation?: SandboxEgressTracePropagationConfig; + agentRunner: AgentRunner; } /** Handle the authenticated internal agent-dispatch callback. */ export async function POST( request: Request, waitUntil: WaitUntilFn, - options: AgentDispatchHandlerOptions = {}, + options: AgentDispatchHandlerOptions, ): Promise { const payload = await verifyDispatchCallbackRequest(request); if (!payload) { @@ -21,7 +21,7 @@ export async function POST( waitUntil(() => runAgentDispatchSlice(payload, { - tracePropagation: options.tracePropagation, + agentRunner: options.agentRunner, }).catch((error) => { logException( error, diff --git a/packages/junior/src/handlers/mcp-oauth-callback.ts b/packages/junior/src/handlers/mcp-oauth-callback.ts index e8fd9b52e..7e9c8a09b 100644 --- a/packages/junior/src/handlers/mcp-oauth-callback.ts +++ b/packages/junior/src/handlers/mcp-oauth-callback.ts @@ -14,7 +14,8 @@ import { } from "@/chat/mcp/auth-store"; import { finalizeMcpAuthorization } from "@/chat/mcp/oauth"; import { logException, logWarn } from "@/chat/logging"; -import type { AssistantReply, generateAssistantReply } from "@/chat/respond"; +import type { AssistantReply } from "@/chat/respond"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import { getChannelConfigurationServiceById, getPersistedSandboxState, @@ -86,7 +87,7 @@ const CALLBACK_PAGES = { } as const; interface McpOAuthCallbackOptions { - generateReply?: typeof generateAssistantReply; + agentRunner: AgentRunner; } function mcpAuthorizationId(args: { @@ -185,10 +186,10 @@ async function persistFailedReplyState( async function resumeAuthorizedMcpTurn(args: { authSession: McpAuthSessionState; - generateReply?: typeof generateAssistantReply; + agentRunner: AgentRunner; provider: string; }): Promise { - const { authSession, generateReply, provider } = args; + const { authSession, agentRunner, provider } = args; if ( !authSession.channelId || !authSession.destination || @@ -238,7 +239,7 @@ async function resumeAuthorizedMcpTurn(args: { messageTs: getTurnUserSlackMessageTs(userMessage), lockKey: threadId, connectedText: "", - generateReply, + agentRunner, beforeStart: async () => { const lockedState = await getPersistedThreadState(threadId); const lockedConversation = coerceThreadConversationState(lockedState); @@ -448,7 +449,7 @@ export async function GET( request: Request, provider: string, waitUntil: WaitUntilFn, - options: McpOAuthCallbackOptions = {}, + options: McpOAuthCallbackOptions, ): Promise { const url = new URL(request.url); const state = url.searchParams.get("state")?.trim(); @@ -482,7 +483,7 @@ export async function GET( waitUntil(() => resumeAuthorizedMcpTurn({ authSession, - generateReply: options.generateReply, + agentRunner: options.agentRunner, provider, }), ); diff --git a/packages/junior/src/handlers/oauth-callback.ts b/packages/junior/src/handlers/oauth-callback.ts index 2e152a9fb..02128a792 100644 --- a/packages/junior/src/handlers/oauth-callback.ts +++ b/packages/junior/src/handlers/oauth-callback.ts @@ -62,11 +62,12 @@ import { import { escapeXml } from "@/chat/xml"; import type { WaitUntilFn } from "@/handlers/types"; import { scheduleAgentContinue } from "@/chat/services/agent-continue"; -import type { AssistantReply, generateAssistantReply } from "@/chat/respond"; +import type { AssistantReply } from "@/chat/respond"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import { requireSlackDestination } from "@/chat/destination"; interface OAuthCallbackOptions { - generateReply?: typeof generateAssistantReply; + agentRunner: AgentRunner; } /** @@ -261,7 +262,7 @@ async function resumeOAuthSessionRecordTurn( messageTs: getTurnUserSlackMessageTs(userMessage), lockKey: stored.resumeConversationId, initialText: "", - generateReply: options.generateReply, + agentRunner: options.agentRunner, beforeStart: async () => { const lockedState = await getPersistedThreadState( stored.resumeConversationId!, @@ -506,7 +507,7 @@ async function resumePendingOAuthMessage( threadTs: stored.threadTs, messageTs, connectedText: "", - generateReply: options.generateReply, + agentRunner: options.agentRunner, replyContext: { credentialContext: { actor: { type: "user", userId: stored.userId }, @@ -543,7 +544,7 @@ export async function GET( request: Request, provider: string, waitUntil: WaitUntilFn, - options: OAuthCallbackOptions = {}, + options: OAuthCallbackOptions, ): Promise { const providerConfig = pluginCatalogRuntime.getOAuthConfig(provider); if (!providerConfig) { diff --git a/packages/junior/tests/component/runtime/agent-continue-runner.test.ts b/packages/junior/tests/component/runtime/agent-continue-runner.test.ts index 65b241b9b..b9ce2d70a 100644 --- a/packages/junior/tests/component/runtime/agent-continue-runner.test.ts +++ b/packages/junior/tests/component/runtime/agent-continue-runner.test.ts @@ -6,6 +6,7 @@ import { getAgentTurnSessionRecord, upsertAgentTurnSessionRecord, } from "@/chat/state/turn-session"; +import { neverRunAgentRunner } from "../../fixtures/agent-runner"; import { SLACK_DESTINATION } from "../../fixtures/conversation-work"; const SLACK_SOURCE = createSlackSource({ @@ -31,6 +32,8 @@ function restoreEnv(name: string, value: string | undefined): void { process.env[name] = value; } +const agentRunnerShouldNotRun = neverRunAgentRunner(); + describe("agent continuation runner callbacks", () => { beforeEach(async () => { process.env.JUNIOR_STATE_ADAPTER = "memory"; @@ -117,6 +120,7 @@ describe("agent continuation runner callbacks", () => { expectedVersion: sessionRecord.version, }, { + agentRunner: agentRunnerShouldNotRun, resumeTurn: async (args) => { const prepared = await args.beforeStart?.(); if (!prepared) { @@ -219,6 +223,7 @@ describe("agent continuation runner callbacks", () => { expectedVersion: sessionRecord.version, }, { + agentRunner: agentRunnerShouldNotRun, resumeTurn: async (args) => { const prepared = await args.beforeStart?.(); if (prepared !== false) { @@ -308,6 +313,7 @@ describe("agent continuation runner callbacks", () => { expectedVersion: sessionRecord.version, }, { + agentRunner: agentRunnerShouldNotRun, resumeTurn: async (args) => { const prepared = await args.beforeStart?.(); if (prepared !== false) { diff --git a/packages/junior/tests/component/runtime/agent-continue.test.ts b/packages/junior/tests/component/runtime/agent-continue.test.ts index db5545104..5d422ccdb 100644 --- a/packages/junior/tests/component/runtime/agent-continue.test.ts +++ b/packages/junior/tests/component/runtime/agent-continue.test.ts @@ -11,6 +11,7 @@ import { SLACK_DESTINATION, createConversationWorkQueueTestAdapter, } from "../../fixtures/conversation-work"; +import { neverRunAgentRunner } from "../../fixtures/agent-runner"; const ORIGINAL_ENV = vi.hoisted(() => { const original = { @@ -28,6 +29,8 @@ function restoreEnv(name: string, value: string | undefined): void { process.env[name] = value; } +const agentRunnerShouldNotRun = neverRunAgentRunner(); + describe("agent continuation scheduling", () => { beforeEach(async () => { process.env.JUNIOR_STATE_ADAPTER = "memory"; @@ -125,7 +128,7 @@ describe("agent continuation scheduling", () => { sessionId: "turn_msg_2", expectedVersion: 1, }, - { scheduleAgentContinue }, + { agentRunner: agentRunnerShouldNotRun, scheduleAgentContinue }, ); await vi.advanceTimersByTimeAsync(4_000); @@ -156,9 +159,11 @@ describe("agent continuation scheduling", () => { piMessages: [], }); - await expect(resumeAwaitingSlackContinuation(conversationId)).resolves.toBe( - false, - ); + await expect( + resumeAwaitingSlackContinuation(conversationId, { + agentRunner: agentRunnerShouldNotRun, + }), + ).resolves.toBe(false); await expect( getAgentTurnSessionRecord(conversationId, "turn_msg_3"), ).resolves.toMatchObject({ @@ -187,13 +192,13 @@ describe("agent continuation scheduling", () => { await expect( resumeAwaitingSlackContinuation(conversationId, { - generateReply, + agentRunner: { run: generateReply }, resumeTurn, }), ).resolves.toBe(true); expect(resumeTurn).toHaveBeenCalledWith( - expect.objectContaining({ generateReply }), + expect.objectContaining({ agentRunner: { run: generateReply } }), ); }); @@ -253,9 +258,11 @@ describe("agent continuation scheduling", () => { }, }); - await expect(resumeAwaitingSlackContinuation(conversationId)).resolves.toBe( - false, - ); + await expect( + resumeAwaitingSlackContinuation(conversationId, { + agentRunner: agentRunnerShouldNotRun, + }), + ).resolves.toBe(false); await expect( getAgentTurnSessionRecord(conversationId, sessionId), ).resolves.toMatchObject({ diff --git a/packages/junior/tests/component/task-execution/slack-conversation-work.test.ts b/packages/junior/tests/component/task-execution/slack-conversation-work.test.ts index f4b062b2f..ad9fd227c 100644 --- a/packages/junior/tests/component/task-execution/slack-conversation-work.test.ts +++ b/packages/junior/tests/component/task-execution/slack-conversation-work.test.ts @@ -1307,8 +1307,10 @@ describe("Slack conversation work execution", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => { - throw new Error("persistent queued failure"); + agentRunner: { + run: async () => { + throw new Error("persistent queued failure"); + }, }, }, }, @@ -1604,23 +1606,25 @@ describe("Slack conversation work execution", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async (_text, context) => { - await context?.onInputCommitted?.(); - await context?.onArtifactStateUpdated?.({ - lastCanvasId: "F_YIELD_CANVAS", - lastCanvasUrl: "https://slack.example/docs/T/F_YIELD_CANVAS", - recentCanvases: [ - { - id: "F_YIELD_CANVAS", - title: "Yielded canvas", - url: "https://slack.example/docs/T/F_YIELD_CANVAS", - createdAt: "2026-07-02T12:00:00.000Z", - }, - ], - }); - currentNowMs = 242_000; - yieldedSessionId = context?.correlation?.turnId; - return { status: "suspended", resumeVersion: 1 }; + agentRunner: { + run: async (_text, context) => { + await context?.onInputCommitted?.(); + await context?.onArtifactStateUpdated?.({ + lastCanvasId: "F_YIELD_CANVAS", + lastCanvasUrl: "https://slack.example/docs/T/F_YIELD_CANVAS", + recentCanvases: [ + { + id: "F_YIELD_CANVAS", + title: "Yielded canvas", + url: "https://slack.example/docs/T/F_YIELD_CANVAS", + createdAt: "2026-07-02T12:00:00.000Z", + }, + ], + }); + currentNowMs = 242_000; + yieldedSessionId = context?.correlation?.turnId; + return { status: "suspended", resumeVersion: 1 }; + }, }, }, }, diff --git a/packages/junior/tests/fixtures/agent-runner.ts b/packages/junior/tests/fixtures/agent-runner.ts new file mode 100644 index 000000000..2e4e1d056 --- /dev/null +++ b/packages/junior/tests/fixtures/agent-runner.ts @@ -0,0 +1,25 @@ +import type { AgentRunner } from "@/chat/runtime/agent-runner"; + +/** + * Default harness runner: resolve @/chat/respond at call time so a test's + * vi.mock of that module is honored regardless of import order, while tests + * without the mock exercise the real reply generator. + */ +export const respondAgentRunner: AgentRunner = { + run: async (messageText, context) => { + const { generateAssistantReply } = await import("@/chat/respond"); + return await generateAssistantReply(messageText, context); + }, +}; + +/** + * Guard runner for paths that must never reach agent execution; failing loud + * beats silently producing a reply the test did not script. + */ +export function neverRunAgentRunner(): AgentRunner { + return { + run: async () => { + throw new Error("agent runner should not run in this test"); + }, + }; +} diff --git a/packages/junior/tests/fixtures/mcp-oauth-callback-harness.ts b/packages/junior/tests/fixtures/mcp-oauth-callback-harness.ts index b3c2a3d96..f486e2a48 100644 --- a/packages/junior/tests/fixtures/mcp-oauth-callback-harness.ts +++ b/packages/junior/tests/fixtures/mcp-oauth-callback-harness.ts @@ -1,12 +1,15 @@ +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import { waitUntilCallbacks, testWaitUntil, } from "./oauth-callback-after-harness"; +import { respondAgentRunner } from "./agent-runner"; export async function runMcpOauthCallbackRoute(args: { provider: string; state: string; code: string; + agentRunner?: AgentRunner; }) { waitUntilCallbacks.length = 0; const { GET } = await import("@/handlers/mcp-oauth-callback"); @@ -17,6 +20,7 @@ export async function runMcpOauthCallbackRoute(args: { ), args.provider, testWaitUntil, + { agentRunner: args.agentRunner ?? respondAgentRunner }, ); const callbacks = waitUntilCallbacks.splice(0, waitUntilCallbacks.length); for (const callback of callbacks) { diff --git a/packages/junior/tests/fixtures/oauth-callback-harness.ts b/packages/junior/tests/fixtures/oauth-callback-harness.ts index 8a61e7082..7c324f029 100644 --- a/packages/junior/tests/fixtures/oauth-callback-harness.ts +++ b/packages/junior/tests/fixtures/oauth-callback-harness.ts @@ -1,12 +1,15 @@ +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import { waitUntilCallbacks, testWaitUntil, } from "./oauth-callback-after-harness"; +import { respondAgentRunner } from "./agent-runner"; export async function runOauthCallbackRoute(args: { provider: string; state: string; code: string; + agentRunner?: AgentRunner; }) { waitUntilCallbacks.length = 0; const { GET } = await import("@/handlers/oauth-callback"); @@ -17,6 +20,7 @@ export async function runOauthCallbackRoute(args: { ), args.provider, testWaitUntil, + { agentRunner: args.agentRunner ?? respondAgentRunner }, ); const callbacks = waitUntilCallbacks.splice(0, waitUntilCallbacks.length); for (const callback of callbacks) { diff --git a/packages/junior/tests/integration/agent-continue-slack.test.ts b/packages/junior/tests/integration/agent-continue-slack.test.ts index 642e1da0d..be8766d82 100644 --- a/packages/junior/tests/integration/agent-continue-slack.test.ts +++ b/packages/junior/tests/integration/agent-continue-slack.test.ts @@ -69,7 +69,7 @@ function continueAgentRun(args: { sessionId: args.sessionId, }, { - generateReply: generateAssistantReplyMock, + agentRunner: { run: generateAssistantReplyMock }, scheduleAgentContinue: (request) => agentContinueServiceModule.scheduleAgentContinue(request, { queue, @@ -922,7 +922,7 @@ describe("agent continuation Slack integration", () => { agentContinueRunnerModule.resumeAwaitingSlackContinuation( conversationId, { - generateReply: generateAssistantReplyMock, + agentRunner: { run: generateAssistantReplyMock }, scheduleAgentContinue: (request) => agentContinueServiceModule.scheduleAgentContinue(request, { queue, @@ -1036,7 +1036,7 @@ describe("agent continuation Slack integration", () => { agentContinueRunnerModule.resumeAwaitingSlackContinuation( conversationId, { - generateReply: generateAssistantReplyMock, + agentRunner: { run: generateAssistantReplyMock }, }, ), ); diff --git a/packages/junior/tests/integration/agent-dispatch-runner.test.ts b/packages/junior/tests/integration/agent-dispatch-runner.test.ts index da0c0f8ab..ac56d3e47 100644 --- a/packages/junior/tests/integration/agent-dispatch-runner.test.ts +++ b/packages/junior/tests/integration/agent-dispatch-runner.test.ts @@ -25,6 +25,7 @@ import { } from "@/chat/credentials/subject"; import { getAgentTurnSessionRecord } from "@/chat/state/turn-session"; import { completedAgentRun } from "@/chat/runtime/agent-run-outcome"; +import { createAgentRunner } from "@/chat/runtime/agent-runner"; import { chatPostMessageOk } from "../fixtures/slack/factories/api"; import { getCapturedSlackApiCalls, @@ -210,9 +211,10 @@ describe("agent dispatch runner", () => { expectedVersion: created.record.version, }, { - generateAssistantReply, + agentRunner: createAgentRunner(generateAssistantReply, { + tracePropagation: { domains: ["*.sentry.io"] }, + }), scheduleSessionCompletedPluginTasks, - tracePropagation: { domains: ["*.sentry.io"] }, }, ); @@ -317,7 +319,7 @@ describe("agent dispatch runner", () => { id: created.record.id, expectedVersion: created.record.version, }, - { generateAssistantReply }, + { agentRunner: { run: generateAssistantReply } }, ); const persistedDestination = @@ -364,7 +366,7 @@ describe("agent dispatch runner", () => { id: created.record.id, expectedVersion: created.record.version, }, - { generateAssistantReply, scheduleCallback }, + { agentRunner: { run: generateAssistantReply }, scheduleCallback }, ); await expect(getDispatchRecord(created.record.id)).resolves.toMatchObject({ @@ -419,7 +421,7 @@ describe("agent dispatch runner", () => { id: created.record.id, expectedVersion: created.record.version, }, - { generateAssistantReply }, + { agentRunner: { run: generateAssistantReply } }, ); await expect(getDispatchRecord(created.record.id)).resolves.toMatchObject({ @@ -464,7 +466,7 @@ describe("agent dispatch runner", () => { expectedVersion: created.record.version, }, { - generateAssistantReply: async () => completedAgentRun(createReply()), + agentRunner: { run: async () => completedAgentRun(createReply()) }, }, ); } finally { @@ -486,7 +488,7 @@ describe("agent dispatch runner", () => { id: created.record.id, expectedVersion: created.record.version, }, - { generateAssistantReply: rerunGenerate }, + { agentRunner: { run: rerunGenerate } }, ); expect(rerunGenerate).not.toHaveBeenCalled(); expect(getCapturedSlackApiCalls("chat.postMessage")).toHaveLength(1); @@ -530,7 +532,7 @@ describe("agent dispatch runner", () => { id: created.record.id, expectedVersion: created.record.version, }, - { generateAssistantReply }, + { agentRunner: { run: generateAssistantReply } }, ); await expect(getDispatchRecord(created.record.id)).resolves.toMatchObject({ @@ -572,7 +574,7 @@ describe("agent dispatch runner", () => { id: created.record.id, expectedVersion: created.record.version, }, - { generateAssistantReply: async () => completedAgentRun(createReply()) }, + { agentRunner: { run: async () => completedAgentRun(createReply()) } }, ); await expect(getDispatchRecord(created.record.id)).resolves.toMatchObject({ status: "completed", @@ -608,7 +610,7 @@ describe("agent dispatch runner", () => { id: created.record.id, expectedVersion: reverted.version, }, - { generateAssistantReply: rerunGenerate }, + { agentRunner: { run: rerunGenerate } }, ); expect(rerunGenerate).not.toHaveBeenCalled(); @@ -645,8 +647,10 @@ describe("agent dispatch runner", () => { expectedVersion: created.record.version, }, { - generateAssistantReply: async () => { - throw new Error("busy conversation should not run"); + agentRunner: { + run: async () => { + throw new Error("busy conversation should not run"); + }, }, }, ); diff --git a/packages/junior/tests/integration/local-agent-runner.test.ts b/packages/junior/tests/integration/local-agent-runner.test.ts index 95a34e084..37ab516bf 100644 --- a/packages/junior/tests/integration/local-agent-runner.test.ts +++ b/packages/junior/tests/integration/local-agent-runner.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import type { - AssistantReply, - generateAssistantReply, - ReplyRequestContext, -} from "@/chat/respond"; +import type { AssistantReply, ReplyRequestContext } from "@/chat/respond"; import { defineJuniorPlugin, type PluginRunContext, @@ -16,6 +12,7 @@ import { type LocalToolResult, } from "@/chat/local/runner"; import type { PiMessage } from "@/chat/pi/messages"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import { persistCompletedSessionRecord } from "@/chat/services/turn-session-record"; import { getPersistedSandboxState, @@ -86,12 +83,10 @@ describe("local agent runner", () => { expect(conversationId).toBeDefined(); const contexts: ReplyRequestContext[] = []; - const generateReply = vi.fn( - async (_text, context) => { - contexts.push(context); - return completedAgentRun(successReply("hello from local")); - }, - ); + const generateReply = vi.fn(async (_text, context) => { + contexts.push(context); + return completedAgentRun(successReply("hello from local")); + }); const delivered: LocalAgentReply[] = []; await runLocalAgentTurn( @@ -103,7 +98,7 @@ describe("local agent runner", () => { deliverReply: async (reply) => { delivered.push(reply); }, - generateAssistantReply: generateReply, + agentRunner: { run: generateReply }, }, ); @@ -171,23 +166,21 @@ describe("local agent runner", () => { }); expect(conversationId).toBeDefined(); - const generateReply = vi.fn( - async (_text, context) => { - context.onToolInvocation?.({ - toolName: "createMemory", - params: { content: "The requester prefers short updates." }, - }); - await context.onToolResult?.({ - ok: true, - toolName: "createMemory", - params: { content: "The requester prefers short updates." }, - result: { ok: true }, - }); - return completedAgentRun( - successReply("saved", { toolCalls: ["createMemory"] }), - ); - }, - ); + const generateReply = vi.fn(async (_text, context) => { + context.onToolInvocation?.({ + toolName: "createMemory", + params: { content: "The requester prefers short updates." }, + }); + await context.onToolResult?.({ + ok: true, + toolName: "createMemory", + params: { content: "The requester prefers short updates." }, + result: { ok: true }, + }); + return completedAgentRun( + successReply("saved", { toolCalls: ["createMemory"] }), + ); + }); const invocations: LocalToolInvocation[] = []; const results: LocalToolResult[] = []; @@ -198,7 +191,7 @@ describe("local agent runner", () => { }, { deliverReply: async () => undefined, - generateAssistantReply: generateReply, + agentRunner: { run: generateReply }, onToolInvocation: async (invocation) => { invocations.push(invocation); }, @@ -258,23 +251,25 @@ describe("local agent runner", () => { }, { deliverReply: async () => undefined, - generateAssistantReply: async (_text, context) => { - const piMessages = [ - { - role: "user", - content: "capture this local turn", - }, - { - role: "assistant", - content: "captured", - }, - ] as PiMessage[]; - await persistCompletedSessionForFakeReply(context, piMessages); - return completedAgentRun( - successReply("captured", { - piMessages, - }), - ); + agentRunner: { + run: async (_text, context) => { + const piMessages = [ + { + role: "user", + content: "capture this local turn", + }, + { + role: "assistant", + content: "captured", + }, + ] as PiMessage[]; + await persistCompletedSessionForFakeReply(context, piMessages); + return completedAgentRun( + successReply("captured", { + piMessages, + }), + ); + }, }, }, ); @@ -323,12 +318,10 @@ describe("local agent runner", () => { expect(conversationId).toBeDefined(); const contexts: ReplyRequestContext[] = []; - const generateReply = vi.fn( - async (text, context) => { - contexts.push(context); - return completedAgentRun(successReply(`reply to ${text}`)); - }, - ); + const generateReply = vi.fn(async (text, context) => { + contexts.push(context); + return completedAgentRun(successReply(`reply to ${text}`)); + }); await runLocalAgentTurn( { @@ -337,7 +330,7 @@ describe("local agent runner", () => { }, { deliverReply: async () => undefined, - generateAssistantReply: generateReply, + agentRunner: { run: generateReply }, }, ); await runLocalAgentTurn( @@ -347,7 +340,7 @@ describe("local agent runner", () => { }, { deliverReply: async () => undefined, - generateAssistantReply: generateReply, + agentRunner: { run: generateReply }, }, ); @@ -373,7 +366,7 @@ describe("local agent runner", () => { }); expect(conversationId).toBeDefined(); - const generateReply = vi.fn(async () => + const generateReply = vi.fn(async () => completedAgentRun(successReply("not delivered")), ); @@ -384,7 +377,7 @@ describe("local agent runner", () => { message: "hello", }, { - generateAssistantReply: generateReply, + agentRunner: { run: generateReply }, } as unknown as Parameters[1], ), ).rejects.toThrow("Local reply delivery is required"); @@ -396,7 +389,7 @@ describe("local agent runner", () => { }); it("rejects malformed local conversation ids before generation", async () => { - const generateReply = vi.fn(async () => { + const generateReply = vi.fn(async () => { throw new Error("generation should not run"); }); @@ -408,7 +401,7 @@ describe("local agent runner", () => { }, { deliverReply: async () => undefined, - generateAssistantReply: generateReply, + agentRunner: { run: generateReply }, }, ), ).rejects.toThrow("Invalid local conversation id"); @@ -431,12 +424,10 @@ describe("local agent runner", () => { }); const contexts: ReplyRequestContext[] = []; - const generateReply = vi.fn( - async (_text, context) => { - contexts.push(context); - return completedAgentRun(successReply("uses projection")); - }, - ); + const generateReply = vi.fn(async (_text, context) => { + contexts.push(context); + return completedAgentRun(successReply("uses projection")); + }); await runLocalAgentTurn( { @@ -445,7 +436,7 @@ describe("local agent runner", () => { }, { deliverReply: async () => undefined, - generateAssistantReply: generateReply, + agentRunner: { run: generateReply }, }, ); @@ -469,7 +460,7 @@ describe("local agent runner", () => { content: [{ type: "text", text: "persisted pi output" }], }, ] as PiMessage[]; - const generateReply = vi.fn(async () => + const generateReply = vi.fn(async () => completedAgentRun( successReply("persisted visible output", { piMessages: generatedMessages, @@ -484,7 +475,7 @@ describe("local agent runner", () => { }, { deliverReply: async () => undefined, - generateAssistantReply: generateReply, + agentRunner: { run: generateReply }, }, ); @@ -503,9 +494,11 @@ describe("local agent runner", () => { }, { deliverReply: async () => undefined, - generateAssistantReply: async (_text, context) => { - contexts.push(context); - return completedAgentRun(successReply("follow up reply")); + agentRunner: { + run: async (_text, context) => { + contexts.push(context); + return completedAgentRun(successReply("follow up reply")); + }, }, }, ); @@ -561,16 +554,18 @@ describe("local agent runner", () => { deliverReply: async (reply) => { delivered.push(reply); }, - generateAssistantReply: async (_text, context) => { - await persistCompletedSessionForFakeReply( - context, - generatedMessages, - ); - return completedAgentRun( - successReply("visible reply", { - piMessages: generatedMessages, - }), - ); + agentRunner: { + run: async (_text, context) => { + await persistCompletedSessionForFakeReply( + context, + generatedMessages, + ); + return completedAgentRun( + successReply("visible reply", { + piMessages: generatedMessages, + }), + ); + }, }, }, ), @@ -625,9 +620,11 @@ describe("local agent runner", () => { }, { deliverReply: async () => undefined, - generateAssistantReply: async (_text, context) => { - contexts.push(context); - return completedAgentRun(successReply("uses newer fallback")); + agentRunner: { + run: async (_text, context) => { + contexts.push(context); + return completedAgentRun(successReply("uses newer fallback")); + }, }, }, ); @@ -646,24 +643,22 @@ describe("local agent runner", () => { role: "assistant", content: [{ type: "text", text: "undelivered pi output" }], } as PiMessage; - const generateReply = vi.fn( - async (_text, context) => { - await context.onArtifactStateUpdated?.({ - lastCanvasId: "canvas-undelivered", - lastCanvasUrl: "https://example.invalid/canvas", - }); - await context.onSandboxAcquired?.({ - sandboxDependencyProfileHash: "profile-undelivered", - sandboxId: "sandbox-undelivered", - }); - await commitMessages({ - conversationId: conversationId!, - messages: [assistantMessage], - ttlMs: 60_000, - }); - return completedAgentRun(successReply("not delivered")); - }, - ); + const generateReply = vi.fn(async (_text, context) => { + await context.onArtifactStateUpdated?.({ + lastCanvasId: "canvas-undelivered", + lastCanvasUrl: "https://example.invalid/canvas", + }); + await context.onSandboxAcquired?.({ + sandboxDependencyProfileHash: "profile-undelivered", + sandboxId: "sandbox-undelivered", + }); + await commitMessages({ + conversationId: conversationId!, + messages: [assistantMessage], + ttlMs: 60_000, + }); + return completedAgentRun(successReply("not delivered")); + }); await expect( runLocalAgentTurn( @@ -675,7 +670,7 @@ describe("local agent runner", () => { deliverReply: async () => { throw new Error("stdout closed"); }, - generateAssistantReply: generateReply, + agentRunner: { run: generateReply }, }, ), ).rejects.toThrow("stdout closed"); diff --git a/packages/junior/tests/integration/mcp-oauth-callback-slack.test.ts b/packages/junior/tests/integration/mcp-oauth-callback-slack.test.ts index e159fc7a6..65e3e89e1 100644 --- a/packages/junior/tests/integration/mcp-oauth-callback-slack.test.ts +++ b/packages/junior/tests/integration/mcp-oauth-callback-slack.test.ts @@ -20,13 +20,8 @@ import { } from "../fixtures/plugin-app"; import { completedAgentRun } from "@/chat/runtime/agent-run-outcome"; -const { generateAssistantReplyMock } = vi.hoisted(() => ({ - generateAssistantReplyMock: vi.fn(), -})); - -vi.mock("@/chat/respond", () => ({ - generateAssistantReply: generateAssistantReplyMock, -})); +const generateAssistantReplyMock = vi.fn(); +const testAgentRunner = { run: generateAssistantReplyMock }; const ORIGINAL_ENV = { ...process.env }; const EVAL_MCP_PLUGIN_ROOT = path.resolve( @@ -353,6 +348,7 @@ describe("mcp oauth callback slack integration", () => { provider: EVAL_MCP_AUTH_PROVIDER, state: authProvider.authSessionId, code: EVAL_MCP_AUTH_CODE, + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); @@ -517,6 +513,7 @@ describe("mcp oauth callback slack integration", () => { provider: EVAL_MCP_AUTH_PROVIDER, state: authProvider.authSessionId, code: EVAL_MCP_AUTH_CODE, + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); @@ -653,6 +650,7 @@ describe("mcp oauth callback slack integration", () => { provider: EVAL_MCP_AUTH_PROVIDER, state: authProvider.authSessionId, code: EVAL_MCP_AUTH_CODE, + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); @@ -759,6 +757,7 @@ describe("mcp oauth callback slack integration", () => { provider: EVAL_MCP_AUTH_PROVIDER, state: authProvider.authSessionId, code: EVAL_MCP_AUTH_CODE, + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); @@ -824,6 +823,7 @@ describe("mcp oauth callback slack integration", () => { provider: EVAL_MCP_AUTH_PROVIDER, state: authProvider.authSessionId, code: EVAL_MCP_AUTH_CODE, + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); @@ -888,6 +888,7 @@ describe("mcp oauth callback slack integration", () => { provider: EVAL_MCP_AUTH_PROVIDER, state: authProvider.authSessionId, code: EVAL_MCP_AUTH_CODE, + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); @@ -972,6 +973,7 @@ describe("mcp oauth callback slack integration", () => { provider: EVAL_MCP_AUTH_PROVIDER, state: authProvider.authSessionId, code: EVAL_MCP_AUTH_CODE, + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); @@ -1057,6 +1059,7 @@ describe("mcp oauth callback slack integration", () => { provider: EVAL_MCP_AUTH_PROVIDER, state: authProvider.authSessionId, code: EVAL_MCP_AUTH_CODE, + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); diff --git a/packages/junior/tests/integration/oauth-callback-slack.test.ts b/packages/junior/tests/integration/oauth-callback-slack.test.ts index ebe51c263..d529a1785 100644 --- a/packages/junior/tests/integration/oauth-callback-slack.test.ts +++ b/packages/junior/tests/integration/oauth-callback-slack.test.ts @@ -11,13 +11,8 @@ import { } from "../fixtures/plugin-app"; import { completedAgentRun } from "@/chat/runtime/agent-run-outcome"; -const { generateAssistantReplyMock } = vi.hoisted(() => ({ - generateAssistantReplyMock: vi.fn(), -})); - -vi.mock("@/chat/respond", () => ({ - generateAssistantReply: generateAssistantReplyMock, -})); +const generateAssistantReplyMock = vi.fn(); +const testAgentRunner = { run: generateAssistantReplyMock }; const ORIGINAL_ENV = { ...process.env }; const EVAL_OAUTH_PLUGIN_ROOT = path.resolve( @@ -106,6 +101,7 @@ describe("oauth callback slack integration", () => { provider: "eval-oauth", state: "eval-oauth-state", code: "eval-oauth-code", + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); @@ -174,6 +170,7 @@ describe("oauth callback slack integration", () => { provider: "eval-oauth", state: "eval-oauth-resume-state", code: "eval-oauth-code", + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); @@ -341,6 +338,7 @@ describe("oauth callback slack integration", () => { provider: "eval-oauth", state: "eval-oauth-session-record-state", code: "eval-oauth-code", + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); @@ -516,6 +514,7 @@ describe("oauth callback slack integration", () => { provider: "eval-oauth", state: "eval-oauth-mismatched-requester-state", code: "eval-oauth-code", + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); @@ -665,6 +664,7 @@ describe("oauth callback slack integration", () => { provider: "eval-oauth", state: "eval-oauth-locked-state", code: "eval-oauth-code", + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); @@ -803,6 +803,7 @@ describe("oauth callback slack integration", () => { provider: "eval-oauth", state: "eval-oauth-reused-link-state", code: "eval-oauth-code", + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); @@ -862,6 +863,7 @@ describe("oauth callback slack integration", () => { provider: "eval-oauth", state: "eval-oauth-abandoned-state", code: "eval-oauth-code", + agentRunner: testAgentRunner, }); expect(response.status).toBe(200); diff --git a/packages/junior/tests/integration/oauth-resume-slack.test.ts b/packages/junior/tests/integration/oauth-resume-slack.test.ts index f8c212664..35236f562 100644 --- a/packages/junior/tests/integration/oauth-resume-slack.test.ts +++ b/packages/junior/tests/integration/oauth-resume-slack.test.ts @@ -82,16 +82,18 @@ describe("oauth resume slack integration", () => { source: testSlackSource("1700000000.001"), requester: { platform: "slack", teamId: "T123", userId: "U123" }, }, - generateReply: async () => - completedAgentRun({ - text: "The budget deadline you mentioned earlier was Friday.", - diagnostics: makeDiagnostics("success", { - durationMs: 842, - usage: { - totalTokens: 1234, - }, + agentRunner: { + run: async () => + completedAgentRun({ + text: "The budget deadline you mentioned earlier was Friday.", + diagnostics: makeDiagnostics("success", { + durationMs: 842, + usage: { + totalTokens: 1234, + }, + }), }), - }), + }, }); expect(getCapturedSlackApiCalls("assistant.threads.setStatus")).toEqual([ @@ -184,16 +186,18 @@ describe("oauth resume slack integration", () => { turnId: "turn-1", }, }, - generateReply: async () => - completedAgentRun({ - text: "done", - diagnostics: makeDiagnostics("success", { - durationMs: 500, - usage: { - outputTokens: 7, - }, + agentRunner: { + run: async () => + completedAgentRun({ + text: "done", + diagnostics: makeDiagnostics("success", { + durationMs: 500, + usage: { + outputTokens: 7, + }, + }), }), - }), + }, }); expect(getCapturedSlackApiCalls("chat.postMessage")).toEqual([ @@ -243,10 +247,12 @@ describe("oauth resume slack integration", () => { turnId: "turn-auth-pause", }, }, - generateReply: async () => ({ - status: "awaiting_auth" as const, - providerDisplayName: "Eval Auth", - }), + agentRunner: { + run: async () => ({ + status: "awaiting_auth" as const, + providerDisplayName: "Eval Auth", + }), + }, onAuthPause: async () => undefined, }); @@ -286,11 +292,13 @@ describe("oauth resume slack integration", () => { source: testSlackSource("1700000000.002"), requester: { platform: "slack", teamId: "T123", userId: "U123" }, }, - generateReply: async () => - completedAgentRun({ - text: longReply, - diagnostics: makeDiagnostics(), - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: longReply, + diagnostics: makeDiagnostics(), + }), + }, }); const postCalls = getCapturedSlackApiCalls("chat.postMessage"); @@ -326,11 +334,13 @@ describe("oauth resume slack integration", () => { source: testSlackSource("1700000000.003"), requester: { platform: "slack", teamId: "T123", userId: "U123" }, }, - generateReply: async () => - completedAgentRun({ - text: "Partial output", - diagnostics: makeDiagnostics("provider_error"), - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "Partial output", + diagnostics: makeDiagnostics("provider_error"), + }), + }, }); const postCalls = getCapturedSlackApiCalls("chat.postMessage"); @@ -363,14 +373,16 @@ describe("oauth resume slack integration", () => { source: testSlackSource("1700000000.006"), requester: { platform: "slack", teamId: "T123", userId: "U123" }, }, - generateReply: async () => - completedAgentRun({ - text: "", - diagnostics: makeDiagnostics("execution_failure", { - assistantMessageCount: 0, - usedPrimaryText: false, + agentRunner: { + run: async () => + completedAgentRun({ + text: "", + diagnostics: makeDiagnostics("execution_failure", { + assistantMessageCount: 0, + usedPrimaryText: false, + }), }), - }), + }, }); const postCalls = getCapturedSlackApiCalls("chat.postMessage"); @@ -401,17 +413,19 @@ describe("oauth resume slack integration", () => { source: testSlackSource("1700000000.004"), requester: { platform: "slack", teamId: "T123", userId: "U123" }, }, - generateReply: async () => - completedAgentRun({ - text: "Here is the resumed artifact.", - files: [ - { - data: Buffer.from("resume-file"), - filename: "resume.txt", - }, - ], - diagnostics: makeDiagnostics(), - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "Here is the resumed artifact.", + files: [ + { + data: Buffer.from("resume-file"), + filename: "resume.txt", + }, + ], + diagnostics: makeDiagnostics(), + }), + }, }); const postCalls = getCapturedSlackApiCalls("chat.postMessage"); @@ -460,17 +474,19 @@ describe("oauth resume slack integration", () => { source: testSlackSource("1700000000.005"), requester: { platform: "slack", teamId: "T123", userId: "U123" }, }, - generateReply: async () => - completedAgentRun({ - text: "Here is the resumed artifact.", - files: [ - { - data: Buffer.from("resume-file"), - filename: "resume.txt", - }, - ], - diagnostics: makeDiagnostics(), - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "Here is the resumed artifact.", + files: [ + { + data: Buffer.from("resume-file"), + filename: "resume.txt", + }, + ], + diagnostics: makeDiagnostics(), + }), + }, }); expect(getCapturedSlackApiCalls("chat.postMessage")).toEqual([ diff --git a/packages/junior/tests/integration/slack/assistant-context-canvas-routing.test.ts b/packages/junior/tests/integration/slack/assistant-context-canvas-routing.test.ts index 8a6067813..2352ed4b7 100644 --- a/packages/junior/tests/integration/slack/assistant-context-canvas-routing.test.ts +++ b/packages/junior/tests/integration/slack/assistant-context-canvas-routing.test.ts @@ -42,24 +42,26 @@ describe("Slack behavior: assistant context canvas routing", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - await createCanvas({ - title: "Shared update", - markdown: "Context-aware update", - channelId: context?.toolChannelId, - }); - return completedAgentRun({ - text: "Shared canvas created.", - diagnostics: { - assistantMessageCount: 1, - modelId: "fake-agent-model", - outcome: "success", - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async (_prompt, context) => { + await createCanvas({ + title: "Shared update", + markdown: "Context-aware update", + channelId: context?.toolChannelId, + }); + return completedAgentRun({ + text: "Shared canvas created.", + diagnostics: { + assistantMessageCount: 1, + modelId: "fake-agent-model", + outcome: "success", + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, diff --git a/packages/junior/tests/integration/slack/assistant-context-channel-behavior.test.ts b/packages/junior/tests/integration/slack/assistant-context-channel-behavior.test.ts index 363a4ef76..76e9cd93f 100644 --- a/packages/junior/tests/integration/slack/assistant-context-channel-behavior.test.ts +++ b/packages/junior/tests/integration/slack/assistant-context-channel-behavior.test.ts @@ -14,20 +14,22 @@ describe("Slack behavior: assistant context channel routing", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - capturedToolChannelIds.push(context?.toolChannelId); - return completedAgentRun({ - text: "Canvas draft prepared.", - diagnostics: { - assistantMessageCount: 1, - modelId: "fake-agent-model", - outcome: "success", - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async (_prompt, context) => { + capturedToolChannelIds.push(context?.toolChannelId); + return completedAgentRun({ + text: "Canvas draft prepared.", + diagnostics: { + assistantMessageCount: 1, + modelId: "fake-agent-model", + outcome: "success", + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, diff --git a/packages/junior/tests/integration/slack/assistant-thread-contract.test.ts b/packages/junior/tests/integration/slack/assistant-thread-contract.test.ts index 2bf69f3ad..4d25f582b 100644 --- a/packages/junior/tests/integration/slack/assistant-thread-contract.test.ts +++ b/packages/junior/tests/integration/slack/assistant-thread-contract.test.ts @@ -9,7 +9,7 @@ import { createSlackWebhookTestClient } from "../../fixtures/slack/webhook-clien import { createSlackRuntime } from "@/chat/app/factory"; import { JuniorChat } from "@/chat/ingress/junior-chat"; import { makeAssistantStatus } from "@/chat/slack/assistant-thread/status"; -import type { ReplyExecutorServices } from "@/chat/runtime/reply-executor"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import { createJuniorSlackAdapter } from "@/chat/slack/adapter"; import type { ConversationMemoryDeps } from "@/chat/services/conversation-memory"; import { handleChatSdkPlatformWebhook } from "@/handlers/webhooks"; @@ -76,7 +76,7 @@ function makeCompletedReply(text: string) { async function createDirectMessageBot(args: { completeText?: ConversationMemoryDeps["completeText"]; - generateAssistantReply: ReplyExecutorServices["generateAssistantReply"]; + agentRunner: AgentRunner; }) { const bot = new JuniorChat<{ slack: SlackAdapter }>({ userName: "junior", @@ -101,7 +101,7 @@ async function createDirectMessageBot(args: { } : {}), replyExecutor: { - generateAssistantReply: args.generateAssistantReply, + agentRunner: args.agentRunner, }, }, }); @@ -115,9 +115,7 @@ async function createDirectMessageBot(args: { return bot; } -async function createMentionBot(args: { - generateAssistantReply: ReplyExecutorServices["generateAssistantReply"]; -}) { +async function createMentionBot(args: { agentRunner: AgentRunner }) { const bot = new JuniorChat<{ slack: SlackAdapter }>({ userName: "junior", adapters: { @@ -133,7 +131,7 @@ async function createMentionBot(args: { getSlackAdapter: () => bot.getAdapter("slack"), services: { replyExecutor: { - generateAssistantReply: args.generateAssistantReply, + agentRunner: args.agentRunner, }, }, }); @@ -158,9 +156,11 @@ describe("Slack contract: assistant-thread delivery", () => { it("does not post assistant status when the DM message omits thread_ts", async () => { const bot = await createDirectMessageBot({ - generateAssistantReply: async (_prompt, context) => { - await context?.onStatus?.(makeAssistantStatus("running", "bash")); - return makeCompletedReply("Done."); + agentRunner: { + run: async (_prompt, context) => { + await context?.onStatus?.(makeAssistantStatus("running", "bash")); + return makeCompletedReply("Done."); + }, }, }); const waitUntil = slackWebhookClient.waitUntil(); @@ -180,9 +180,11 @@ describe("Slack contract: assistant-thread delivery", () => { it("posts assistant status with a raw DM channel id when thread_ts is present", async () => { const bot = await createDirectMessageBot({ - generateAssistantReply: async (_prompt, context) => { - await context?.onStatus?.(makeAssistantStatus("running", "bash")); - return makeCompletedReply("Done."); + agentRunner: { + run: async (_prompt, context) => { + await context?.onStatus?.(makeAssistantStatus("running", "bash")); + return makeCompletedReply("Done."); + }, }, }); const waitUntil = slackWebhookClient.waitUntil(); @@ -221,9 +223,11 @@ describe("Slack contract: assistant-thread delivery", () => { it("posts assistant status for the first channel-thread reply before Slack adds thread_ts", async () => { const bot = await createMentionBot({ - generateAssistantReply: async (_prompt, context) => { - await context?.onStatus?.(makeAssistantStatus("running", "bash")); - return makeCompletedReply("Done."); + agentRunner: { + run: async (_prompt, context) => { + await context?.onStatus?.(makeAssistantStatus("running", "bash")); + return makeCompletedReply("Done."); + }, }, }); const waitUntil = slackWebhookClient.waitUntil(); @@ -265,8 +269,10 @@ describe("Slack contract: assistant-thread delivery", () => { text: "Debugging Node.js Memory Leaks", message: { role: "assistant", content: "" }, }) as any, - generateAssistantReply: async () => - makeCompletedReply("Here is how to debug memory leaks."), + agentRunner: { + run: async () => + makeCompletedReply("Here is how to debug memory leaks."), + }, }); const waitUntil = slackWebhookClient.waitUntil(); @@ -305,8 +311,10 @@ describe("Slack contract: assistant-thread delivery", () => { } as any); }; }), - generateAssistantReply: async () => - makeCompletedReply("Here is how to debug memory leaks."), + agentRunner: { + run: async () => + makeCompletedReply("Here is how to debug memory leaks."), + }, }); const waitUntil = slackWebhookClient.waitUntil(); @@ -333,6 +341,7 @@ describe("Slack contract: assistant-thread delivery", () => { ]), ); + resetSlackApiMockState(); resolveTitle!(); await vi.waitFor(() => { expect(slackApiOutbox.calls("assistant.threads.setTitle")).toEqual([ @@ -354,8 +363,10 @@ describe("Slack contract: assistant-thread delivery", () => { text: "Debugging Node.js Memory Leaks", message: { role: "assistant", content: "" }, }) as any, - generateAssistantReply: async () => - makeCompletedReply("Here is how to debug memory leaks."), + agentRunner: { + run: async () => + makeCompletedReply("Here is how to debug memory leaks."), + }, }); const waitUntil = slackWebhookClient.waitUntil(); diff --git a/packages/junior/tests/integration/slack/attachment-behavior.test.ts b/packages/junior/tests/integration/slack/attachment-behavior.test.ts index 55a62b969..3e84ea10e 100644 --- a/packages/junior/tests/integration/slack/attachment-behavior.test.ts +++ b/packages/junior/tests/integration/slack/attachment-behavior.test.ts @@ -61,25 +61,27 @@ describe("Slack behavior: attachment handling", () => { completeText: completeTextMock, }, replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - const attachments = context?.userAttachments ?? []; - capturedAttachmentCounts.push(attachments.length); - if (attachments[0]) { - capturedAttachmentMediaTypes.push(attachments[0].mediaType); - } - - return completedAgentRun({ - text: "Image received. The chart trend is upward.", - diagnostics: { - assistantMessageCount: 1, - modelId: "fake-agent-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async (_prompt, context) => { + const attachments = context?.userAttachments ?? []; + capturedAttachmentCounts.push(attachments.length); + if (attachments[0]) { + capturedAttachmentMediaTypes.push(attachments[0].mediaType); + } + + return completedAgentRun({ + text: "Image received. The chart trend is upward.", + diagnostics: { + assistantMessageCount: 1, + modelId: "fake-agent-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, @@ -141,7 +143,7 @@ describe("Slack behavior: attachment handling", () => { completeText: completeTextMock, }, replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, }, }, }); diff --git a/packages/junior/tests/integration/slack/attachment-media-behavior.test.ts b/packages/junior/tests/integration/slack/attachment-media-behavior.test.ts index 5649ff739..2b4a3b321 100644 --- a/packages/junior/tests/integration/slack/attachment-media-behavior.test.ts +++ b/packages/junior/tests/integration/slack/attachment-media-behavior.test.ts @@ -69,26 +69,28 @@ describe("Slack behavior: mixed attachment media", () => { completeText: completeTextMock, }, replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - const attachments = context?.userAttachments ?? []; - capturedAttachmentMediaTypes.push( - attachments.map((attachment) => attachment.mediaType), - ); - capturedAttachmentNames.push( - attachments.map((attachment) => attachment.filename ?? ""), - ); - return completedAgentRun({ - text: "Processed attachments.", - diagnostics: { - assistantMessageCount: 1, - modelId: "fake-agent-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async (_prompt, context) => { + const attachments = context?.userAttachments ?? []; + capturedAttachmentMediaTypes.push( + attachments.map((attachment) => attachment.mediaType), + ); + capturedAttachmentNames.push( + attachments.map((attachment) => attachment.filename ?? ""), + ); + return completedAgentRun({ + text: "Processed attachments.", + diagnostics: { + assistantMessageCount: 1, + modelId: "fake-agent-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, @@ -163,29 +165,31 @@ describe("Slack behavior: mixed attachment media", () => { const { slackRuntime } = await createRuntime({ services: { replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - const attachments = context?.userAttachments ?? []; - capturedAttachmentMediaTypes.push( - attachments.map((attachment) => attachment.mediaType), - ); - capturedAttachmentNames.push( - attachments.map((attachment) => attachment.filename ?? ""), - ); - capturedOmittedImageCounts.push( - context?.omittedImageAttachmentCount ?? 0, - ); - return completedAgentRun({ - text: "Processed attachments.", - diagnostics: { - assistantMessageCount: 1, - modelId: "fake-agent-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async (_prompt, context) => { + const attachments = context?.userAttachments ?? []; + capturedAttachmentMediaTypes.push( + attachments.map((attachment) => attachment.mediaType), + ); + capturedAttachmentNames.push( + attachments.map((attachment) => attachment.filename ?? ""), + ); + capturedOmittedImageCounts.push( + context?.omittedImageAttachmentCount ?? 0, + ); + return completedAgentRun({ + text: "Processed attachments.", + diagnostics: { + assistantMessageCount: 1, + modelId: "fake-agent-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, @@ -248,11 +252,13 @@ describe("Slack behavior: mixed attachment media", () => { const { slackRuntime } = await createRuntime({ services: { replyExecutor: { - generateAssistantReply: async (prompt, context) => { - capturedOmittedImageCounts.push( - context?.omittedImageAttachmentCount ?? 0, - ); - return generateAssistantReply(prompt, context); + agentRunner: { + run: async (prompt, context) => { + capturedOmittedImageCounts.push( + context?.omittedImageAttachmentCount ?? 0, + ); + return generateAssistantReply(prompt, context); + }, }, }, }, diff --git a/packages/junior/tests/integration/slack/bot-handlers.test.ts b/packages/junior/tests/integration/slack/bot-handlers.test.ts index 9d036e368..c9ed2d01c 100644 --- a/packages/junior/tests/integration/slack/bot-handlers.test.ts +++ b/packages/junior/tests/integration/slack/bot-handlers.test.ts @@ -176,19 +176,21 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: "Hello from the bot!", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "Hello from the bot!", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }), + }, scheduleSessionCompletedPluginTasks, }, visionContext: { @@ -237,7 +239,7 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, }, }, }); @@ -326,19 +328,21 @@ describe("bot handlers (integration)", () => { }) as any, }, replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: "Replying to mention", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "Replying to mention", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }), + }, }, visionContext: { listThreadReplies: async () => [], @@ -442,8 +446,10 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => { - throw new Error("LLM unavailable"); + agentRunner: { + run: async () => { + throw new Error("LLM unavailable"); + }, }, }, visionContext: { @@ -482,54 +488,56 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => { - // Simulate respond's durable input checkpoint: the session record - // is running at the prompt boundary when generation finishes. - await upsertAgentTurnSessionRecord({ - conversationId, - sessionId, - sliceId: 1, - state: "running", - piMessages: promptMessages, - }); - return completedAgentRun({ - text: finalText, - piMessages: [ - ...promptMessages, - { - role: "assistant" as const, - content: [{ type: "text" as const, text: finalText }], - api: "responses" as const, - provider: "openai", - model: "gpt-5.3", - usage: { - input: 1, - output: 1, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 2, - cost: { - input: 0, - output: 0, + agentRunner: { + run: async () => { + // Simulate respond's durable input checkpoint: the session record + // is running at the prompt boundary when generation finishes. + await upsertAgentTurnSessionRecord({ + conversationId, + sessionId, + sliceId: 1, + state: "running", + piMessages: promptMessages, + }); + return completedAgentRun({ + text: finalText, + piMessages: [ + ...promptMessages, + { + role: "assistant" as const, + content: [{ type: "text" as const, text: finalText }], + api: "responses" as const, + provider: "openai", + model: "gpt-5.3", + usage: { + input: 1, + output: 1, cacheRead: 0, cacheWrite: 0, - total: 0, + totalTokens: 2, + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + total: 0, + }, }, + stopReason: "stop" as const, + timestamp: 2, }, - stopReason: "stop" as const, - timestamp: 2, + ], + diagnostics: { + assistantMessageCount: 1, + modelId: "fake-agent-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, }, - ], - diagnostics: { - assistantMessageCount: 1, - modelId: "fake-agent-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + }); + }, }, }, visionContext: { @@ -607,19 +615,21 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: finalText, - diagnostics: { - assistantMessageCount: 1, - modelId: "fake-agent-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: finalText, + diagnostics: { + assistantMessageCount: 1, + modelId: "fake-agent-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }), + }, }, visionContext: { listThreadReplies: async () => [], @@ -681,25 +691,27 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - capturedCorrelation.push({ - conversationId: context?.correlation?.conversationId, - threadId: context?.correlation?.threadId, - turnId: context?.correlation?.turnId, - runId: context?.correlation?.runId, - }); - return completedAgentRun({ - text: "Done.", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async (_prompt, context) => { + capturedCorrelation.push({ + conversationId: context?.correlation?.conversationId, + threadId: context?.correlation?.threadId, + turnId: context?.correlation?.turnId, + runId: context?.correlation?.runId, + }); + return completedAgentRun({ + text: "Done.", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, @@ -736,11 +748,13 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => { - return { - status: "awaiting_auth", - providerDisplayName: "Notion", - }; + agentRunner: { + run: async () => { + return { + status: "awaiting_auth", + providerDisplayName: "Notion", + }; + }, }, }, }, @@ -813,11 +827,13 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => { - return { - status: "awaiting_auth", - providerDisplayName: "GitHub", - }; + agentRunner: { + run: async () => { + return { + status: "awaiting_auth", + providerDisplayName: "GitHub", + }; + }, }, }, }, @@ -897,8 +913,10 @@ describe("bot handlers (integration)", () => { services: { replyExecutor: { scheduleAgentContinue, - generateAssistantReply: async () => { - return { status: "suspended", resumeVersion: 3 }; + agentRunner: { + run: async () => { + return { status: "suspended", resumeVersion: 3 }; + }, }, }, }, @@ -947,8 +965,10 @@ describe("bot handlers (integration)", () => { services: { replyExecutor: { scheduleAgentContinue, - generateAssistantReply: async () => { - return { status: "suspended", resumeVersion: 4 }; + agentRunner: { + run: async () => { + return { status: "suspended", resumeVersion: 4 }; + }, }, }, }, @@ -990,8 +1010,10 @@ describe("bot handlers (integration)", () => { services: { replyExecutor: { scheduleAgentContinue, - generateAssistantReply: async () => { - return { status: "suspended", resumeVersion: 3 }; + agentRunner: { + run: async () => { + return { status: "suspended", resumeVersion: 3 }; + }, }, }, }, @@ -1041,7 +1063,7 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, getAwaitingAgentContinueRequest, scheduleAgentContinue, }, @@ -1134,7 +1156,7 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, }, }, }); @@ -1204,7 +1226,7 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, scheduleAgentContinue, }, }, @@ -1284,7 +1306,7 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply: vi.fn(), + agentRunner: { run: vi.fn() }, scheduleAgentContinue, }, }, @@ -1341,7 +1363,7 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply: vi.fn(), + agentRunner: { run: vi.fn() }, scheduleAgentContinue, }, }, @@ -1386,8 +1408,10 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => { - throw new Error("transient turn failure"); + agentRunner: { + run: async () => { + throw new Error("transient turn failure"); + }, }, }, }, @@ -1419,9 +1443,11 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply: async (_input, context) => { - await context.onInputCommitted?.(); - throw new Error("post-ack turn failure"); + agentRunner: { + run: async (_input, context) => { + await context.onInputCommitted?.(); + throw new Error("post-ack turn failure"); + }, }, }, }, @@ -1458,8 +1484,10 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => { - throw new Error("persistent turn failure"); + agentRunner: { + run: async () => { + throw new Error("persistent turn failure"); + }, }, }, }, @@ -1518,7 +1546,7 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, }, }, }); @@ -1573,7 +1601,7 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, getAwaitingAgentContinueRequest, scheduleAgentContinue, }, @@ -1624,7 +1652,7 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, getAwaitingAgentContinueRequest, scheduleAgentContinue, }, @@ -1676,7 +1704,7 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, getAwaitingAgentContinueRequest, scheduleAgentContinue, }, @@ -1736,7 +1764,7 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, getAwaitingAgentContinueRequest, scheduleAgentContinue, }, @@ -1771,20 +1799,22 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - await context?.onTextDelta?.("Partial output..."); - return completedAgentRun({ - text: "Partial output...", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "provider_error" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async (_prompt, context) => { + await context?.onTextDelta?.("Partial output..."); + return completedAgentRun({ + text: "Partial output...", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "provider_error" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, @@ -1821,22 +1851,24 @@ describe("bot handlers (integration)", () => { slackAdapter: fakeAdapter, services: { replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - await context?.onStatus?.( - makeAssistantStatus("reading", "channel messages"), - ); - return completedAgentRun({ - text: "Done.", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async (_prompt, context) => { + await context?.onStatus?.( + makeAssistantStatus("reading", "channel messages"), + ); + return completedAgentRun({ + text: "Done.", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, @@ -1892,20 +1924,22 @@ describe("bot handlers (integration)", () => { completeText: async () => ({ text: "Status thread" }) as never, }, replyExecutor: { - generateAssistantReply: async () => { - replyStarted = true; - return completedAgentRun({ - text: "Still replied while status was pending.", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async () => { + replyStarted = true; + return completedAgentRun({ + text: "Still replied while status was pending.", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, @@ -1975,20 +2009,22 @@ describe("bot handlers (integration)", () => { completeText: async () => ({ text: "Status thread" }) as never, }, replyExecutor: { - generateAssistantReply: async () => { - replyStarted = true; - return completedAgentRun({ - text: "Reply lands after the pending status is drained.", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async () => { + replyStarted = true; + return completedAgentRun({ + text: "Reply lands after the pending status is drained.", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, @@ -2038,19 +2074,21 @@ describe("bot handlers (integration)", () => { }) as any, }, replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: "Here is how to debug memory leaks.", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "Here is how to debug memory leaks.", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }), + }, }, }, }); @@ -2099,19 +2137,21 @@ describe("bot handlers (integration)", () => { }, }, replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: "Here is the updated answer.", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "Here is the updated answer.", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }), + }, }, }, }); @@ -2159,19 +2199,21 @@ describe("bot handlers (integration)", () => { }) as any, }, replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: "Today is April 16, 2026.", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "Today is April 16, 2026.", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }), + }, }, }, }); @@ -2230,19 +2272,21 @@ describe("bot handlers (integration)", () => { }), }, replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: "Today is April 16, 2026.", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "Today is April 16, 2026.", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }), + }, }, }, }); @@ -2296,32 +2340,31 @@ describe("bot handlers (integration)", () => { }) as any, }, replyExecutor: { - generateAssistantReply: async ( - _text: string, - context?: ReplyRequestContext, - ) => { - await vi.waitFor(() => { - expect( - fakeAdapter.titleCalls.some( - (call) => call.title === "Today's Date", - ), - ).toBe(true); - }); - await context?.onArtifactStateUpdated?.({ - lastCanvasId: "F_CANVAS", - }); - return completedAgentRun({ - text: "Today is April 16, 2026.", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async (_text: string, context?: ReplyRequestContext) => { + await vi.waitFor(() => { + expect( + fakeAdapter.titleCalls.some( + (call) => call.title === "Today's Date", + ), + ).toBe(true); + }); + await context?.onArtifactStateUpdated?.({ + lastCanvasId: "F_CANVAS", + }); + return completedAgentRun({ + text: "Today is April 16, 2026.", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, @@ -2362,20 +2405,22 @@ describe("bot handlers (integration)", () => { }) as any, }, replyExecutor: { - generateAssistantReply: async () => { - turnCount += 1; - return completedAgentRun({ - text: `reply-${turnCount}`, - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async () => { + turnCount += 1; + return completedAgentRun({ + text: `reply-${turnCount}`, + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, @@ -2440,19 +2485,21 @@ describe("bot handlers (integration)", () => { }) as any, }, replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: "This reply should still succeed.", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "This reply should still succeed.", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }), + }, }, }, }); @@ -2501,19 +2548,21 @@ describe("bot handlers (integration)", () => { }, }, replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: "Reply still succeeds.", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "Reply still succeeds.", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }), + }, }, }, }); @@ -2549,20 +2598,22 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - capturedContexts.push(context?.conversationContext); - return completedAgentRun({ - text: "First reply.", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async (_prompt, context) => { + capturedContexts.push(context?.conversationContext); + return completedAgentRun({ + text: "First reply.", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, @@ -2590,20 +2641,22 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - capturedContexts.push(context?.conversationContext); - return completedAgentRun({ - text: "Follow-up reply.", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async (_prompt, context) => { + capturedContexts.push(context?.conversationContext); + return completedAgentRun({ + text: "Follow-up reply.", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, @@ -2660,20 +2713,22 @@ describe("bot handlers (integration)", () => { }) as any, }, replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - capturedContexts.push(context?.conversationContext); - return completedAgentRun({ - text: "Responding to first message only.", - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async (_prompt, context) => { + capturedContexts.push(context?.conversationContext); + return completedAgentRun({ + text: "Responding to first message only.", + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, @@ -2718,20 +2773,22 @@ describe("bot handlers (integration)", () => { const { slackRuntime } = createRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => { - turnCount += 1; - return completedAgentRun({ - text: `reply-${turnCount}`, - diagnostics: { - assistantMessageCount: 1, - modelId: "test-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async () => { + turnCount += 1; + return completedAgentRun({ + text: `reply-${turnCount}`, + diagnostics: { + assistantMessageCount: 1, + modelId: "test-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, diff --git a/packages/junior/tests/integration/slack/bot-image-hydration.test.ts b/packages/junior/tests/integration/slack/bot-image-hydration.test.ts index a8cffd4d6..e9e463aa6 100644 --- a/packages/junior/tests/integration/slack/bot-image-hydration.test.ts +++ b/packages/junior/tests/integration/slack/bot-image-hydration.test.ts @@ -83,7 +83,7 @@ describe("bot image hydration", () => { listThreadReplies: listThreadRepliesMock, }, replyExecutor: { - generateAssistantReply: async () => makeSuccessOutcome(), + agentRunner: { run: async () => makeSuccessOutcome() }, }, }, }, @@ -182,7 +182,7 @@ describe("bot image hydration", () => { listThreadReplies: listThreadRepliesMock, }, replyExecutor: { - generateAssistantReply: async () => makeSuccessOutcome(), + agentRunner: { run: async () => makeSuccessOutcome() }, }, }, }); @@ -284,7 +284,7 @@ describe("bot image hydration", () => { listThreadReplies: listThreadRepliesMock, }, replyExecutor: { - generateAssistantReply: async () => makeSuccessOutcome(), + agentRunner: { run: async () => makeSuccessOutcome() }, }, }, }); @@ -366,7 +366,7 @@ describe("bot image hydration", () => { completeText: completeTextMock, }, replyExecutor: { - generateAssistantReply: async () => makeSuccessOutcome(), + agentRunner: { run: async () => makeSuccessOutcome() }, }, }, }, @@ -476,7 +476,7 @@ describe("bot image hydration", () => { completeText: completeTextMock, }, replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, }, }, }, @@ -631,7 +631,7 @@ describe("bot image hydration", () => { completeText: completeTextMock, }, replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, }, }, }, @@ -790,7 +790,7 @@ describe("bot image hydration", () => { completeText: completeTextMock, }, replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, }, }, }, @@ -913,7 +913,7 @@ describe("bot image hydration", () => { completeText: completeTextMock, }, replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, }, }, }, @@ -1015,11 +1015,13 @@ describe("bot image hydration", () => { listThreadReplies: listThreadRepliesMock.mockResolvedValue([]), }, replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - ...makeSuccessReply("Here is your image"), - files: [generatedFile], - }), + agentRunner: { + run: async () => + completedAgentRun({ + ...makeSuccessReply("Here is your image"), + files: [generatedFile], + }), + }, }, }, }); @@ -1071,17 +1073,19 @@ describe("bot image hydration", () => { listThreadReplies: listThreadRepliesMock.mockResolvedValue([]), }, replyExecutor: { - generateAssistantReply: async (_text: string, _context: any) => { - return completedAgentRun({ - ...makeSuccessReply("finalized content"), - files: [ - { - data: Buffer.from("fake-png"), - filename: "generated.png", - mimeType: "image/png", - }, - ], - }); + agentRunner: { + run: async (_text: string, _context: any) => { + return completedAgentRun({ + ...makeSuccessReply("finalized content"), + files: [ + { + data: Buffer.from("fake-png"), + filename: "generated.png", + mimeType: "image/png", + }, + ], + }); + }, }, }, }, diff --git a/packages/junior/tests/integration/slack/canvas-failure-recovery-behavior.test.ts b/packages/junior/tests/integration/slack/canvas-failure-recovery-behavior.test.ts index 512a7af65..a00b36fb9 100644 --- a/packages/junior/tests/integration/slack/canvas-failure-recovery-behavior.test.ts +++ b/packages/junior/tests/integration/slack/canvas-failure-recovery-behavior.test.ts @@ -42,7 +42,7 @@ describe("Slack behavior: canvas failure recovery", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, }, }, }); @@ -80,7 +80,7 @@ describe("Slack behavior: canvas failure recovery", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, }, }, }); diff --git a/packages/junior/tests/integration/slack/conversation-turn-steering-behavior.test.ts b/packages/junior/tests/integration/slack/conversation-turn-steering-behavior.test.ts index 6ea63a329..a27f19221 100644 --- a/packages/junior/tests/integration/slack/conversation-turn-steering-behavior.test.ts +++ b/packages/junior/tests/integration/slack/conversation-turn-steering-behavior.test.ts @@ -13,7 +13,7 @@ import { slackApiOutbox } from "../../fixtures/slack-api-outbox"; import { resetSlackApiMockState } from "../../msw/handlers/slack-api"; import { createSlackRuntime } from "@/chat/app/factory"; import type { JuniorRuntimeServiceOverrides } from "@/chat/app/services"; -import type { ReplyExecutorServices } from "@/chat/runtime/reply-executor"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import type { ReplySteeringMessage } from "@/chat/respond"; import { createJuniorSlackAdapter } from "@/chat/slack/adapter"; import { disconnectStateAdapter, getStateAdapter } from "@/chat/state/adapter"; @@ -103,7 +103,7 @@ function completeObjectWithDecision( function createTurnHarness(args: { completeObject?: CompleteObjectOverride; - generateAssistantReply: ReplyExecutorServices["generateAssistantReply"]; + agentRunner: AgentRunner; services?: Parameters[0]["services"]; state: StateAdapter; }) { @@ -119,7 +119,7 @@ function createTurnHarness(args: { ...(args.services ?? {}), replyExecutor: { ...(args.services?.replyExecutor ?? {}), - generateAssistantReply: args.generateAssistantReply, + agentRunner: args.agentRunner, }, subscribedReplyPolicy: { completeObject: @@ -179,11 +179,13 @@ describe("Slack behavior: durable turn steering", () => { it("does not enqueue duplicate Slack event retries for a persisted message", async () => { const state = getStateAdapter(); const { conversationId, queue, services } = createTurnHarness({ - generateAssistantReply: async () => - completedAgentRun({ - text: "not used", - diagnostics: makeDiagnostics(), - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "not used", + diagnostics: makeDiagnostics(), + }), + }, state, }); const event = makeMessageEvent({ @@ -245,35 +247,37 @@ describe("Slack behavior: durable turn steering", () => { steeringTexts: string[]; }> = []; const state = getStateAdapter(); - const generateAssistantReply: ReplyExecutorServices["generateAssistantReply"] = - async (prompt, context) => { - await context?.onInputCommitted?.(); - if (!blockingCallReleased) { - agentEntered.resolve(); - await releaseAgent.promise; - blockingCallReleased = true; - } - - const steeringMessages: ReplySteeringMessage[] = []; - const drained = await context?.drainSteeringMessages?.( - async (messages) => { - steeringMessages.push(...messages); - }, - ); - if (steeringMessages.length === 0 && drained) { - steeringMessages.push(...drained); - } - - const steeringTexts = steeringMessages.map((message) => message.text); - agentCalls.push({ prompt, steeringTexts }); - return completedAgentRun({ - text: [ - `Handled initial: ${prompt}`, - `Steered: ${steeringTexts.join(" | ")}`, - ].join("\n"), - diagnostics: makeDiagnostics(), - }); - }; + const generateAssistantReply: AgentRunner["run"] = async ( + prompt, + context, + ) => { + await context?.onInputCommitted?.(); + if (!blockingCallReleased) { + agentEntered.resolve(); + await releaseAgent.promise; + blockingCallReleased = true; + } + + const steeringMessages: ReplySteeringMessage[] = []; + const drained = await context?.drainSteeringMessages?.( + async (messages) => { + steeringMessages.push(...messages); + }, + ); + if (steeringMessages.length === 0 && drained) { + steeringMessages.push(...drained); + } + + const steeringTexts = steeringMessages.map((message) => message.text); + agentCalls.push({ prompt, steeringTexts }); + return completedAgentRun({ + text: [ + `Handled initial: ${prompt}`, + `Steered: ${steeringTexts.join(" | ")}`, + ].join("\n"), + diagnostics: makeDiagnostics(), + }); + }; const { conversationId, queue, runNextQueuedWork, services } = createTurnHarness({ completeObject: completeObjectWithDecision((prompt) => @@ -291,7 +295,7 @@ describe("Slack behavior: durable turn steering", () => { reason: "active steering follow-up", }, ), - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, state, }); @@ -444,13 +448,15 @@ describe("Slack behavior: durable turn steering", () => { confidence: 1, reason: "side conversation", })), - generateAssistantReply: async (prompt, context) => { - replyCalls.push(prompt); - await context?.onInputCommitted?.(); - return completedAgentRun({ - text: "Started.", - diagnostics: makeDiagnostics(), - }); + agentRunner: { + run: async (prompt, context) => { + replyCalls.push(prompt); + await context?.onInputCommitted?.(); + return completedAgentRun({ + text: "Started.", + diagnostics: makeDiagnostics(), + }); + }, }, state, }); @@ -504,24 +510,26 @@ describe("Slack behavior: durable turn steering", () => { const releaseAgent = deferred(); const drainedTexts: string[] = []; const state = getStateAdapter(); - const generateAssistantReply: ReplyExecutorServices["generateAssistantReply"] = - async (_prompt, context) => { - await context?.onInputCommitted?.(); - agentEntered.resolve(); - await releaseAgent.promise; - const drained = await context?.drainSteeringMessages?.( - async (messages) => { - drainedTexts.push(...messages.map((message) => message.text)); - }, - ); - if (drainedTexts.length === 0 && drained) { - drainedTexts.push(...drained.map((message) => message.text)); - } - return completedAgentRun({ - text: "Done with the initial request.", - diagnostics: makeDiagnostics(), - }); - }; + const generateAssistantReply: AgentRunner["run"] = async ( + _prompt, + context, + ) => { + await context?.onInputCommitted?.(); + agentEntered.resolve(); + await releaseAgent.promise; + const drained = await context?.drainSteeringMessages?.( + async (messages) => { + drainedTexts.push(...messages.map((message) => message.text)); + }, + ); + if (drainedTexts.length === 0 && drained) { + drainedTexts.push(...drained.map((message) => message.text)); + } + return completedAgentRun({ + text: "Done with the initial request.", + diagnostics: makeDiagnostics(), + }); + }; const { conversationId, runNextQueuedWork, services } = createTurnHarness({ completeObject: completeObjectWithDecision((prompt) => prompt.includes("stop watching") @@ -538,7 +546,7 @@ describe("Slack behavior: durable turn steering", () => { reason: "active steering follow-up", }, ), - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, state, }); @@ -626,14 +634,16 @@ describe("Slack behavior: durable turn steering", () => { it("keeps the mailbox pending when the agent fails before input commit", async () => { const state = getStateAdapter(); - const generateAssistantReply: ReplyExecutorServices["generateAssistantReply"] = - async (_prompt, context) => { - expect(context?.onInputCommitted).toEqual(expect.any(Function)); - throw new Error("agent crashed before input commit"); - }; + const generateAssistantReply: AgentRunner["run"] = async ( + _prompt, + context, + ) => { + expect(context?.onInputCommitted).toEqual(expect.any(Function)); + throw new Error("agent crashed before input commit"); + }; const { conversationId, queue, runNextQueuedWork, services } = createTurnHarness({ - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, state, }); diff --git a/packages/junior/tests/integration/slack/file-delivery-behavior.test.ts b/packages/junior/tests/integration/slack/file-delivery-behavior.test.ts index bfb70ddc3..99e76803e 100644 --- a/packages/junior/tests/integration/slack/file-delivery-behavior.test.ts +++ b/packages/junior/tests/integration/slack/file-delivery-behavior.test.ts @@ -27,26 +27,28 @@ describe("Slack behavior: file delivery", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - await context?.onTextDelta?.("Preview is ready."); - return completedAgentRun({ - text: "Preview is ready.", - deliveryPlan: { - mode: "thread", + agentRunner: { + run: async (_prompt, context) => { + await context?.onTextDelta?.("Preview is ready."); + return completedAgentRun({ + text: "Preview is ready.", + deliveryPlan: { + mode: "thread", - postThreadText: true, - attachFiles: "followup", - }, - diagnostics: { - assistantMessageCount: 1, - modelId: "fake-agent-model", - outcome: "success", - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + postThreadText: true, + attachFiles: "followup", + }, + diagnostics: { + assistantMessageCount: 1, + modelId: "fake-agent-model", + outcome: "success", + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, diff --git a/packages/junior/tests/integration/slack/finalized-reply-behavior.test.ts b/packages/junior/tests/integration/slack/finalized-reply-behavior.test.ts index 2bc142781..b295ba55e 100644 --- a/packages/junior/tests/integration/slack/finalized-reply-behavior.test.ts +++ b/packages/junior/tests/integration/slack/finalized-reply-behavior.test.ts @@ -70,13 +70,15 @@ describe("Slack behavior: finalized thread replies", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - await context?.onTextDelta?.("Hello "); - await context?.onTextDelta?.("world"); - return completedAgentRun({ - text: "Hello world", - diagnostics: makeDiagnostics(), - }); + agentRunner: { + run: async (_prompt, context) => { + await context?.onTextDelta?.("Hello "); + await context?.onTextDelta?.("world"); + return completedAgentRun({ + text: "Hello world", + diagnostics: makeDiagnostics(), + }); + }, }, }, }, @@ -104,14 +106,16 @@ describe("Slack behavior: finalized thread replies", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - await context?.onTextDelta?.("Fetching sources now..."); - await context?.onAssistantMessageStart?.(); - await context?.onTextDelta?.(finalReply); - return completedAgentRun({ - text: finalReply, - diagnostics: makeDiagnostics({ toolCalls: ["webSearch"] }), - }); + agentRunner: { + run: async (_prompt, context) => { + await context?.onTextDelta?.("Fetching sources now..."); + await context?.onAssistantMessageStart?.(); + await context?.onTextDelta?.(finalReply); + return completedAgentRun({ + text: finalReply, + diagnostics: makeDiagnostics({ toolCalls: ["webSearch"] }), + }); + }, }, }, }, @@ -137,12 +141,14 @@ describe("Slack behavior: finalized thread replies", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: "", - files: [{ data: Buffer.from("hello"), filename: "hello.txt" }], - diagnostics: makeDiagnostics(), - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "", + files: [{ data: Buffer.from("hello"), filename: "hello.txt" }], + diagnostics: makeDiagnostics(), + }), + }, }, }, }); @@ -170,17 +176,21 @@ describe("Slack behavior: finalized thread replies", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: "Posted it in channel.", - files: [{ data: Buffer.from("report"), filename: "report.txt" }], - deliveryPlan: { - mode: "channel_only", - postThreadText: false, - attachFiles: "inline", - }, - diagnostics: makeDiagnostics(), - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "Posted it in channel.", + files: [ + { data: Buffer.from("report"), filename: "report.txt" }, + ], + deliveryPlan: { + mode: "channel_only", + postThreadText: false, + attachFiles: "inline", + }, + diagnostics: makeDiagnostics(), + }), + }, }, }, }); @@ -208,16 +218,18 @@ describe("Slack behavior: finalized thread replies", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: "", - deliveryPlan: { - mode: "thread", - postThreadText: true, - attachFiles: "none", - }, - diagnostics: makeDiagnostics(), - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "", + deliveryPlan: { + mode: "thread", + postThreadText: true, + attachFiles: "none", + }, + diagnostics: makeDiagnostics(), + }), + }, }, }, }); @@ -244,14 +256,18 @@ describe("Slack behavior: finalized thread replies", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: "ok", - files: [{ data: Buffer.from("report"), filename: "report.txt" }], - diagnostics: makeDiagnostics({ - toolCalls: ["slackMessageAddReaction"], + agentRunner: { + run: async () => + completedAgentRun({ + text: "ok", + files: [ + { data: Buffer.from("report"), filename: "report.txt" }, + ], + diagnostics: makeDiagnostics({ + toolCalls: ["slackMessageAddReaction"], + }), }), - }), + }, }, }, }); @@ -283,11 +299,13 @@ describe("Slack behavior: finalized thread replies", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: longReply, - diagnostics: makeDiagnostics(), - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: longReply, + diagnostics: makeDiagnostics(), + }), + }, }, }, }); @@ -320,11 +338,13 @@ describe("Slack behavior: finalized thread replies", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: longReply, - diagnostics: makeDiagnostics(), - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: longReply, + diagnostics: makeDiagnostics(), + }), + }, }, }, }); @@ -358,11 +378,13 @@ describe("Slack behavior: finalized thread replies", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => - completedAgentRun({ - text: longReply, - diagnostics: makeDiagnostics({ outcome: "provider_error" }), - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: longReply, + diagnostics: makeDiagnostics({ outcome: "provider_error" }), + }), + }, }, }, }); diff --git a/packages/junior/tests/integration/slack/message-changed-behavior.test.ts b/packages/junior/tests/integration/slack/message-changed-behavior.test.ts index 24ad48d13..44dbf64e0 100644 --- a/packages/junior/tests/integration/slack/message-changed-behavior.test.ts +++ b/packages/junior/tests/integration/slack/message-changed-behavior.test.ts @@ -263,20 +263,22 @@ describe("Slack behavior: message_changed webhook ingress", () => { fullName: "David Cramer", userName: "dcramer", }), - generateAssistantReply: async (_prompt, context) => { - expect(context?.requester).toEqual({ - email: "david@example.com", - fullName: "David Cramer", - platform: "slack", - teamId: TEST_SLACK_TEAM_ID, - userId: "U123", - userName: "dcramer", - }); - await context?.onTextDelta?.("Hello world"); - return completedAgentRun({ - text: "Hello world", - diagnostics: makeDiagnostics(), - }); + agentRunner: { + run: async (_prompt, context) => { + expect(context?.requester).toEqual({ + email: "david@example.com", + fullName: "David Cramer", + platform: "slack", + teamId: TEST_SLACK_TEAM_ID, + userId: "U123", + userName: "dcramer", + }); + await context?.onTextDelta?.("Hello world"); + return completedAgentRun({ + text: "Hello world", + diagnostics: makeDiagnostics(), + }); + }, }, }, }, diff --git a/packages/junior/tests/integration/slack/message-changed-reply-contract.test.ts b/packages/junior/tests/integration/slack/message-changed-reply-contract.test.ts index 600319ede..1a5eac235 100644 --- a/packages/junior/tests/integration/slack/message-changed-reply-contract.test.ts +++ b/packages/junior/tests/integration/slack/message-changed-reply-contract.test.ts @@ -7,7 +7,7 @@ import { slackApiOutbox } from "../../fixtures/slack-api-outbox"; import { createSlackWebhookTestClient } from "../../fixtures/slack/webhook-client"; import { createSlackRuntime } from "@/chat/app/factory"; import { JuniorChat } from "@/chat/ingress/junior-chat"; -import type { ReplyExecutorServices } from "@/chat/runtime/reply-executor"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import { createJuniorSlackAdapter } from "@/chat/slack/adapter"; import { handleChatSdkPlatformWebhook } from "@/handlers/webhooks"; import { completedAgentRun } from "@/chat/runtime/agent-run-outcome"; @@ -63,9 +63,7 @@ function createEditedMentionRequest(args: { }); } -async function createEditedDmBot(args: { - generateAssistantReply: ReplyExecutorServices["generateAssistantReply"]; -}) { +async function createEditedDmBot(args: { agentRunner: AgentRunner }) { const state = createMemoryState(); await state.connect(); const bot = new JuniorChat<{ slack: SlackAdapter }>({ @@ -83,7 +81,7 @@ async function createEditedDmBot(args: { getSlackAdapter: () => bot.getAdapter("slack"), services: { replyExecutor: { - generateAssistantReply: args.generateAssistantReply, + agentRunner: args.agentRunner, }, }, }); @@ -100,12 +98,14 @@ async function createEditedDmBot(args: { describe("Slack contract: edited-message reply delivery", () => { it("posts the finalized reply into the edited DM thread with chat.postMessage", async () => { const bot = await createEditedDmBot({ - generateAssistantReply: async (_prompt, context) => { - await context?.onTextDelta?.("Hello world"); - return completedAgentRun({ - text: "Hello world", - diagnostics: makeDiagnostics(), - }); + agentRunner: { + run: async (_prompt, context) => { + await context?.onTextDelta?.("Hello world"); + return completedAgentRun({ + text: "Hello world", + diagnostics: makeDiagnostics(), + }); + }, }, }); const waitUntil = slackWebhookClient.waitUntil(); @@ -157,11 +157,13 @@ describe("Slack contract: edited-message reply delivery", () => { (_, i) => `line ${i + 1}`, ).join("\n"); const bot = await createEditedDmBot({ - generateAssistantReply: async () => - completedAgentRun({ - text: longReply, - diagnostics: makeDiagnostics(), - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: longReply, + diagnostics: makeDiagnostics(), + }), + }, }); const waitUntil = slackWebhookClient.waitUntil(); diff --git a/packages/junior/tests/integration/slack/message-content-behavior.test.ts b/packages/junior/tests/integration/slack/message-content-behavior.test.ts index a26b78d7d..e402033c3 100644 --- a/packages/junior/tests/integration/slack/message-content-behavior.test.ts +++ b/packages/junior/tests/integration/slack/message-content-behavior.test.ts @@ -61,12 +61,14 @@ describe("Slack behavior: message content", () => { }, }, replyExecutor: { - generateAssistantReply: async (prompt, context) => { - calls.push({ - prompt, - contextConversation: context?.conversationContext, - }); - return completedReply("Summary sent."); + agentRunner: { + run: async (prompt, context) => { + calls.push({ + prompt, + contextConversation: context?.conversationContext, + }); + return completedReply("Summary sent."); + }, }, }, }, @@ -95,9 +97,11 @@ describe("Slack behavior: message content", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async (prompt) => { - calls.push({ prompt }); - return completedReply("Done."); + agentRunner: { + run: async (prompt) => { + calls.push({ prompt }); + return completedReply("Done."); + }, }, }, }, @@ -126,12 +130,14 @@ describe("Slack behavior: message content", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async (prompt, context) => { - calls.push({ - prompt, - contextConversation: context?.conversationContext, - }); - return completedReply("Alert reviewed."); + agentRunner: { + run: async (prompt, context) => { + calls.push({ + prompt, + contextConversation: context?.conversationContext, + }); + return completedReply("Alert reviewed."); + }, }, }, }, @@ -176,9 +182,11 @@ describe("Slack behavior: message content", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => { - replyCalled = true; - return completedReply("Should not happen"); + agentRunner: { + run: async () => { + replyCalled = true; + return completedReply("Should not happen"); + }, }, }, }, @@ -239,28 +247,30 @@ describe("Slack behavior: message content", () => { }, }, replyExecutor: { - generateAssistantReply: async (prompt, context) => { - calls.push({ - prompt, - contextConversation: context?.conversationContext, - piMessages: context?.piMessages, - }); - if ( - calls.length === 1 && - context?.correlation?.conversationId && - context.correlation.turnId - ) { - await upsertAgentTurnSessionRecord({ - conversationId: context.correlation.conversationId, - sessionId: context.correlation.turnId, - sliceId: 1, - state: "completed", - piMessages: storedFirstTurnHistory, + agentRunner: { + run: async (prompt, context) => { + calls.push({ + prompt, + contextConversation: context?.conversationContext, + piMessages: context?.piMessages, }); - } - return completedReply( - calls.length === 1 ? "First response." : "Second response.", - ); + if ( + calls.length === 1 && + context?.correlation?.conversationId && + context.correlation.turnId + ) { + await upsertAgentTurnSessionRecord({ + conversationId: context.correlation.conversationId, + sessionId: context.correlation.turnId, + sliceId: 1, + state: "completed", + piMessages: storedFirstTurnHistory, + }); + } + return completedReply( + calls.length === 1 ? "First response." : "Second response.", + ); + }, }, }, }, @@ -339,13 +349,15 @@ describe("Slack behavior: message content", () => { autoCompactionTriggerTokens: 100, }, replyExecutor: { - generateAssistantReply: async (prompt, context) => { - calls.push({ - prompt, - contextConversation: context?.conversationContext, - piMessages: context?.piMessages, - }); - return completedReply("Done."); + agentRunner: { + run: async (prompt, context) => { + calls.push({ + prompt, + contextConversation: context?.conversationContext, + piMessages: context?.piMessages, + }); + return completedReply("Done."); + }, }, }, }, @@ -451,13 +463,15 @@ describe("Slack behavior: message content", () => { autoCompactionTriggerTokens: 100, }, replyExecutor: { - generateAssistantReply: async (prompt, context) => { - calls.push({ - prompt, - contextConversation: context?.conversationContext, - piMessages: context?.piMessages, - }); - return completedReply("Done."); + agentRunner: { + run: async (prompt, context) => { + calls.push({ + prompt, + contextConversation: context?.conversationContext, + piMessages: context?.piMessages, + }); + return completedReply("Done."); + }, }, }, }, diff --git a/packages/junior/tests/integration/slack/message-im-attachment-contract.test.ts b/packages/junior/tests/integration/slack/message-im-attachment-contract.test.ts index cfe6bd651..9839e14bf 100644 --- a/packages/junior/tests/integration/slack/message-im-attachment-contract.test.ts +++ b/packages/junior/tests/integration/slack/message-im-attachment-contract.test.ts @@ -5,7 +5,7 @@ import { createMemoryState } from "@chat-adapter/state-memory"; import { slackEventsApiEnvelope } from "../../fixtures/slack/factories/events"; import { createSlackWebhookTestClient } from "../../fixtures/slack/webhook-client"; import { mswServer } from "../../msw/server"; -import type { ReplyExecutorServices } from "@/chat/runtime/reply-executor"; +import type { AgentRunner } from "@/chat/runtime/agent-runner"; import { completedAgentRun } from "@/chat/runtime/agent-run-outcome"; const SIGNING_SECRET = "test-signing-secret"; @@ -31,7 +31,7 @@ function makeDiagnostics() { async function createDirectMessageBot(args: { completeText: () => Promise<{ text: string; message: never }>; - generateAssistantReply: ReplyExecutorServices["generateAssistantReply"]; + agentRunner: AgentRunner; }) { const [{ createSlackRuntime }, { JuniorChat }, { createJuniorSlackAdapter }] = await Promise.all([ @@ -57,7 +57,7 @@ async function createDirectMessageBot(args: { completeText: args.completeText, }, replyExecutor: { - generateAssistantReply: args.generateAssistantReply, + agentRunner: args.agentRunner, }, }, }); @@ -103,18 +103,20 @@ describe("Slack contract: message.im attachment ingress", () => { text: "Screenshot shows the current incident chart.", message: {} as never, }), - generateAssistantReply: async (_prompt, context) => { - const attachments = context?.userAttachments ?? []; - capturedAttachmentMediaTypes.push( - attachments.map((attachment) => attachment.mediaType), - ); - capturedAttachmentNames.push( - attachments.map((attachment) => attachment.filename ?? ""), - ); - return completedAgentRun({ - text: "Processed screenshot.", - diagnostics: makeDiagnostics(), - }); + agentRunner: { + run: async (_prompt, context) => { + const attachments = context?.userAttachments ?? []; + capturedAttachmentMediaTypes.push( + attachments.map((attachment) => attachment.mediaType), + ); + capturedAttachmentNames.push( + attachments.map((attachment) => attachment.filename ?? ""), + ); + return completedAgentRun({ + text: "Processed screenshot.", + diagnostics: makeDiagnostics(), + }); + }, }, }); const waitUntil = slackWebhookClient.waitUntil(); diff --git a/packages/junior/tests/integration/slack/new-mention-behavior.test.ts b/packages/junior/tests/integration/slack/new-mention-behavior.test.ts index 59682a3e5..f4d5a34b9 100644 --- a/packages/junior/tests/integration/slack/new-mention-behavior.test.ts +++ b/packages/junior/tests/integration/slack/new-mention-behavior.test.ts @@ -53,11 +53,13 @@ describe("Slack behavior: new mention", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async (prompt) => { - fakeReplyCalls.push({ prompt }); - return completedReply( - "Acknowledged. Rollback is complete and error rates are stable.", - ); + agentRunner: { + run: async (prompt) => { + fakeReplyCalls.push({ prompt }); + return completedReply( + "Acknowledged. Rollback is complete and error rates are stable.", + ); + }, }, }, }, @@ -94,9 +96,11 @@ describe("Slack behavior: new mention", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async (prompt) => { - fakeReplyCalls.push({ prompt }); - return completedReply("Handled both updates."); + agentRunner: { + run: async (prompt) => { + fakeReplyCalls.push({ prompt }); + return completedReply("Handled both updates."); + }, }, }, }, @@ -162,17 +166,19 @@ describe("Slack behavior: new mention", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async (prompt, context) => { - const attachments = context?.userAttachments ?? []; - fakeReplyCalls.push({ - prompt, - inboundAttachmentCount: context?.inboundAttachmentCount, - filenames: attachments.map( - (attachment) => attachment.filename ?? "", - ), - attachmentText: attachments[0]?.data?.toString("utf8"), - }); - return completedReply("Handled queued attachment."); + agentRunner: { + run: async (prompt, context) => { + const attachments = context?.userAttachments ?? []; + fakeReplyCalls.push({ + prompt, + inboundAttachmentCount: context?.inboundAttachmentCount, + filenames: attachments.map( + (attachment) => attachment.filename ?? "", + ), + attachmentText: attachments[0]?.data?.toString("utf8"), + }); + return completedReply("Handled queued attachment."); + }, }, }, }, @@ -230,9 +236,11 @@ describe("Slack behavior: new mention", () => { slackAdapter, services: { replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - await context?.onStatus?.(makeAssistantStatus("running", "bash")); - return completedReply("Done.", ["bash"]); + agentRunner: { + run: async (_prompt, context) => { + await context?.onStatus?.(makeAssistantStatus("running", "bash")); + return completedReply("Done.", ["bash"]); + }, }, }, }, @@ -267,8 +275,10 @@ describe("Slack behavior: new mention", () => { slackAdapter, services: { replyExecutor: { - generateAssistantReply: async () => { - throw new Error("model exploded"); + agentRunner: { + run: async () => { + throw new Error("model exploded"); + }, }, }, }, @@ -301,20 +311,22 @@ describe("Slack behavior: new mention", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => { - return completedAgentRun({ - text: "Posted in channel.", - deliveryMode: "channel_only" as const, - diagnostics: { - assistantMessageCount: 1, - modelId: "fake-agent-model", - outcome: "success" as const, - toolCalls: ["slackChannelPostMessage"], - toolErrorCount: 0, - toolResultCount: 1, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async () => { + return completedAgentRun({ + text: "Posted in channel.", + deliveryMode: "channel_only" as const, + diagnostics: { + assistantMessageCount: 1, + modelId: "fake-agent-model", + outcome: "success" as const, + toolCalls: ["slackChannelPostMessage"], + toolErrorCount: 0, + toolResultCount: 1, + usedPrimaryText: true, + }, + }); + }, }, }, }, diff --git a/packages/junior/tests/integration/slack/processing-reaction-behavior.test.ts b/packages/junior/tests/integration/slack/processing-reaction-behavior.test.ts index 5f7af1666..e07e35ae3 100644 --- a/packages/junior/tests/integration/slack/processing-reaction-behavior.test.ts +++ b/packages/junior/tests/integration/slack/processing-reaction-behavior.test.ts @@ -35,13 +35,15 @@ describe("Slack behavior: processing reaction", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => { - expect(slackApiOutbox.reactionAdds()).toHaveLength(1); - expect(slackApiOutbox.reactionRemovals()).toHaveLength(0); - return completedAgentRun({ - text: "Done.", - diagnostics: successDiagnostics(), - }); + agentRunner: { + run: async () => { + expect(slackApiOutbox.reactionAdds()).toHaveLength(1); + expect(slackApiOutbox.reactionRemovals()).toHaveLength(0); + return completedAgentRun({ + text: "Done.", + diagnostics: successDiagnostics(), + }); + }, }, }, }, @@ -93,8 +95,10 @@ describe("Slack behavior: processing reaction", () => { }, }, replyExecutor: { - generateAssistantReply: async () => { - throw new Error("assistant should not run for skipped message"); + agentRunner: { + run: async () => { + throw new Error("assistant should not run for skipped message"); + }, }, }, }, @@ -143,13 +147,15 @@ describe("Slack behavior: processing reaction", () => { }, }, replyExecutor: { - generateAssistantReply: async () => { - expect(slackApiOutbox.reactionAdds()).toHaveLength(1); - expect(slackApiOutbox.reactionRemovals()).toHaveLength(0); - return completedAgentRun({ - text: "Done.", - diagnostics: successDiagnostics(), - }); + agentRunner: { + run: async () => { + expect(slackApiOutbox.reactionAdds()).toHaveLength(1); + expect(slackApiOutbox.reactionRemovals()).toHaveLength(0); + return completedAgentRun({ + text: "Done.", + diagnostics: successDiagnostics(), + }); + }, }, }, }, @@ -187,13 +193,15 @@ describe("Slack behavior: processing reaction", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async () => { - expect(slackApiOutbox.reactionAdds()).toHaveLength(0); - expect(slackApiOutbox.reactionRemovals()).toHaveLength(0); - return completedAgentRun({ - text: "Done.", - diagnostics: successDiagnostics(), - }); + agentRunner: { + run: async () => { + expect(slackApiOutbox.reactionAdds()).toHaveLength(0); + expect(slackApiOutbox.reactionRemovals()).toHaveLength(0); + return completedAgentRun({ + text: "Done.", + diagnostics: successDiagnostics(), + }); + }, }, }, }, @@ -238,15 +246,17 @@ describe("Slack behavior: processing reaction", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - context?.onToolInvocation?.({ - toolName: "slackMessageAddReaction", - params: { emoji: ":eyes:" }, - }); - return completedAgentRun({ - text: "Done.", - diagnostics: successDiagnostics(["slackMessageAddReaction"]), - }); + agentRunner: { + run: async (_prompt, context) => { + context?.onToolInvocation?.({ + toolName: "slackMessageAddReaction", + params: { emoji: ":eyes:" }, + }); + return completedAgentRun({ + text: "Done.", + diagnostics: successDiagnostics(["slackMessageAddReaction"]), + }); + }, }, }, }, diff --git a/packages/junior/tests/integration/slack/provider-default-config-behavior.test.ts b/packages/junior/tests/integration/slack/provider-default-config-behavior.test.ts index 94068d5a1..53cf49e4e 100644 --- a/packages/junior/tests/integration/slack/provider-default-config-behavior.test.ts +++ b/packages/junior/tests/integration/slack/provider-default-config-behavior.test.ts @@ -26,7 +26,7 @@ describe("Slack behavior: provider default configuration", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, }, }, }); @@ -87,7 +87,7 @@ describe("Slack behavior: provider default configuration", () => { const { slackRuntime } = createTestChatRuntime({ services: { replyExecutor: { - generateAssistantReply, + agentRunner: { run: generateAssistantReply }, }, }, }); diff --git a/packages/junior/tests/integration/slack/subscribed-message-behavior.test.ts b/packages/junior/tests/integration/slack/subscribed-message-behavior.test.ts index 83d21d959..84d33551d 100644 --- a/packages/junior/tests/integration/slack/subscribed-message-behavior.test.ts +++ b/packages/junior/tests/integration/slack/subscribed-message-behavior.test.ts @@ -79,10 +79,12 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async () => { - throw new Error( - "generateAssistantReply should not run when classifier skips reply", - ); + agentRunner: { + run: async () => { + throw new Error( + "generateAssistantReply should not run when classifier skips reply", + ); + }, }, }, }, @@ -118,8 +120,10 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async () => { - throw new Error("generateAssistantReply should not run"); + agentRunner: { + run: async () => { + throw new Error("generateAssistantReply should not run"); + }, }, }, }, @@ -155,9 +159,11 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async (_prompt, context) => { - replyContexts.push(context); - return completedReply("I checked the subscribed PR event."); + agentRunner: { + run: async (_prompt, context) => { + replyContexts.push(context); + return completedReply("I checked the subscribed PR event."); + }, }, }, }, @@ -213,11 +219,13 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async () => { - return { - status: "awaiting_auth", - providerDisplayName: "GitHub", - }; + agentRunner: { + run: async () => { + return { + status: "awaiting_auth", + providerDisplayName: "GitHub", + }; + }, }, }, }, @@ -276,11 +284,13 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async (prompt) => { - replyCalls.push(prompt); - return completedReply( - "Action item captured: monitor dashboards for 30 minutes.", - ); + agentRunner: { + run: async (prompt) => { + replyCalls.push(prompt); + return completedReply( + "Action item captured: monitor dashboards for 30 minutes.", + ); + }, }, }, }, @@ -325,9 +335,11 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async (prompt) => { - replyCalls.push(prompt); - return completedReply("Yes. Shipping status is green."); + agentRunner: { + run: async (prompt) => { + replyCalls.push(prompt); + return completedReply("Yes. Shipping status is green."); + }, }, }, }, @@ -367,9 +379,11 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async (prompt) => { - replyCalls.push(prompt); - return completedReply("Handled queued subscribed turn."); + agentRunner: { + run: async (prompt) => { + replyCalls.push(prompt); + return completedReply("Handled queued subscribed turn."); + }, }, }, }, @@ -432,13 +446,15 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async (prompt) => { - replyCalls.push(prompt); - return completedReply( - replyCalls.length === 1 - ? "I can help with this thread." - : "I'm back because you mentioned me again.", - ); + agentRunner: { + run: async (prompt) => { + replyCalls.push(prompt); + return completedReply( + replyCalls.length === 1 + ? "I can help with this thread." + : "I'm back because you mentioned me again.", + ); + }, }, }, }, @@ -513,9 +529,11 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async () => { - replyCalled = true; - return completedReply("This should never be posted."); + agentRunner: { + run: async () => { + replyCalled = true; + return completedReply("This should never be posted."); + }, }, }, }, @@ -559,9 +577,11 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async () => { - replyCalled = true; - return completedReply("This should never be posted."); + agentRunner: { + run: async () => { + replyCalled = true; + return completedReply("This should never be posted."); + }, }, }, }, @@ -611,9 +631,11 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async () => { - replyCalled = true; - return completedReply("This should never be posted."); + agentRunner: { + run: async () => { + replyCalled = true; + return completedReply("This should never be posted."); + }, }, }, }, @@ -665,9 +687,11 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async () => { - replyCalled = true; - return completedReply("This should never be posted."); + agentRunner: { + run: async () => { + replyCalled = true; + return completedReply("This should never be posted."); + }, }, }, }, @@ -717,9 +741,11 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async () => { - replyCalled = true; - return completedReply("This should never be posted."); + agentRunner: { + run: async () => { + replyCalled = true; + return completedReply("This should never be posted."); + }, }, }, }, @@ -776,9 +802,11 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async () => { - replyCalled = true; - return completedReply("This should never be posted."); + agentRunner: { + run: async () => { + replyCalled = true; + return completedReply("This should never be posted."); + }, }, }, }, @@ -836,9 +864,11 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async () => { - replyCalled = true; - return completedReply("This should never be posted."); + agentRunner: { + run: async () => { + replyCalled = true; + return completedReply("This should never be posted."); + }, }, }, }, @@ -906,9 +936,11 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async (prompt) => { - replyCalls.push(prompt); - return completedReply("You asked for the budget by Friday."); + agentRunner: { + run: async (prompt) => { + replyCalls.push(prompt); + return completedReply("You asked for the budget by Friday."); + }, }, }, }, @@ -965,13 +997,15 @@ describe("Slack behavior: subscribed messages", () => { }, }, replyExecutor: { - generateAssistantReply: async (prompt) => { - replyCalls.push(prompt); - return completedReply( - replyCalls.length === 1 - ? "The deploy changed billing, auth, and the API gateway." - : "The three services were billing, auth, and the API gateway.", - ); + agentRunner: { + run: async (prompt) => { + replyCalls.push(prompt); + return completedReply( + replyCalls.length === 1 + ? "The deploy changed billing, auth, and the API gateway." + : "The three services were billing, auth, and the API gateway.", + ); + }, }, }, }, diff --git a/packages/junior/tests/integration/slack/thread-continuity-behavior.test.ts b/packages/junior/tests/integration/slack/thread-continuity-behavior.test.ts index 7e868d144..003ac6a91 100644 --- a/packages/junior/tests/integration/slack/thread-continuity-behavior.test.ts +++ b/packages/junior/tests/integration/slack/thread-continuity-behavior.test.ts @@ -45,21 +45,24 @@ describe("Slack behavior: thread continuity", () => { }, }, replyExecutor: { - generateAssistantReply: async (prompt) => { - prompts.push(prompt); - return completedAgentRun({ - text: - scriptedReplies[prompts.length - 1] ?? "Unexpected extra reply", - diagnostics: { - assistantMessageCount: 1, - modelId: "fake-agent-model", - outcome: "success", - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }); + agentRunner: { + run: async (prompt) => { + prompts.push(prompt); + return completedAgentRun({ + text: + scriptedReplies[prompts.length - 1] ?? + "Unexpected extra reply", + diagnostics: { + assistantMessageCount: 1, + modelId: "fake-agent-model", + outcome: "success", + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }); + }, }, }, }, diff --git a/packages/junior/tests/unit/handlers/mcp-oauth-callback.test.ts b/packages/junior/tests/unit/handlers/mcp-oauth-callback.test.ts index 6fdcff99d..aaa7b7429 100644 --- a/packages/junior/tests/unit/handlers/mcp-oauth-callback.test.ts +++ b/packages/junior/tests/unit/handlers/mcp-oauth-callback.test.ts @@ -13,6 +13,7 @@ import { createWaitUntilCollector, type WaitUntilCollector, } from "../../fixtures/wait-until"; +import { neverRunAgentRunner } from "../../fixtures/agent-runner"; let waitUntil: WaitUntilCollector; @@ -20,6 +21,8 @@ function makeRequest(url: string): Request { return new Request(url, { method: "GET" }); } +const testAgentRunner = neverRunAgentRunner(); + describe("mcp oauth callback handler", () => { beforeEach(() => { finalizeMcpAuthorizationMock.mockReset(); @@ -35,6 +38,7 @@ describe("mcp oauth callback handler", () => { makeRequest("https://example.com/api/oauth/callback/mcp/demo?code=abc"), "demo", waitUntil.fn, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(400); @@ -50,6 +54,7 @@ describe("mcp oauth callback handler", () => { ), "demo", waitUntil.fn, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(400); @@ -70,6 +75,7 @@ describe("mcp oauth callback handler", () => { ), "demo", waitUntil.fn, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(500); diff --git a/packages/junior/tests/unit/handlers/oauth-callback.test.ts b/packages/junior/tests/unit/handlers/oauth-callback.test.ts index 4f49b9229..0a60f8b58 100644 --- a/packages/junior/tests/unit/handlers/oauth-callback.test.ts +++ b/packages/junior/tests/unit/handlers/oauth-callback.test.ts @@ -111,6 +111,7 @@ import { createUserTokenStore } from "@/chat/capabilities/factory"; import { disconnectStateAdapter, getStateAdapter } from "@/chat/state/adapter"; import { GET } from "@/handlers/oauth-callback"; import type { WaitUntilFn } from "@/handlers/types"; +import { neverRunAgentRunner } from "../../fixtures/agent-runner"; const ORIGINAL_ENV = { ...process.env }; @@ -118,6 +119,8 @@ const testWaitUntil: WaitUntilFn = (task) => { waitUntilCallbacks.push(typeof task === "function" ? task : () => task); }; +const testAgentRunner = neverRunAgentRunner(); + beforeEach(async () => { process.env.JUNIOR_STATE_ADAPTER = "memory"; await disconnectStateAdapter(); @@ -249,6 +252,7 @@ describe("oauth callback handler", () => { ), "unknown", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(404); @@ -262,6 +266,7 @@ describe("oauth callback handler", () => { makeRequest("https://example.com/api/oauth/callback/sentry"), "sentry", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(400); @@ -277,6 +282,7 @@ describe("oauth callback handler", () => { ), "sentry", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(400); @@ -301,6 +307,7 @@ describe("oauth callback handler", () => { ), "sentry", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(400); @@ -329,6 +336,7 @@ describe("oauth callback handler", () => { ), "sentry", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(await getStoredState(stateKey)).toBeFalsy(); @@ -349,6 +357,7 @@ describe("oauth callback handler", () => { ), "sentry", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(500); @@ -379,6 +388,7 @@ describe("oauth callback handler", () => { ), "sentry", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(200); @@ -416,6 +426,7 @@ describe("oauth callback handler", () => { ), "example", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(200); @@ -473,6 +484,7 @@ describe("oauth callback handler", () => { ), "github", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(200); @@ -524,6 +536,7 @@ describe("oauth callback handler", () => { ), "sentry", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(400); @@ -549,6 +562,7 @@ describe("oauth callback handler", () => { ), "sentry", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(500); @@ -573,6 +587,7 @@ describe("oauth callback handler", () => { ), "sentry", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(500); @@ -595,6 +610,7 @@ describe("oauth callback handler", () => { ), "sentry", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(400); @@ -615,6 +631,7 @@ describe("oauth callback handler", () => { ), "sentry", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(400); @@ -630,6 +647,7 @@ describe("oauth callback handler", () => { ), "sentry", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(400); @@ -645,6 +663,7 @@ describe("oauth callback handler", () => { ), "sentry", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(400); @@ -683,6 +702,7 @@ describe("oauth callback handler", () => { ), "sentry", testWaitUntil, + { agentRunner: testAgentRunner }, ); expect(response.status).toBe(200); diff --git a/packages/junior/tests/unit/handlers/oauth-resume.test.ts b/packages/junior/tests/unit/handlers/oauth-resume.test.ts index 7577acfb5..f06473763 100644 --- a/packages/junior/tests/unit/handlers/oauth-resume.test.ts +++ b/packages/junior/tests/unit/handlers/oauth-resume.test.ts @@ -120,7 +120,7 @@ describe("resumeAuthorizedRequest", () => { source: testSlackSource("1700000000.0001"), requester: { platform: "slack", teamId: "T-test", userId: "U-test" }, }, - generateReply: () => new Promise(() => {}), + agentRunner: { run: () => new Promise(() => {}) }, replyTimeoutMs: 10, onFailure, }); @@ -163,8 +163,10 @@ describe("resumeAuthorizedRequest", () => { source: testSlackSource("1700000000.0004"), requester: { platform: "slack", teamId: "T-test", userId: "U-test" }, }, - generateReply: async () => { - throw new Error("resume failed"); + agentRunner: { + run: async () => { + throw new Error("resume failed"); + }, }, onFailure, }); @@ -204,19 +206,21 @@ describe("resumeAuthorizedRequest", () => { source: testSlackSource("1700000000.0005"), requester: { platform: "slack", teamId: "T-test", userId: "U-test" }, }, - generateReply: async () => - completedAgentRun({ - text: "Final resumed answer", - diagnostics: { - assistantMessageCount: 1, - modelId: "fake-agent-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "Final resumed answer", + diagnostics: { + assistantMessageCount: 1, + modelId: "fake-agent-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }), + }, onSuccess: async () => { throw new Error("state write failed"); }, @@ -262,19 +266,21 @@ describe("resumeAuthorizedRequest", () => { source: testSlackSource("1700000000.0006"), requester: { platform: "slack", teamId: "T-test", userId: "U-test" }, }, - generateReply: async () => - completedAgentRun({ - text: "Final resumed answer", - diagnostics: { - assistantMessageCount: 1, - modelId: "fake-agent-model", - outcome: "success" as const, - toolCalls: [], - toolErrorCount: 0, - toolResultCount: 0, - usedPrimaryText: true, - }, - }), + agentRunner: { + run: async () => + completedAgentRun({ + text: "Final resumed answer", + diagnostics: { + assistantMessageCount: 1, + modelId: "fake-agent-model", + outcome: "success" as const, + toolCalls: [], + toolErrorCount: 0, + toolResultCount: 0, + usedPrimaryText: true, + }, + }), + }, scheduleSessionCompletedPluginTasks, }); @@ -310,10 +316,12 @@ describe("resumeAuthorizedRequest", () => { source: testSlackSource("1700000000.0002"), requester: { platform: "slack", teamId: "T-test", userId: "U-test" }, }, - generateReply: async () => ({ - status: "suspended" as const, - resumeVersion: 3, - }), + agentRunner: { + run: async () => ({ + status: "suspended" as const, + resumeVersion: 3, + }), + }, onTimeoutPause, }); @@ -336,10 +344,12 @@ describe("resumeAuthorizedRequest", () => { source: testSlackSource("1700000000.0003"), requester: { platform: "slack", teamId: "T-test", userId: "U-test" }, }, - generateReply: async () => ({ - status: "suspended" as const, - resumeVersion: 3, - }), + agentRunner: { + run: async () => ({ + status: "suspended" as const, + resumeVersion: 3, + }), + }, onTimeoutPause: async () => { throw new Error("continuation scheduling failed"); },