From e36d222fedc0b609cd943d515831deb4ae725407 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 25 Jun 2026 19:23:32 -0500 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20upgrade=20AI=20SDK=20v6=20?= =?UTF-8?q?=E2=86=92=20v7=20(api)=20=E2=80=94=20chat#1816?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump to v7: ai 6.0.190 → 7.0.2; @ai-sdk/{anthropic,google,openai} 3 → 4; @ai-sdk/gateway 3 → 4; @ai-sdk/mcp 1 → 2. (Zod already v4.) The agent harness survived v7 intact — ToolLoopAgent, the agent loop, and context passthrough all still exist. This was a rename + typed-context migration, not a redesign. Codemod handled the mechanical renames (isToolOrDynamicToolUIPart → isToolUIPart, experimental_generateImage → generateImage, stepCountIs → isStepCount, system → instructions, experimental_context → context, etc.). Manual work, by category: - Typed tool context (the real v7 change): v7 infers a tool's context as `never` unless declared, which broke both `tool()` overloads (TS2769) and `toolsContext`. Annotated every context-using tool's execute 2nd arg as `ToolExecutionOptions` (bash/glob/grep/task/webFetch/read/ write/edit/skill). v7 also split the old single `experimental_context` into a per-tool-name `toolsContext` map — added `buildToolsContext()` to fan the one AgentContext out to every tool key; wired it into runAgentStep + taskTool. - Usage token shape: v7 nests details under inputTokenDetails/outputTokenDetails and drops top-level reasoningTokens/cachedInputTokens. Fixed addLanguageModelUsage (codemod had doubled a path), ZERO_USAGE, recordCreditDeduction. - Provider options: GoogleProviderOptions → GoogleGenerativeAIProviderOptions. - ToolLoopAgent gained a 4th (runtime-context) generic — updated RoutingDecision / ChatConfig agent types to v7 arity. - Reverted codemod over-applications: local generateText/generateArray `{system}` wrappers (pass `instructions: system` inward), toolChain `system`→`instructions` on the custom ToolChainItem/PrepareStepResult types, webFetch discriminant `as const` + param-default move (v7 tool() inference). - Test fixtures aligned to v7 (mock exports + usage/UIMessage shapes); no weakened assertions. Verified: source `tsc --noEmit` clean (0 errors), full `pnpm test` green (3676 passed / 659 files). Pre-existing test-file type-debt in unrelated domains left untouched. Full build validated on Vercel preview. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../workflows/__tests__/runAgentStep.test.ts | 42 +++--- .../__tests__/runAgentWorkflow.test.ts | 8 +- app/lib/workflows/runAgentStep.ts | 9 +- app/lib/workflows/runAgentWorkflow.ts | 6 +- .../messageMetadata/addLanguageModelUsage.ts | 2 - lib/agent/tools/__tests__/bashTool.test.ts | 14 +- .../tools/__tests__/editFileTool.test.ts | 10 +- lib/agent/tools/__tests__/globTool.test.ts | 8 +- lib/agent/tools/__tests__/grepTool.test.ts | 8 +- .../tools/__tests__/readFileTool.test.ts | 8 +- lib/agent/tools/__tests__/skillTool.test.ts | 14 +- lib/agent/tools/__tests__/taskTool.test.ts | 18 +-- .../tools/__tests__/webFetchTool.test.ts | 8 +- .../tools/__tests__/writeFileTool.test.ts | 4 +- lib/agent/tools/bashTool.ts | 8 +- lib/agent/tools/buildToolsContext.ts | 23 +++ lib/agent/tools/editFileTool.ts | 2 +- lib/agent/tools/globTool.ts | 5 +- lib/agent/tools/grepTool.ts | 5 +- lib/agent/tools/readFileTool.ts | 2 +- lib/agent/tools/skillTool.ts | 2 +- lib/agent/tools/taskTool.ts | 27 +++- lib/agent/tools/webFetchTool.ts | 17 ++- lib/agent/tools/writeFileTool.ts | 2 +- .../__tests__/createCompactAgent.test.ts | 2 +- lib/agents/CompactAgent/createCompactAgent.ts | 4 +- .../EmailReplyAgent/createEmailReplyAgent.ts | 4 +- .../content/createContentPromptAgent.ts | 4 +- lib/ai/generateArray.ts | 2 +- lib/ai/generateText.ts | 2 +- .../integration/chatEndToEnd.test.ts | 19 +-- lib/chat/__tests__/types.test.ts | 4 +- lib/chat/const.ts | 4 +- .../__tests__/getPrepareStepResult.test.ts | 4 +- .../toolChains/__tests__/toolChains.test.ts | 22 +-- .../toolChains/createNewArtistToolChain.ts | 8 +- .../createReleaseReportToolChain.ts | 4 +- lib/chat/toolChains/getPrepareStepResult.ts | 4 +- lib/chat/toolChains/toolChains.ts | 4 +- lib/chat/types.ts | 4 +- .../__tests__/handleChatCredits.test.ts | 2 +- lib/credits/handleChatCredits.ts | 2 +- lib/evals/scorers/CatalogAvailability.ts | 2 +- lib/evals/scorers/QuestionAnswered.ts | 2 +- package.json | 12 +- pnpm-lock.yaml | 139 +++++++++--------- 46 files changed, 283 insertions(+), 223 deletions(-) create mode 100644 lib/agent/tools/buildToolsContext.ts diff --git a/app/lib/workflows/__tests__/runAgentStep.test.ts b/app/lib/workflows/__tests__/runAgentStep.test.ts index a9faf935e..d4d141cfd 100644 --- a/app/lib/workflows/__tests__/runAgentStep.test.ts +++ b/app/lib/workflows/__tests__/runAgentStep.test.ts @@ -50,11 +50,11 @@ vi.mock("workflow/api", () => ({ })); // Captures the options runAgentStep passes to createUIMessageStream so -// tests can drive its onStepFinish / onFinish callbacks directly. +// tests can drive its onStepEnd / onEnd callbacks directly. type CreateOpts = { generateId?: () => string; - onStepFinish?: (e: { responseMessage: unknown }) => unknown; - onFinish?: (e: { responseMessage: unknown }) => unknown; + onStepEnd?: (e: { responseMessage: unknown }) => unknown; + onEnd?: (e: { responseMessage: unknown }) => unknown; execute?: (a: { writer: { write: () => void; merge: () => void; onError: undefined } }) => void; }; let capturedCreateOpts: CreateOpts; @@ -174,9 +174,9 @@ describe("runAgentStep", () => { } as never); const args = vi.mocked(streamText).mock.calls[0]?.[0] as { system?: string }; - expect(args.system).toMatch(/# Environment/); - expect(args.system).toMatch(/Working directory: \. \(workspace root\)/); - expect(args.system).toMatch(/workspace-relative paths/); + expect(args.instructions).toMatch(/# Environment/); + expect(args.instructions).toMatch(/Working directory: \. \(workspace root\)/); + expect(args.instructions).toMatch(/workspace-relative paths/); }); it("wraps tools with anthropic cacheControl on the last tool before passing to streamText", async () => { @@ -242,26 +242,26 @@ describe("runAgentStep", () => { expect(cb({ part: { type: "start" } })).toBeUndefined(); }); - it("persists the assistant message on each step via onStepFinish", async () => { + it("persists the assistant message on each step via onStepEnd", async () => { vi.mocked(streamText).mockReturnValue(makeStreamResult() as never); const { stream } = makeWritable(); await runAgentStep({ ...baseInput, writable: stream } as never); const msg = { id: "a1", role: "assistant", parts: [{ type: "text", text: "partial" }] }; - await capturedCreateOpts.onStepFinish?.({ responseMessage: msg }); + await capturedCreateOpts.onStepEnd?.({ responseMessage: msg }); expect(persistAssistantMessage).toHaveBeenCalledWith("chat-1", msg); }); - it("persists the final assistant message via onFinish", async () => { + it("persists the final assistant message via onEnd", async () => { vi.mocked(streamText).mockReturnValue(makeStreamResult() as never); const { stream } = makeWritable(); await runAgentStep({ ...baseInput, writable: stream } as never); const msg = { id: "a1", role: "assistant", parts: [{ type: "text", text: "done" }] }; - await capturedCreateOpts.onFinish?.({ responseMessage: msg }); + await capturedCreateOpts.onEnd?.({ responseMessage: msg }); expect(persistAssistantMessage).toHaveBeenCalledWith("chat-1", msg); }); @@ -306,7 +306,7 @@ describe("runAgentStep", () => { expect(result.finishReason).toBe("stop"); }); - it("returns the responseMessage captured from onFinish (so the workflow can charge credits)", async () => { + it("returns the responseMessage captured from onEnd (so the workflow can charge credits)", async () => { const emitted = { id: "asst-test-id", role: "assistant", @@ -317,8 +317,8 @@ describe("runAgentStep", () => { vi.mocked(createUIMessageStream).mockImplementation((opts: never) => { const o = opts as CreateOpts; o.execute?.({ writer: { write: () => {}, merge: () => {}, onError: undefined } }); - // Drive onFinish so runAgentStep captures the final message. - void o.onFinish?.({ responseMessage: emitted }); + // Drive onEnd so runAgentStep captures the final message. + void o.onEnd?.({ responseMessage: emitted }); return new ReadableStream({ start(controller) { controller.close(); @@ -332,8 +332,8 @@ describe("runAgentStep", () => { expect(result.responseMessage).toEqual(emitted); }); - it("returns responseMessage: undefined when onFinish never fires", async () => { - // Default mock never invokes onFinish. + it("returns responseMessage: undefined when onEnd never fires", async () => { + // Default mock never invokes onEnd. vi.mocked(streamText).mockReturnValue(makeStreamResult() as never); const { stream } = makeWritable(); @@ -422,7 +422,7 @@ describe("runAgentStep", () => { }); it("re-persists with closed tool-error parts when aborting mid-tool-call", async () => { - // onStepFinish runs while the step is still emitting (a tool-call was + // onStepEnd runs while the step is still emitting (a tool-call was // streamed in this step). The step's captured responseMessage has the // tool-call in input-available, with no terminal output chunk yet. const openMessage = { @@ -444,9 +444,9 @@ describe("runAgentStep", () => { capturedCreateOpts.execute?.({ writer: { write: () => {}, merge: () => {}, onError: undefined }, }); - // Drive onStepFinish synchronously so responseMessage is populated + // Drive onStepEnd synchronously so responseMessage is populated // before the abort path runs in runAgentStep. - capturedCreateOpts.onStepFinish?.({ responseMessage: openMessage }); + capturedCreateOpts.onStepEnd?.({ responseMessage: openMessage }); return new ReadableStream({ start(controller) { controller.close(); @@ -467,7 +467,7 @@ describe("runAgentStep", () => { expect(result.aborted).toBe(true); - // Two persists: the step's onStepFinish, then the abort-path re-persist + // Two persists: the step's onStepEnd, then the abort-path re-persist // with closed tool parts. expect(persistAssistantMessage).toHaveBeenCalledTimes(2); const second = vi.mocked(persistAssistantMessage).mock.calls[1]?.[1] as { @@ -492,7 +492,7 @@ describe("runAgentStep", () => { capturedCreateOpts.execute?.({ writer: { write: () => {}, merge: () => {}, onError: undefined }, }); - capturedCreateOpts.onStepFinish?.({ responseMessage: closedMessage }); + capturedCreateOpts.onStepEnd?.({ responseMessage: closedMessage }); return new ReadableStream({ start(controller) { controller.close(); @@ -511,7 +511,7 @@ describe("runAgentStep", () => { await runAgentStep({ ...baseInput, writable: stream } as never); - // Just the onStepFinish persist — no re-persist needed. + // Just the onStepEnd persist — no re-persist needed. expect(persistAssistantMessage).toHaveBeenCalledTimes(1); }); diff --git a/app/lib/workflows/__tests__/runAgentWorkflow.test.ts b/app/lib/workflows/__tests__/runAgentWorkflow.test.ts index be1adf5d0..2809e2c06 100644 --- a/app/lib/workflows/__tests__/runAgentWorkflow.test.ts +++ b/app/lib/workflows/__tests__/runAgentWorkflow.test.ts @@ -258,7 +258,13 @@ describe("runAgentWorkflow", () => { model: "anthropic/claude-haiku-4.5", source: "api", gatewayCostUsd: undefined, - usage: { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 }, + usage: { + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + inputTokenDetails: { noCacheTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }, + outputTokenDetails: { textTokens: 0, reasoningTokens: 0 }, + }, }); }); diff --git a/app/lib/workflows/runAgentStep.ts b/app/lib/workflows/runAgentStep.ts index aebebfe46..d3fe2eb8f 100644 --- a/app/lib/workflows/runAgentStep.ts +++ b/app/lib/workflows/runAgentStep.ts @@ -10,6 +10,7 @@ import { agentCustomInstructions } from "@/lib/chat/agentCustomInstructions"; import { buildAgentSystemPrompt } from "@/lib/chat/buildAgentSystemPrompt"; import { CHAT_AGENT_STOP_WHEN } from "@/lib/chat/const"; import { buildAgentTools } from "@/lib/agent/buildAgentTools"; +import { buildToolsContext } from "@/lib/agent/tools/buildToolsContext"; import type { AgentContext, DurableAgentContext } from "@/lib/agent/tools/AgentContext"; import { buildMessageMetadataCallback } from "@/lib/agent/messageMetadata/buildMessageMetadataCallback"; import { addCacheControlToTools } from "@/lib/agent/contextManagement/addCacheControlToTools"; @@ -132,12 +133,12 @@ export async function runAgentStep(input: RunAgentStepInput): Promise({ generateId: () => input.assistantMessageId, - onStepFinish: ({ responseMessage: stepMessage }) => { + onStepEnd: ({ responseMessage: stepMessage }) => { responseMessage = stepMessage; return persistAssistantMessage(input.chatId, stepMessage); }, - onFinish: ({ responseMessage: finalMessage }) => { + onEnd: ({ responseMessage: finalMessage }) => { responseMessage = finalMessage; return persistAssistantMessage(input.chatId, finalMessage); }, diff --git a/app/lib/workflows/runAgentWorkflow.ts b/app/lib/workflows/runAgentWorkflow.ts index c0cdec7ff..73374099b 100644 --- a/app/lib/workflows/runAgentWorkflow.ts +++ b/app/lib/workflows/runAgentWorkflow.ts @@ -12,9 +12,11 @@ import type { DurableAgentContext } from "@/lib/agent/tools/AgentContext"; const ZERO_USAGE: LanguageModelUsage = { inputTokens: 0, - cachedInputTokens: 0, outputTokens: 0, -} as LanguageModelUsage; + totalTokens: 0, + inputTokenDetails: { noCacheTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }, + outputTokenDetails: { textTokens: 0, reasoningTokens: 0 }, +}; export type RunAgentWorkflowInput = { messages: UIMessage[]; diff --git a/lib/agent/messageMetadata/addLanguageModelUsage.ts b/lib/agent/messageMetadata/addLanguageModelUsage.ts index 4a676364f..581b5300a 100644 --- a/lib/agent/messageMetadata/addLanguageModelUsage.ts +++ b/lib/agent/messageMetadata/addLanguageModelUsage.ts @@ -43,7 +43,5 @@ export function addLanguageModelUsage( ), }, totalTokens: addTokenCounts(a.totalTokens, b.totalTokens), - reasoningTokens: addTokenCounts(a.reasoningTokens, b.reasoningTokens), - cachedInputTokens: addTokenCounts(a.cachedInputTokens, b.cachedInputTokens), }; } diff --git a/lib/agent/tools/__tests__/bashTool.test.ts b/lib/agent/tools/__tests__/bashTool.test.ts index 568a7f72d..58e596449 100644 --- a/lib/agent/tools/__tests__/bashTool.test.ts +++ b/lib/agent/tools/__tests__/bashTool.test.ts @@ -36,7 +36,7 @@ describe("bashTool.execute", () => { const tool = bashTool; const result = await tool.execute!({ command: "ls" }, { - experimental_context: baseContext, + context: baseContext, } as never); expect(result).toEqual({ success: true, @@ -66,7 +66,7 @@ describe("bashTool.execute", () => { const tool = bashTool; const result = (await tool.execute!({ command: "find ." }, { - experimental_context: baseContext, + context: baseContext, } as never)) as { truncated?: boolean }; expect(result.truncated).toBe(true); }); @@ -85,7 +85,7 @@ describe("bashTool.execute", () => { const tool = bashTool; await tool.execute!({ command: "ls", cwd: "apps/web" }, { - experimental_context: baseContext, + context: baseContext, } as never); expect(sandbox.exec).toHaveBeenCalledWith( "ls", @@ -109,7 +109,7 @@ describe("bashTool.execute", () => { const tool = bashTool; await tool.execute!({ command: "curl example.com" }, { - experimental_context: { ...baseContext, recoupOrgId: "org-uuid" }, + context: { ...baseContext, recoupOrgId: "org-uuid" }, } as never); const opts = sandbox.exec.mock.calls[0]?.[3] as { env?: Record }; expect(opts.env).toEqual({ RECOUP_ORG_ID: "org-uuid" }); @@ -123,7 +123,7 @@ describe("bashTool.execute", () => { const tool = bashTool; const result = (await tool.execute!({ command: "npm run dev", detached: true }, { - experimental_context: baseContext, + context: baseContext, } as never)) as { success: boolean; stdout: string }; expect(result.success).toBe(true); expect(result.stdout).toMatch(/cmd-123/); @@ -136,7 +136,7 @@ describe("bashTool.execute", () => { const tool = bashTool; const result = (await tool.execute!({ command: "npm run dev", detached: true }, { - experimental_context: baseContext, + context: baseContext, } as never)) as { success: boolean; stderr: string }; expect(result.success).toBe(false); expect(result.stderr).toMatch(/detached mode is not supported/i); @@ -150,7 +150,7 @@ describe("bashTool.execute", () => { const tool = bashTool; await tool.execute!({ command: "npm run dev", detached: true }, { - experimental_context: { ...baseContext, recoupOrgId: "org-uuid" }, + context: { ...baseContext, recoupOrgId: "org-uuid" }, } as never); // execDetached signature is (command, cwd) — no env arg. expect(sandbox.execDetached.mock.calls[0]).toHaveLength(2); diff --git a/lib/agent/tools/__tests__/editFileTool.test.ts b/lib/agent/tools/__tests__/editFileTool.test.ts index 3a2cac81d..5fc2f3bd0 100644 --- a/lib/agent/tools/__tests__/editFileTool.test.ts +++ b/lib/agent/tools/__tests__/editFileTool.test.ts @@ -29,7 +29,7 @@ describe("editFileTool", () => { const tool = editFileTool; const result = (await tool.execute!( { filePath: "a.txt", oldString: "old value", newString: "new value" }, - { experimental_context: ctx } as never, + { context: ctx } as never, )) as { success: boolean; replacements: number; startLine: number }; expect(result.success).toBe(true); expect(result.replacements).toBe(1); @@ -42,7 +42,7 @@ describe("editFileTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = editFileTool; const result = (await tool.execute!({ filePath: "a.txt", oldString: "x", newString: "x" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; error: string }; expect(result.success).toBe(false); expect(result.error).toMatch(/must be different/); @@ -54,7 +54,7 @@ describe("editFileTool", () => { const tool = editFileTool; const result = (await tool.execute!( { filePath: "a.txt", oldString: "missing", newString: "other" }, - { experimental_context: ctx } as never, + { context: ctx } as never, )) as { success: boolean; error: string }; expect(result.success).toBe(false); expect(result.error).toMatch(/not found/); @@ -65,7 +65,7 @@ describe("editFileTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = editFileTool; const result = (await tool.execute!({ filePath: "a.txt", oldString: "foo", newString: "baz" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; error: string }; expect(result.success).toBe(false); expect(result.error).toMatch(/2 times/); @@ -77,7 +77,7 @@ describe("editFileTool", () => { const tool = editFileTool; const result = (await tool.execute!( { filePath: "a.txt", oldString: "foo", newString: "qux", replaceAll: true }, - { experimental_context: ctx } as never, + { context: ctx } as never, )) as { success: boolean; replacements: number }; expect(result.success).toBe(true); expect(result.replacements).toBe(3); diff --git a/lib/agent/tools/__tests__/globTool.test.ts b/lib/agent/tools/__tests__/globTool.test.ts index 3f35d0a71..967b6e905 100644 --- a/lib/agent/tools/__tests__/globTool.test.ts +++ b/lib/agent/tools/__tests__/globTool.test.ts @@ -30,7 +30,7 @@ describe("globTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = globTool; const result = (await tool.execute!({ pattern: "**/*.ts" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; count: number; @@ -55,7 +55,7 @@ describe("globTool", () => { ); vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = globTool; - await tool.execute!({ pattern: "**/*.ts" }, { experimental_context: ctx } as never); + await tool.execute!({ pattern: "**/*.ts" }, { context: ctx } as never); const cmd = sb.exec.mock.calls[0]?.[0] as string; expect(cmd).not.toContain("-maxdepth"); }); @@ -72,7 +72,7 @@ describe("globTool", () => { ); vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = globTool; - await tool.execute!({ pattern: "*.json" }, { experimental_context: ctx } as never); + await tool.execute!({ pattern: "*.json" }, { context: ctx } as never); expect(sb.exec.mock.calls[0]?.[0]).toMatch(/-maxdepth\s+1/); }); @@ -89,7 +89,7 @@ describe("globTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = globTool; const result = (await tool.execute!({ pattern: "**/*.ts" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; error: string }; expect(result.success).toBe(false); expect(result.error).toMatch(/exit 2/); diff --git a/lib/agent/tools/__tests__/grepTool.test.ts b/lib/agent/tools/__tests__/grepTool.test.ts index e3545f501..f8d654371 100644 --- a/lib/agent/tools/__tests__/grepTool.test.ts +++ b/lib/agent/tools/__tests__/grepTool.test.ts @@ -29,7 +29,7 @@ describe("grepTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = grepTool; const result = (await tool.execute!({ pattern: "login", path: "src" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; matches: Array<{ file: string; line: number; content: string }>; @@ -58,7 +58,7 @@ describe("grepTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = grepTool; const result = (await tool.execute!({ pattern: "nothing", path: "src" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; matchCount: number }; expect(result.success).toBe(true); expect(result.matchCount).toBe(0); @@ -77,7 +77,7 @@ describe("grepTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = grepTool; const result = (await tool.execute!({ pattern: "[", path: "src" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; error: string }; expect(result.success).toBe(false); expect(result.error).toMatch(/invalid regex/); @@ -96,7 +96,7 @@ describe("grepTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = grepTool; await tool.execute!({ pattern: "x", path: ".", caseSensitive: false }, { - experimental_context: ctx, + context: ctx, } as never); expect(sb.exec.mock.calls[0]?.[0]).toContain(" -i "); }); diff --git a/lib/agent/tools/__tests__/readFileTool.test.ts b/lib/agent/tools/__tests__/readFileTool.test.ts index 6d1d27fa3..39ca44e51 100644 --- a/lib/agent/tools/__tests__/readFileTool.test.ts +++ b/lib/agent/tools/__tests__/readFileTool.test.ts @@ -32,7 +32,7 @@ describe("readFileTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = readFileTool; const result = (await tool.execute!({ filePath: "README.md" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; content: string; totalLines: number; path: string }; expect(result.success).toBe(true); expect(result.totalLines).toBe(3); @@ -50,7 +50,7 @@ describe("readFileTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = readFileTool; const result = (await tool.execute!({ filePath: "x.txt", offset: 2, limit: 2 }, { - experimental_context: ctx, + context: ctx, } as never)) as { content: string; startLine: number; endLine: number }; expect(result.startLine).toBe(2); // `endLine` is the last line included (1-indexed). With offset=2,limit=2 @@ -68,7 +68,7 @@ describe("readFileTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = readFileTool; const result = (await tool.execute!({ filePath: "src" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; error: string }; expect(result.success).toBe(false); expect(result.error).toMatch(/directory/i); @@ -81,7 +81,7 @@ describe("readFileTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = readFileTool; const result = (await tool.execute!({ filePath: "missing.ts" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; error: string }; expect(result.success).toBe(false); expect(result.error).toMatch(/not found/); diff --git a/lib/agent/tools/__tests__/skillTool.test.ts b/lib/agent/tools/__tests__/skillTool.test.ts index 0b3196dbc..7a110e27d 100644 --- a/lib/agent/tools/__tests__/skillTool.test.ts +++ b/lib/agent/tools/__tests__/skillTool.test.ts @@ -24,7 +24,7 @@ describe("skillTool", () => { it("returns success:false with available skills when the requested skill isn't in context", async () => { vi.mocked(connectVercel).mockResolvedValue(makeSandbox(vi.fn()) as never); const result = (await skillTool.execute!({ skill: "unknown" }, { - experimental_context: { + context: { ...baseCtx, skills: [ { @@ -51,7 +51,7 @@ describe("skillTool", () => { it("returns success:false when no skills are loaded", async () => { vi.mocked(connectVercel).mockResolvedValue(makeSandbox(vi.fn()) as never); const result = (await skillTool.execute!({ skill: "commit" }, { - experimental_context: { ...baseCtx, skills: [] }, + context: { ...baseCtx, skills: [] }, } as never)) as { success: boolean; error: string }; expect(result.success).toBe(false); expect(result.error).toMatch(/Available skills: none/); @@ -63,7 +63,7 @@ describe("skillTool", () => { const result = (await skillTool.execute!( { skill: "COMMIT" }, // model typed it loud { - experimental_context: { + context: { ...baseCtx, skills: [ { @@ -85,7 +85,7 @@ describe("skillTool", () => { const sb = makeSandbox(vi.fn().mockResolvedValue(skillMd("Run git commit -m ..."))); vi.mocked(connectVercel).mockResolvedValue(sb as never); const result = (await skillTool.execute!({ skill: "commit" }, { - experimental_context: { + context: { ...baseCtx, skills: [ { @@ -109,7 +109,7 @@ describe("skillTool", () => { const sb = makeSandbox(vi.fn().mockResolvedValue(skillMd('git commit -m "$ARGUMENTS"'))); vi.mocked(connectVercel).mockResolvedValue(sb as never); const result = (await skillTool.execute!({ skill: "commit", args: "fix bug" }, { - experimental_context: { + context: { ...baseCtx, skills: [ { @@ -129,7 +129,7 @@ describe("skillTool", () => { it("rejects skills with disable-model-invocation set", async () => { vi.mocked(connectVercel).mockResolvedValue(makeSandbox(vi.fn()) as never); const result = (await skillTool.execute!({ skill: "internal" }, { - experimental_context: { + context: { ...baseCtx, skills: [ { @@ -150,7 +150,7 @@ describe("skillTool", () => { const sb = makeSandbox(vi.fn().mockRejectedValue(new Error("ENOENT"))); vi.mocked(connectVercel).mockResolvedValue(sb as never); const result = (await skillTool.execute!({ skill: "commit" }, { - experimental_context: { + context: { ...baseCtx, skills: [ { diff --git a/lib/agent/tools/__tests__/taskTool.test.ts b/lib/agent/tools/__tests__/taskTool.test.ts index 8e876afdb..6dc281a42 100644 --- a/lib/agent/tools/__tests__/taskTool.test.ts +++ b/lib/agent/tools/__tests__/taskTool.test.ts @@ -28,7 +28,7 @@ function makeStreamResult(opts: { const calls = opts.toolCalls ?? []; const finishCount = opts.finishSteps ?? 1; return { - fullStream: (async function* () { + stream: (async function* () { for (const c of calls) { yield { type: "tool-call", toolName: c.toolName, input: c.input }; } @@ -60,7 +60,7 @@ describe("taskTool.execute (async generator)", () => { it("yields an initial chunk with toolCallCount=0 + startedAt + modelId before the subagent does any work", async () => { vi.mocked(streamText).mockReturnValue(makeStreamResult({}) as never); const gen = taskTool.execute!({ task: "x", instructions: "y" }, { - experimental_context: ctx, + context: ctx, } as never) as AsyncGenerator; const first = await gen.next(); expect(first.done).toBe(false); @@ -86,7 +86,7 @@ describe("taskTool.execute (async generator)", () => { ); const chunks = (await drainGenerator( taskTool.execute!({ task: "x", instructions: "y" }, { - experimental_context: ctx, + context: ctx, } as never) as AsyncGenerator, )) as TaskToolOutput[]; // Two tool-call yields + one finish-step yield (sticky pending so the @@ -110,7 +110,7 @@ describe("taskTool.execute (async generator)", () => { ); const chunks = (await drainGenerator( taskTool.execute!({ task: "x", instructions: "y" }, { - experimental_context: ctx, + context: ctx, } as never) as AsyncGenerator, )) as TaskToolOutput[]; const usageChunks = chunks.filter(c => c.usage); @@ -129,7 +129,7 @@ describe("taskTool.execute (async generator)", () => { vi.mocked(streamText).mockReturnValue(makeStreamResult({ responseMessages }) as never); const chunks = (await drainGenerator( taskTool.execute!({ task: "x", instructions: "y" }, { - experimental_context: ctx, + context: ctx, } as never) as AsyncGenerator, )) as TaskToolOutput[]; const finalChunk = chunks.find(c => c.final); @@ -143,7 +143,7 @@ describe("taskTool.execute (async generator)", () => { vi.mocked(streamText).mockReturnValue(makeStreamResult({}) as never); await drainGenerator( taskTool.execute!({ task: "x", instructions: "y" }, { - experimental_context: { ...ctx, subagentModel: subagentModelOverride }, + context: { ...ctx, subagentModel: subagentModelOverride }, } as never) as AsyncGenerator, ); const args = vi.mocked(streamText).mock.calls[0]?.[0] as { model: unknown }; @@ -152,7 +152,7 @@ describe("taskTool.execute (async generator)", () => { it("throws when agent context is missing the `model` field", async () => { const gen = taskTool.execute!({ task: "x", instructions: "y" }, { - experimental_context: { sandbox: ctx.sandbox /* no model */ }, + context: { sandbox: ctx.sandbox /* no model */ }, } as never) as AsyncGenerator; await expect(gen.next()).rejects.toThrow(/model not initialized/i); }); @@ -161,7 +161,7 @@ describe("taskTool.execute (async generator)", () => { vi.mocked(streamText).mockReturnValue(makeStreamResult({}) as never); await drainGenerator( taskTool.execute!({ task: "x", instructions: "y" }, { - experimental_context: ctx, + context: ctx, } as never) as AsyncGenerator, ); const args = vi.mocked(streamText).mock.calls[0]?.[0] as { tools: Record }; @@ -179,7 +179,7 @@ describe("taskTool.execute (async generator)", () => { vi.mocked(streamText).mockReturnValue(makeStreamResult({}) as never); await drainGenerator( taskTool.execute!({ task: "x", instructions: "y" }, { - experimental_context: ctx, + context: ctx, } as never) as AsyncGenerator, ); const args = vi.mocked(streamText).mock.calls[0]?.[0] as { diff --git a/lib/agent/tools/__tests__/webFetchTool.test.ts b/lib/agent/tools/__tests__/webFetchTool.test.ts index 47fb75c92..7d8c11be6 100644 --- a/lib/agent/tools/__tests__/webFetchTool.test.ts +++ b/lib/agent/tools/__tests__/webFetchTool.test.ts @@ -28,7 +28,7 @@ describe("webFetchTool", () => { ); vi.mocked(connectVercel).mockResolvedValue(sb as never); const result = (await webFetchTool.execute!({ url: "https://example.com/api" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; status: number; body: string; truncated: boolean }; expect(result).toEqual({ success: true, @@ -50,7 +50,7 @@ describe("webFetchTool", () => { ); vi.mocked(connectVercel).mockResolvedValue(sb as never); const result = (await webFetchTool.execute!({ url: "https://example.com/huge" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; truncated: boolean }; expect(result.success).toBe(true); expect(result.truncated).toBe(true); @@ -68,7 +68,7 @@ describe("webFetchTool", () => { ); vi.mocked(connectVercel).mockResolvedValue(sb as never); const result = (await webFetchTool.execute!({ url: "https://example.com/unreachable" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; error: string }; expect(result.success).toBe(false); expect(result.error).toMatch(/Failed to connect/); @@ -87,7 +87,7 @@ describe("webFetchTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); await webFetchTool.execute!( { url: "https://example.com/api", method: "POST", body: '{"x":1}' }, - { experimental_context: ctx } as never, + { context: ctx } as never, ); const cmd = sb.exec.mock.calls[0]?.[0] as string; expect(cmd).toContain("-X POST"); diff --git a/lib/agent/tools/__tests__/writeFileTool.test.ts b/lib/agent/tools/__tests__/writeFileTool.test.ts index 3656a777c..138088b9d 100644 --- a/lib/agent/tools/__tests__/writeFileTool.test.ts +++ b/lib/agent/tools/__tests__/writeFileTool.test.ts @@ -28,7 +28,7 @@ describe("writeFileTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = writeFileTool; const result = (await tool.execute!({ filePath: "src/index.ts", content: "export {}" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; path: string; bytesWritten: number }; expect(result.success).toBe(true); expect(result.path).toBe("src/index.ts"); @@ -44,7 +44,7 @@ describe("writeFileTool", () => { vi.mocked(connectVercel).mockResolvedValue(sb as never); const tool = writeFileTool; const result = (await tool.execute!({ filePath: "a.ts", content: "x" }, { - experimental_context: ctx, + context: ctx, } as never)) as { success: boolean; error: string }; expect(result.success).toBe(false); expect(result.error).toMatch(/EACCES/); diff --git a/lib/agent/tools/bashTool.ts b/lib/agent/tools/bashTool.ts index 479a608db..83c00da7b 100644 --- a/lib/agent/tools/bashTool.ts +++ b/lib/agent/tools/bashTool.ts @@ -1,5 +1,6 @@ -import { tool } from "ai"; +import { tool, type ToolExecutionOptions } from "ai"; import { z } from "zod"; +import type { AgentContext } from "@/lib/agent/tools/AgentContext"; import * as path from "path"; import { buildRecoupExecEnv } from "@/lib/agent/tools/buildRecoupExecEnv"; import { getSandbox } from "@/lib/agent/tools/getSandbox"; @@ -61,7 +62,10 @@ IMPORTANT: - Always quote file paths that may contain spaces - Use detached: true to start dev servers / long-running processes in the background`, inputSchema: bashInputSchema, - execute: async ({ command, cwd, detached }, { experimental_context, abortSignal }) => { + execute: async ( + { command, cwd, detached }, + { context: experimental_context, abortSignal }: ToolExecutionOptions, + ) => { const sandbox = await getSandbox(experimental_context, "bash"); const workingDirectory = sandbox.workingDirectory; const workingDir = cwd diff --git a/lib/agent/tools/buildToolsContext.ts b/lib/agent/tools/buildToolsContext.ts new file mode 100644 index 000000000..e7a95385d --- /dev/null +++ b/lib/agent/tools/buildToolsContext.ts @@ -0,0 +1,23 @@ +import type { ToolSet } from "ai"; +import type { AgentContext } from "@/lib/agent/tools/AgentContext"; + +/** + * Build the AI SDK v7 `toolsContext` map. + * + * v7 split the old single `experimental_context` into shared `runtimeContext` + * and a per-tool-name `toolsContext` map (see the AI SDK 7.0 migration guide: + * "Context: experimental_context Became Tool context"). Every Recoup agent + * tool reads the same `AgentContext` (sandbox handle, org id, skills, model), + * so we fan the single context out to one entry per tool in the set. + * + * Returning a `Record` (index signature) keeps this DRY + * across tool sets and stays assignable to the SDK's concrete + * `InferToolSetContext` shape, since every typed tool's required entry + * is satisfied with an `AgentContext` value. + */ +export function buildToolsContext( + tools: ToolSet, + context: AgentContext, +): Record { + return Object.fromEntries(Object.keys(tools).map(name => [name, context])); +} diff --git a/lib/agent/tools/editFileTool.ts b/lib/agent/tools/editFileTool.ts index d8274c0bc..5fdea1994 100644 --- a/lib/agent/tools/editFileTool.ts +++ b/lib/agent/tools/editFileTool.ts @@ -47,7 +47,7 @@ IMPORTANT: inputSchema: editInputSchema, execute: async ( { filePath, oldString, newString, replaceAll = false }, - { experimental_context }, + { context: experimental_context }, ) => { const sandbox = await getSandbox(experimental_context, "edit"); const workingDirectory = sandbox.workingDirectory; diff --git a/lib/agent/tools/globTool.ts b/lib/agent/tools/globTool.ts index d1de234d2..9db6ea3fe 100644 --- a/lib/agent/tools/globTool.ts +++ b/lib/agent/tools/globTool.ts @@ -1,5 +1,6 @@ -import { tool } from "ai"; +import { tool, type ToolExecutionOptions } from "ai"; import { z } from "zod"; +import type { AgentContext } from "@/lib/agent/tools/AgentContext"; import * as path from "path"; import { getSandbox } from "@/lib/agent/tools/getSandbox"; import { shellEscape } from "@/lib/agent/tools/shellEscape"; @@ -54,7 +55,7 @@ IMPORTANT: inputSchema: globInputSchema, execute: async ( { pattern, path: basePath, limit = DEFAULT_LIMIT }, - { experimental_context, abortSignal }, + { context: experimental_context, abortSignal }: ToolExecutionOptions, ) => { const sandbox = await getSandbox(experimental_context, "glob"); const workingDirectory = sandbox.workingDirectory; diff --git a/lib/agent/tools/grepTool.ts b/lib/agent/tools/grepTool.ts index f172f61af..d815c0777 100644 --- a/lib/agent/tools/grepTool.ts +++ b/lib/agent/tools/grepTool.ts @@ -1,5 +1,6 @@ -import { tool } from "ai"; +import { tool, type ToolExecutionOptions } from "ai"; import { z } from "zod"; +import type { AgentContext } from "@/lib/agent/tools/AgentContext"; import * as path from "path"; import { getSandbox } from "@/lib/agent/tools/getSandbox"; import { shellEscape } from "@/lib/agent/tools/shellEscape"; @@ -56,7 +57,7 @@ IMPORTANT: inputSchema: grepInputSchema, execute: async ( { pattern, path: searchPath, glob, caseSensitive = true }, - { experimental_context, abortSignal }, + { context: experimental_context, abortSignal }: ToolExecutionOptions, ) => { const sandbox = await getSandbox(experimental_context, "grep"); const workingDirectory = sandbox.workingDirectory; diff --git a/lib/agent/tools/readFileTool.ts b/lib/agent/tools/readFileTool.ts index f5a486a64..c3838f9f5 100644 --- a/lib/agent/tools/readFileTool.ts +++ b/lib/agent/tools/readFileTool.ts @@ -30,7 +30,7 @@ IMPORTANT: - This tool can only read files, not directories — attempting to read a directory returns an error - You can call multiple reads in parallel to speculatively load several files`, inputSchema: readInputSchema, - execute: async ({ filePath, offset = 1, limit = 2000 }, { experimental_context }) => { + execute: async ({ filePath, offset = 1, limit = 2000 }, { context: experimental_context }) => { const sandbox = await getSandbox(experimental_context, "read"); const workingDirectory = sandbox.workingDirectory; diff --git a/lib/agent/tools/skillTool.ts b/lib/agent/tools/skillTool.ts index 8c74f35d1..e6b1068b7 100644 --- a/lib/agent/tools/skillTool.ts +++ b/lib/agent/tools/skillTool.ts @@ -43,7 +43,7 @@ Important: - NEVER just announce or mention a skill without actually calling this tool - Only use skills listed in "Available skills" in your system prompt`, inputSchema: skillInputSchema, - execute: async ({ skill, args }, { experimental_context }) => { + execute: async ({ skill, args }, { context: experimental_context }) => { const sandbox = await getSandbox(experimental_context, "skill"); const skills = getSkills(experimental_context); diff --git a/lib/agent/tools/taskTool.ts b/lib/agent/tools/taskTool.ts index 270974fce..b67990b6c 100644 --- a/lib/agent/tools/taskTool.ts +++ b/lib/agent/tools/taskTool.ts @@ -1,6 +1,15 @@ -import { streamText, stepCountIs, tool, type LanguageModelUsage, type ModelMessage } from "ai"; +import { + streamText, + isStepCount, + tool, + type LanguageModelUsage, + type ModelMessage, + type ToolExecutionOptions, +} from "ai"; import { z } from "zod"; +import type { AgentContext } from "@/lib/agent/tools/AgentContext"; import { buildSubagentTools } from "@/lib/agent/tools/buildSubagentTools"; +import { buildToolsContext } from "@/lib/agent/tools/buildToolsContext"; import { getSubagentModel } from "@/lib/agent/tools/getSubagentModel"; import { sumLanguageModelUsage } from "@/lib/agent/messageMetadata/sumLanguageModelUsage"; @@ -96,7 +105,10 @@ IMPORTANT: - The parent agent does not see the subagent's internal tool calls, only its final summary`, inputSchema: taskInputSchema, outputSchema: taskOutputSchema, - execute: async function* ({ task, instructions }, { experimental_context, abortSignal }) { + execute: async function* ( + { task, instructions }, + { context: experimental_context, abortSignal }: ToolExecutionOptions, + ) { const subagentModel = getSubagentModel(experimental_context, "task"); const subagentModelId = typeof subagentModel === "string" @@ -106,13 +118,14 @@ IMPORTANT: // `prompt` (not `messages: []`) is required — the AI SDK records zero // steps and throws NoOutputGeneratedError if the model has only a // system prompt with no user turn. Mirrors open-agents' task tool. + const subagentTools = buildSubagentTools(); const result = streamText({ model: subagentModel, - system: `${SUBAGENT_SYSTEM_PROMPT}\n\n## Your Task\n${task}\n\n## Instructions\n${instructions}`, + instructions: `${SUBAGENT_SYSTEM_PROMPT}\n\n## Your Task\n${task}\n\n## Instructions\n${instructions}`, prompt: "Complete this task and provide a summary of what you accomplished.", - tools: buildSubagentTools(), - stopWhen: stepCountIs(SUBAGENT_STEP_LIMIT), - experimental_context, + tools: subagentTools, + stopWhen: isStepCount(SUBAGENT_STEP_LIMIT), + toolsContext: buildToolsContext(subagentTools, experimental_context), abortSignal, }); @@ -126,7 +139,7 @@ IMPORTANT: // the first step finishes. yield { toolCallCount, startedAt, modelId: subagentModelId }; - for await (const part of result.fullStream) { + for await (const part of result.stream) { if (part.type === "tool-call") { toolCallCount += 1; pending = { name: part.toolName, input: part.input }; diff --git a/lib/agent/tools/webFetchTool.ts b/lib/agent/tools/webFetchTool.ts index b395457f9..e7fdb2a46 100644 --- a/lib/agent/tools/webFetchTool.ts +++ b/lib/agent/tools/webFetchTool.ts @@ -1,5 +1,6 @@ -import { tool } from "ai"; +import { tool, type ToolExecutionOptions } from "ai"; import { z } from "zod"; +import type { AgentContext } from "@/lib/agent/tools/AgentContext"; import { buildRecoupExecEnv } from "@/lib/agent/tools/buildRecoupExecEnv"; import { getSandbox } from "@/lib/agent/tools/getSandbox"; import { shellEscape } from "@/lib/agent/tools/shellEscape"; @@ -48,9 +49,13 @@ USAGE: inputSchema: fetchInputSchema, outputSchema: fetchOutputSchema, execute: async ( - { url, method = "GET", headers, body }, - { experimental_context, abortSignal }, + { url, method: methodInput, headers, body }, + { context: experimental_context, abortSignal }: ToolExecutionOptions, ) => { + // Default applied in-body rather than via a destructure default: combined + // with `outputSchema`, a parameter default poisons the AI SDK v7 `tool()` + // INPUT inference and breaks the typed-overload resolution. + const method = methodInput ?? "GET"; const sandbox = await getSandbox(experimental_context, "web_fetch"); const workingDirectory = sandbox.workingDirectory; const recoupEnv = buildRecoupExecEnv(experimental_context); @@ -99,7 +104,7 @@ USAGE: // exit 23 = curl wrote partial output (`head -c` cut it off — expected for large responses). if (result.exitCode !== 0 && result.exitCode !== 23) { return { - success: false, + success: false as const, error: `Fetch failed: ${result.stderr || result.stdout || "Unknown error"}`, }; } @@ -111,14 +116,14 @@ USAGE: const status = /^\d+$/.test(statusText) ? parseInt(statusText, 10) : null; return { - success: true, + success: true as const, status, body: responseBody, truncated: result.exitCode === 23, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); - return { success: false, error: `Fetch failed: ${message}` }; + return { success: false as const, error: `Fetch failed: ${message}` }; } }, }); diff --git a/lib/agent/tools/writeFileTool.ts b/lib/agent/tools/writeFileTool.ts index c8e59e3c3..66cbcae82 100644 --- a/lib/agent/tools/writeFileTool.ts +++ b/lib/agent/tools/writeFileTool.ts @@ -39,7 +39,7 @@ IMPORTANT: - NEVER proactively create documentation files (e.g., *.md) unless the user explicitly requests them - Do not write files that contain secrets or credentials (API keys, passwords, .env, etc.)`, inputSchema: writeInputSchema, - execute: async ({ filePath, content }, { experimental_context }) => { + execute: async ({ filePath, content }, { context: experimental_context }) => { const sandbox = await getSandbox(experimental_context, "write"); const workingDirectory = sandbox.workingDirectory; diff --git a/lib/agents/CompactAgent/__tests__/createCompactAgent.test.ts b/lib/agents/CompactAgent/__tests__/createCompactAgent.test.ts index a0f713f7e..9d0e35628 100644 --- a/lib/agents/CompactAgent/__tests__/createCompactAgent.test.ts +++ b/lib/agents/CompactAgent/__tests__/createCompactAgent.test.ts @@ -6,7 +6,7 @@ vi.mock("ai", () => ({ config, generate: vi.fn(), })), - stepCountIs: vi.fn().mockReturnValue("stepCountMock"), + isStepCount: vi.fn().mockReturnValue("stepCountMock"), })); vi.mock("@/lib/const", () => ({ diff --git a/lib/agents/CompactAgent/createCompactAgent.ts b/lib/agents/CompactAgent/createCompactAgent.ts index 144a60284..0d2b5ee9d 100644 --- a/lib/agents/CompactAgent/createCompactAgent.ts +++ b/lib/agents/CompactAgent/createCompactAgent.ts @@ -1,4 +1,4 @@ -import { ToolLoopAgent, stepCountIs } from "ai"; +import { ToolLoopAgent, isStepCount } from "ai"; import { LIGHTWEIGHT_MODEL } from "@/lib/const"; const DEFAULT_INSTRUCTIONS = `You are a conversation summarizer. Create a concise summary of the conversation that: @@ -19,6 +19,6 @@ export function createCompactAgent(customInstructions?: string) { return new ToolLoopAgent({ model: LIGHTWEIGHT_MODEL, instructions: customInstructions || DEFAULT_INSTRUCTIONS, - stopWhen: stepCountIs(1), + stopWhen: isStepCount(1), }); } diff --git a/lib/agents/EmailReplyAgent/createEmailReplyAgent.ts b/lib/agents/EmailReplyAgent/createEmailReplyAgent.ts index 29452b7d3..31a9730bb 100644 --- a/lib/agents/EmailReplyAgent/createEmailReplyAgent.ts +++ b/lib/agents/EmailReplyAgent/createEmailReplyAgent.ts @@ -1,4 +1,4 @@ -import { Output, ToolLoopAgent, stepCountIs } from "ai"; +import { Output, ToolLoopAgent, isStepCount } from "ai"; import { z } from "zod"; import { LIGHTWEIGHT_MODEL, INBOUND_EMAIL_DOMAIN } from "@/lib/const"; @@ -25,6 +25,6 @@ export function createEmailReplyAgent() { model: LIGHTWEIGHT_MODEL, instructions, output: Output.object({ schema: replyDecisionSchema }), - stopWhen: stepCountIs(1), + stopWhen: isStepCount(1), }); } diff --git a/lib/agents/content/createContentPromptAgent.ts b/lib/agents/content/createContentPromptAgent.ts index 93dc5d10e..a0c31c226 100644 --- a/lib/agents/content/createContentPromptAgent.ts +++ b/lib/agents/content/createContentPromptAgent.ts @@ -1,4 +1,4 @@ -import { Output, ToolLoopAgent, stepCountIs } from "ai"; +import { Output, ToolLoopAgent, isStepCount } from "ai"; import { z } from "zod"; import { LIGHTWEIGHT_MODEL } from "@/lib/const"; import { CONTENT_TEMPLATES, DEFAULT_CONTENT_TEMPLATE } from "@/lib/content/contentTemplates"; @@ -71,6 +71,6 @@ export function createContentPromptAgent() { model: LIGHTWEIGHT_MODEL, instructions, output: Output.object({ schema: contentPromptFlagsSchema }), - stopWhen: stepCountIs(1), + stopWhen: isStepCount(1), }); } diff --git a/lib/ai/generateArray.ts b/lib/ai/generateArray.ts index c33c459fe..1a07c3b4c 100644 --- a/lib/ai/generateArray.ts +++ b/lib/ai/generateArray.ts @@ -16,7 +16,7 @@ const generateArray = async ({ }): Promise => { const result = await generateObject({ model: DEFAULT_MODEL, - system, + instructions: system, prompt, output: "array", schema: z.object({ diff --git a/lib/ai/generateText.ts b/lib/ai/generateText.ts index 0d509f899..ae70712d5 100644 --- a/lib/ai/generateText.ts +++ b/lib/ai/generateText.ts @@ -11,7 +11,7 @@ const generateText = async ({ model?: string; }) => { const result = await generate({ - system, + instructions: system, model: model || DEFAULT_MODEL, prompt, }); diff --git a/lib/chat/__tests__/integration/chatEndToEnd.test.ts b/lib/chat/__tests__/integration/chatEndToEnd.test.ts index 507eece0a..bacfb94ac 100644 --- a/lib/chat/__tests__/integration/chatEndToEnd.test.ts +++ b/lib/chat/__tests__/integration/chatEndToEnd.test.ts @@ -121,7 +121,7 @@ vi.mock("@/lib/ai/generateText", () => ({ // Mock AI SDK vi.mock("ai", () => ({ convertToModelMessages: vi.fn((messages: unknown[]) => messages), - stepCountIs: vi.fn().mockReturnValue(() => true), + isStepCount: vi.fn().mockReturnValue(() => true), ToolLoopAgent: vi.fn().mockImplementation(() => ({ stream: vi.fn(), tools: {}, @@ -359,17 +359,18 @@ describe("Chat Integration Tests", () => { it("calculates and deducts credits based on usage", async () => { mockGetCreditUsage.mockResolvedValue(0.5); + const usage = { + inputTokens: 1000, + outputTokens: 500, + inputTokenDetails: { cacheReadTokens: 0 }, + }; await handleChatCredits({ - usage: { promptTokens: 1000, completionTokens: 500 }, + usage: usage as never, model: "gpt-4", accountId: "account-123", }); - expect(mockGetCreditUsage).toHaveBeenCalledWith( - { promptTokens: 1000, completionTokens: 500 }, - "gpt-4", - undefined, - ); + expect(mockGetCreditUsage).toHaveBeenCalledWith(usage, "gpt-4", undefined); expect(mockRecordCreditDeduction).toHaveBeenCalledWith( expect.objectContaining({ accountId: "account-123", @@ -395,7 +396,7 @@ describe("Chat Integration Tests", () => { mockRecordCreditDeduction.mockResolvedValue({ success: true, newBalance: 332 }); await handleChatCredits({ - usage: { promptTokens: 10, completionTokens: 5 }, + usage: { inputTokens: 10, outputTokens: 5, inputTokenDetails: { cacheReadTokens: 0 } } as never, model: "gpt-4", accountId: "account-123", }); @@ -423,7 +424,7 @@ describe("Chat Integration Tests", () => { mockGetCreditUsage.mockResolvedValue(0.001); await handleChatCredits({ - usage: { promptTokens: 5, completionTokens: 5 }, + usage: { inputTokens: 5, outputTokens: 5, inputTokenDetails: { cacheReadTokens: 0 } } as never, model: "gpt-4", accountId: "account-123", }); diff --git a/lib/chat/__tests__/types.test.ts b/lib/chat/__tests__/types.test.ts index f4755355c..39f21e8ae 100644 --- a/lib/chat/__tests__/types.test.ts +++ b/lib/chat/__tests__/types.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { stepCountIs, ToolLoopAgent } from "ai"; +import { isStepCount, ToolLoopAgent } from "ai"; import type { ChatConfig, RoutingDecision } from "../types"; describe("Chat Types", () => { @@ -27,7 +27,7 @@ describe("Chat Types", () => { model: "gpt-4", instructions: "Test instructions", agent: new ToolLoopAgent({ model: "gpt-4" }), - stopWhen: stepCountIs(5), + stopWhen: isStepCount(5), }; expect(decision.stopWhen).toBeDefined(); }); diff --git a/lib/chat/const.ts b/lib/chat/const.ts index 7e1772190..eb16f028f 100644 --- a/lib/chat/const.ts +++ b/lib/chat/const.ts @@ -1,4 +1,4 @@ -import { stepCountIs } from "ai"; +import { isStepCount } from "ai"; export const MAX_MESSAGES = 55; @@ -11,7 +11,7 @@ export const MAX_MESSAGES = 55; * Single-shot agents (compact, content, email-reply) use `stepCountIs(1)` * directly — they're not in the multi-step family. */ -export const CHAT_AGENT_STOP_WHEN = stepCountIs(111); +export const CHAT_AGENT_STOP_WHEN = isStepCount(111); export const SYSTEM_PROMPT = `You are Recoup, a friendly, sharp, and strategic AI assistant for the music industry. You help music executives, artist teams, and self-starting artists analyze fan data, optimize marketing, and grow artist careers. diff --git a/lib/chat/toolChains/__tests__/getPrepareStepResult.test.ts b/lib/chat/toolChains/__tests__/getPrepareStepResult.test.ts index df582c015..50b2456a6 100644 --- a/lib/chat/toolChains/__tests__/getPrepareStepResult.test.ts +++ b/lib/chat/toolChains/__tests__/getPrepareStepResult.test.ts @@ -9,7 +9,7 @@ vi.mock("../toolChains", () => ({ custom_chain: [ { toolName: "custom_step_one", - system: "Custom system prompt for step one", + instructions: "Custom system prompt for step one", }, { toolName: "custom_step_two", @@ -219,7 +219,7 @@ describe("getPrepareStepResult", () => { const result = getPrepareStepResult(options as any); expect(result).toEqual({ toolChoice: { type: "tool", toolName: "custom_step_one" }, - system: "Custom system prompt for step one", + instructions: "Custom system prompt for step one", model: "gpt-4-turbo", }); }); diff --git a/lib/chat/toolChains/__tests__/toolChains.test.ts b/lib/chat/toolChains/__tests__/toolChains.test.ts index fdd5782f6..519073653 100644 --- a/lib/chat/toolChains/__tests__/toolChains.test.ts +++ b/lib/chat/toolChains/__tests__/toolChains.test.ts @@ -13,9 +13,9 @@ describe("toolChains", () => { it("allows tool chain item with system prompt", () => { const item: ToolChainItem = { toolName: "test_tool", - system: "Custom system prompt", + instructions: "Custom system prompt", }; - expect(item.system).toBe("Custom system prompt"); + expect(item.instructions).toBe("Custom system prompt"); }); it("allows tool chain item with messages", () => { @@ -47,12 +47,12 @@ describe("toolChains", () => { const result: PrepareStepResult = { toolChoice: { type: "tool", toolName: "test_tool" }, model: "gemini-2.5-pro" as any, - system: "Custom prompt", + instructions: "Custom prompt", messages: [{ role: "user", content: "Test" }], }; expect(result.toolChoice).toBeDefined(); expect(result.model).toBeDefined(); - expect(result.system).toBeDefined(); + expect(result.instructions).toBeDefined(); expect(result.messages).toBeDefined(); }); }); @@ -81,10 +81,10 @@ describe("toolChains", () => { it("includes update_account_info with custom system prompt", () => { const chain = TOOL_CHAINS.create_new_artist; const updateAccountStep = chain.find( - item => item.toolName === "update_account_info" && item.system, + item => item.toolName === "update_account_info" && item.instructions, ); expect(updateAccountStep).toBeDefined(); - expect(updateAccountStep?.system).toContain("get_spotify_search"); + expect(updateAccountStep?.instructions).toContain("get_spotify_search"); }); it("has expected number of steps", () => { @@ -110,16 +110,16 @@ describe("toolChains", () => { const chain = TOOL_CHAINS.create_new_artist; const generateStep = chain.find(item => item.toolName === "generate_txt_file"); expect(generateStep).toBeDefined(); - expect(generateStep?.system).toBeDefined(); - expect(generateStep?.system).toContain("knowledge base report"); + expect(generateStep?.instructions).toBeDefined(); + expect(generateStep?.instructions).toContain("knowledge base report"); }); it("includes update_account_info step with system prompt to link knowledge base", () => { const chain = TOOL_CHAINS.create_new_artist; const updateSteps = chain.filter(item => item.toolName === "update_account_info"); - const knowledgeLinkStep = updateSteps.find(item => item.system?.includes("knowledges")); + const knowledgeLinkStep = updateSteps.find(item => item.instructions?.includes("knowledges")); expect(knowledgeLinkStep).toBeDefined(); - expect(knowledgeLinkStep?.system).toContain("arweaveUrl"); + expect(knowledgeLinkStep?.instructions).toContain("arweaveUrl"); }); }); @@ -133,7 +133,7 @@ describe("toolChains", () => { const chain = TOOL_CHAINS.create_release_report; const generateStep = chain.find(item => item.toolName === "generate_txt_file"); expect(generateStep).toBeDefined(); - expect(generateStep?.system).toBeDefined(); + expect(generateStep?.instructions).toBeDefined(); }); it("ends with send_email", () => { diff --git a/lib/chat/toolChains/createNewArtistToolChain.ts b/lib/chat/toolChains/createNewArtistToolChain.ts index e18fba6eb..faf346c63 100644 --- a/lib/chat/toolChains/createNewArtistToolChain.ts +++ b/lib/chat/toolChains/createNewArtistToolChain.ts @@ -5,12 +5,12 @@ export const createNewArtistToolChain: ToolChainItem[] = [ { toolName: "get_spotify_search" }, { toolName: "update_account_info", - system: + instructions: "From the get_spotify_search results, select the artist whose name best matches the user-provided artist name (prefer exact, case-insensitive match; otherwise choose the closest by name and popularity). Update the account using the update_account_info tool with the artist's basic information: name, image, label, etc.", }, { toolName: "update_artist_socials", - system: + instructions: "Using the matched Spotify artist from the get_spotify_search results, update the artist's socials with the Spotify profile URL (found in external_urls.spotify). Pass the URL in the urls array to update_artist_socials.", }, { toolName: "artist_deep_research" }, @@ -27,13 +27,13 @@ export const createNewArtistToolChain: ToolChainItem[] = [ }, { toolName: "generate_txt_file", - system: + instructions: "Using all the research gathered (from artist_deep_research, spotify_deep_research, search_web results, etc.), generate a comprehensive knowledge base report following the reference template. Include artist biography, discography, social media presence, notable achievements, and any other relevant information. Pass the complete report text to generate_txt_file.", messages: [getKnowledgeBaseReportReferenceMessage()], }, { toolName: "update_account_info", - system: + instructions: "Using the arweaveUrl returned from generate_txt_file, update the artist's knowledges array to include the new knowledge base entry. Set the knowledge object with: url (the arweaveUrl), name (e.g., 'Artist Knowledge Base Report'), and type ('text/plain').", }, { toolName: "youtube_login" }, diff --git a/lib/chat/toolChains/create_release_report/createReleaseReportToolChain.ts b/lib/chat/toolChains/create_release_report/createReleaseReportToolChain.ts index afac8c0e8..4e2001060 100644 --- a/lib/chat/toolChains/create_release_report/createReleaseReportToolChain.ts +++ b/lib/chat/toolChains/create_release_report/createReleaseReportToolChain.ts @@ -12,7 +12,7 @@ export const createReleaseReportToolChain: ToolChainItem[] = [ }, { toolName: "generate_txt_file", - system: `Create a Release Report TXT file matching the reference release report. + instructions: `Create a Release Report TXT file matching the reference release report. Do not make up any data. Only use the data that is present in the web_deep_research tool results. The following sections must be included in the report (only if the data is present in the web_deep_research tool results) passed to the contents parameter in the generate_txt_file tool: - {artwork title} Summary @@ -30,7 +30,7 @@ export const createReleaseReportToolChain: ToolChainItem[] = [ }, { toolName: "update_account_info", - system: + instructions: "Attach the newly created release report to the artist's account info as a knowledge base. IMPORTANT: Use the active_artist_account_id from the system prompt as the artistId parameter.", }, { diff --git a/lib/chat/toolChains/getPrepareStepResult.ts b/lib/chat/toolChains/getPrepareStepResult.ts index 4362ea489..6344d0985 100644 --- a/lib/chat/toolChains/getPrepareStepResult.ts +++ b/lib/chat/toolChains/getPrepareStepResult.ts @@ -48,8 +48,8 @@ const getPrepareStepResult = (options: PrepareStepOptions): PrepareStepResult | }; // Add system prompt if available - if (nextToolItem.system) { - result.system = nextToolItem.system; + if (nextToolItem.instructions) { + result.instructions = nextToolItem.instructions; } // Add messages if available diff --git a/lib/chat/toolChains/toolChains.ts b/lib/chat/toolChains/toolChains.ts index 1761d8ba4..4ad2c3108 100644 --- a/lib/chat/toolChains/toolChains.ts +++ b/lib/chat/toolChains/toolChains.ts @@ -4,14 +4,14 @@ import { createNewArtistToolChain } from "./createNewArtistToolChain"; export type ToolChainItem = { toolName: string; - system?: string; + instructions?: string; messages?: ModelMessage[]; }; export type PrepareStepResult = { toolChoice?: { type: "tool"; toolName: string }; model?: LanguageModel; - system?: string; + instructions?: string; messages?: ModelMessage[]; }; diff --git a/lib/chat/types.ts b/lib/chat/types.ts index 83c9d9765..151643596 100644 --- a/lib/chat/types.ts +++ b/lib/chat/types.ts @@ -4,11 +4,11 @@ import { type ModelMessage, type ToolSet, type StopCondition, type ToolLoopAgent export interface RoutingDecision { model: string; instructions: string; - agent: ToolLoopAgent; + agent: ToolLoopAgent; stopWhen?: StopCondition> | StopCondition>[] | undefined; } export interface ChatConfig { - agent: ToolLoopAgent; + agent: ToolLoopAgent; messages: ModelMessage[]; } diff --git a/lib/credits/__tests__/handleChatCredits.test.ts b/lib/credits/__tests__/handleChatCredits.test.ts index c1786fc8f..f3281966f 100644 --- a/lib/credits/__tests__/handleChatCredits.test.ts +++ b/lib/credits/__tests__/handleChatCredits.test.ts @@ -18,7 +18,7 @@ const mockRecordCreditDeduction = vi.mocked(recordCreditDeduction); const USAGE = { inputTokens: 1000, outputTokens: 500, - cachedInputTokens: 0, + inputTokenDetails: { cacheReadTokens: 0 }, } as never; describe("handleChatCredits", () => { diff --git a/lib/credits/handleChatCredits.ts b/lib/credits/handleChatCredits.ts index 98c8ecdbc..3ac748677 100644 --- a/lib/credits/handleChatCredits.ts +++ b/lib/credits/handleChatCredits.ts @@ -58,7 +58,7 @@ export const handleChatCredits = async ({ modelId: model, inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, - cachedInputTokens: usage.cachedInputTokens, + cachedInputTokens: usage.inputTokenDetails.cacheReadTokens, }); } catch (error) { console.error("Failed to handle chat credits:", error); diff --git a/lib/evals/scorers/CatalogAvailability.ts b/lib/evals/scorers/CatalogAvailability.ts index f4829ea41..8b5490a96 100644 --- a/lib/evals/scorers/CatalogAvailability.ts +++ b/lib/evals/scorers/CatalogAvailability.ts @@ -19,7 +19,7 @@ export const CatalogAvailability = async ({ const result = await generateObject({ model: DEFAULT_MODEL, - system: `You are a music catalog analyst. Your job is to analyze song recommendations and determine which ones are available in the provided music catalog. + instructions: `You are a music catalog analyst. Your job is to analyze song recommendations and determine which ones are available in the provided music catalog. Instructions: 1. Extract all song recommendations from the output text diff --git a/lib/evals/scorers/QuestionAnswered.ts b/lib/evals/scorers/QuestionAnswered.ts index abe0222cb..ee452a3b4 100644 --- a/lib/evals/scorers/QuestionAnswered.ts +++ b/lib/evals/scorers/QuestionAnswered.ts @@ -18,7 +18,7 @@ export const QuestionAnswered = async ({ try { const result = await generateObject({ model: DEFAULT_MODEL, - system: `You are an AI evaluation expert. Your job is to determine if an AI assistant actually answered the customer's question with a specific answer, or if it deflected, explained why it couldn't answer, or gave generic suggestions without providing the requested information. + instructions: `You are an AI evaluation expert. Your job is to determine if an AI assistant actually answered the customer's question with a specific answer, or if it deflected, explained why it couldn't answer, or gave generic suggestions without providing the requested information. Instructions: 1. Read the customer's question carefully to understand what specific information they're asking for diff --git a/package.json b/package.json index b34c72bed..82fcb4d6d 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,11 @@ "eval": "braintrust eval --external-packages playwright playwright-core chromium-bidi @browserbasehq/stagehand @composio/core @composio/vercel" }, "dependencies": { - "@ai-sdk/anthropic": "^3.0.78", - "@ai-sdk/gateway": "3.0.119", - "@ai-sdk/google": "^3.0.79", - "@ai-sdk/mcp": "^1.0.43", - "@ai-sdk/openai": "^3.0.65", + "@ai-sdk/anthropic": "^4.0.0", + "@ai-sdk/gateway": "^4.0.2", + "@ai-sdk/google": "^4.0.0", + "@ai-sdk/mcp": "^2.0.0", + "@ai-sdk/openai": "^4.0.0", "@chat-adapter/github": "^4.15.0", "@chat-adapter/slack": "^4.15.0", "@chat-adapter/state-ioredis": "^4.15.0", @@ -38,7 +38,7 @@ "@trigger.dev/sdk": "^4.4.3", "@vercel/blob": "^2.3.1", "@vercel/sandbox": "2.0.0-beta.11", - "ai": "6.0.190", + "ai": "^7.0.2", "apify-client": "^2.20.0", "arweave": "^1.15.7", "autoevals": "^0.0.129", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e953e3bcb..dc71991af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,20 +9,20 @@ importers: .: dependencies: '@ai-sdk/anthropic': - specifier: ^3.0.78 - version: 3.0.78(zod@4.1.13) + specifier: ^4.0.0 + version: 4.0.0(zod@4.1.13) '@ai-sdk/gateway': - specifier: 3.0.119 - version: 3.0.119(zod@4.1.13) + specifier: ^4.0.2 + version: 4.0.2(zod@4.1.13) '@ai-sdk/google': - specifier: ^3.0.79 - version: 3.0.79(zod@4.1.13) + specifier: ^4.0.0 + version: 4.0.0(zod@4.1.13) '@ai-sdk/mcp': - specifier: ^1.0.43 - version: 1.0.43(zod@4.1.13) + specifier: ^2.0.0 + version: 2.0.0(zod@4.1.13) '@ai-sdk/openai': - specifier: ^3.0.65 - version: 3.0.65(zod@4.1.13) + specifier: ^4.0.0 + version: 4.0.0(zod@4.1.13) '@chat-adapter/github': specifier: ^4.15.0 version: 4.15.0 @@ -46,7 +46,7 @@ importers: version: 0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) '@composio/vercel': specifier: ^0.3.4 - version: 0.3.4(@composio/core@0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ai@6.0.190(zod@4.1.13)) + version: 0.3.4(@composio/core@0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ai@7.0.2(zod@4.1.13)) '@fal-ai/client': specifier: ^1.9.5 version: 1.9.5 @@ -61,7 +61,7 @@ importers: version: 2.86.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@trigger.dev/sdk': specifier: ^4.4.3 - version: 4.4.3(ai@6.0.190(zod@4.1.13))(bufferutil@4.0.9)(utf-8-validate@5.0.10)(zod@4.1.13) + version: 4.4.3(ai@7.0.2(zod@4.1.13))(bufferutil@4.0.9)(utf-8-validate@5.0.10)(zod@4.1.13) '@vercel/blob': specifier: ^2.3.1 version: 2.3.1 @@ -69,8 +69,8 @@ importers: specifier: 2.0.0-beta.11 version: 2.0.0-beta.11 ai: - specifier: 6.0.190 - version: 6.0.190(zod@4.1.13) + specifier: ^7.0.2 + version: 7.0.2(zod@4.1.13) apify-client: specifier: ^2.20.0 version: 2.20.0 @@ -210,39 +210,39 @@ packages: '@adraffy/ens-normalize@1.11.1': resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} - '@ai-sdk/anthropic@3.0.78': - resolution: {integrity: sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA==} - engines: {node: '>=18'} + '@ai-sdk/anthropic@4.0.0': + resolution: {integrity: sha512-N0lT1g6/5DEIZvalpkpwYRCdu7n5qb8qPN3PcTem6k4VkPBLC2+T2LAAyx1GS0eNOxavVa0CP7n2kCiye0yyfw==} + engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@3.0.119': - resolution: {integrity: sha512-VAhfRWC+JexZakkVfmjaJKaTj00x7/UHdE8kMWL3NhuQAlf8oXtg9r4dfvFZrByXxchGRBvYE3biEUyibkg0xg==} - engines: {node: '>=18'} + '@ai-sdk/gateway@4.0.2': + resolution: {integrity: sha512-Jz1BiiTSvhDsCBJrkFRSqLHDRMVjFtYk9GdbSi3UOqY+/epza+oIESMDzfN4m+YHT/1IYmNEmxaMfjXOvxKDjQ==} + engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/google@3.0.79': - resolution: {integrity: sha512-QWVAvYeA7JzEX2wkSyXOWv/I9PD9kvTzdykkSTLi+Eu8RyJ6gA0tdPIGa8esEtOcHE//G5vy6FTB70qQw8l/uw==} - engines: {node: '>=18'} + '@ai-sdk/google@4.0.0': + resolution: {integrity: sha512-UXGGmsYmeJ8VEfFenETFd2SN5tGSU+g2yrLrCuL8uqUkDFpNqV9a9MSKdshh6EXQs8e+2PUELJwqe7qQO3UnSw==} + engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/mcp@1.0.43': - resolution: {integrity: sha512-HdDMeyCcfIn5tW/P1kJ+BmYP8vfY8vppRn7swbVNRcLeFz/cpwik+B+C49Up4u5scRAcATtRJywOa7/rA4BmIA==} - engines: {node: '>=18'} + '@ai-sdk/mcp@2.0.0': + resolution: {integrity: sha512-+N6gJ1AbcDk3+6asoEsdIojVmgEReKNcvWIT716pDL3AepGI6j7RJVdaRyhQLltwzTj0arwpwU4BBzvhOiCd/g==} + engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/openai@3.0.65': - resolution: {integrity: sha512-ZlVoWH+zrdiYDiUt6n/xvfCsk33mzsB81TUQkBRVx79rxU1FKZqVH9J/QCtEpSLqx0cUzjvtIw9l9p7EbUv+dw==} - engines: {node: '>=18'} + '@ai-sdk/openai@4.0.0': + resolution: {integrity: sha512-XcI/bJEG+Ymx8ZwQMY9GE9waHdEcSzT6wn2o+YW80hOQPf8IAGQvdNWKaD0mxLi6AmOM0L01y/p626w7rImblQ==} + engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@4.0.27': - resolution: {integrity: sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw==} - engines: {node: '>=18'} + '@ai-sdk/provider-utils@5.0.0': + resolution: {integrity: sha512-zj66M02jc6ASYwIgWZowsooDUwaVngeNZQ3H10GwcPMZ+KR6gHMhcUuKl6tkai+JPXTKDyHY1pnszuxRtw2D4A==} + engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -250,9 +250,9 @@ packages: resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} engines: {node: '>=18'} - '@ai-sdk/provider@3.0.10': - resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==} - engines: {node: '>=18'} + '@ai-sdk/provider@4.0.0': + resolution: {integrity: sha512-fr9Gs89prDWiuox/T+kCA+i2cJkHpxU5S+tr4megjTzRC27ZsvFhwjU/+XrqqMbvBUlfmXxTOYWy8ng45dsjIg==} + engines: {node: '>=22'} '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} @@ -3434,6 +3434,9 @@ packages: '@workflow/rollup@4.0.4': resolution: {integrity: sha512-DqmSp89Y4WVoMkBWkiNll0b5mZ2WA9uwNsRuO4QOU0XHytuCnvTRGMf0ZV6fXAa3KyeXlgS1r0V8SHwt1ZQ7jg==} + '@workflow/serde@4.1.0': + resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==} + '@workflow/serde@4.1.0-beta.2': resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==} @@ -3603,9 +3606,9 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} - ai@6.0.190: - resolution: {integrity: sha512-T+ixHbWZ6jmHRREpVVJTkFyWJeCekCdzLPan7lp1F32jG5OUw4+odlVYjtMRXVzogU+pWzpMmXdRiHUmdL/q0w==} - engines: {node: '>=18'} + ai@7.0.2: + resolution: {integrity: sha512-VMU08jHIDJnnKDrbC9AFa5ZsPpOTfAPRLvTRHtJk4FGAoeldmJROMxvZ2ak5lCjEJ2GP2OLPQbMRyEK8w0+S4A==} + engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -8137,42 +8140,43 @@ snapshots: '@adraffy/ens-normalize@1.11.1': {} - '@ai-sdk/anthropic@3.0.78(zod@4.1.13)': + '@ai-sdk/anthropic@4.0.0(zod@4.1.13)': dependencies: - '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@4.1.13) + '@ai-sdk/provider': 4.0.0 + '@ai-sdk/provider-utils': 5.0.0(zod@4.1.13) zod: 4.1.13 - '@ai-sdk/gateway@3.0.119(zod@4.1.13)': + '@ai-sdk/gateway@4.0.2(zod@4.1.13)': dependencies: - '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@4.1.13) + '@ai-sdk/provider': 4.0.0 + '@ai-sdk/provider-utils': 5.0.0(zod@4.1.13) '@vercel/oidc': 3.2.0 zod: 4.1.13 - '@ai-sdk/google@3.0.79(zod@4.1.13)': + '@ai-sdk/google@4.0.0(zod@4.1.13)': dependencies: - '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@4.1.13) + '@ai-sdk/provider': 4.0.0 + '@ai-sdk/provider-utils': 5.0.0(zod@4.1.13) zod: 4.1.13 - '@ai-sdk/mcp@1.0.43(zod@4.1.13)': + '@ai-sdk/mcp@2.0.0(zod@4.1.13)': dependencies: - '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@4.1.13) + '@ai-sdk/provider': 4.0.0 + '@ai-sdk/provider-utils': 5.0.0(zod@4.1.13) pkce-challenge: 5.0.1 zod: 4.1.13 - '@ai-sdk/openai@3.0.65(zod@4.1.13)': + '@ai-sdk/openai@4.0.0(zod@4.1.13)': dependencies: - '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@4.1.13) + '@ai-sdk/provider': 4.0.0 + '@ai-sdk/provider-utils': 5.0.0(zod@4.1.13) zod: 4.1.13 - '@ai-sdk/provider-utils@4.0.27(zod@4.1.13)': + '@ai-sdk/provider-utils@5.0.0(zod@4.1.13)': dependencies: - '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider': 4.0.0 '@standard-schema/spec': 1.1.0 + '@workflow/serde': 4.1.0 eventsource-parser: 3.0.8 zod: 4.1.13 @@ -8180,7 +8184,7 @@ snapshots: dependencies: json-schema: 0.4.0 - '@ai-sdk/provider@3.0.10': + '@ai-sdk/provider@4.0.0': dependencies: json-schema: 0.4.0 @@ -8636,10 +8640,10 @@ snapshots: dependencies: zod: 4.1.13 - '@composio/vercel@0.3.4(@composio/core@0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ai@6.0.190(zod@4.1.13))': + '@composio/vercel@0.3.4(@composio/core@0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ai@7.0.2(zod@4.1.13))': dependencies: '@composio/core': 0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) - ai: 6.0.190(zod@4.1.13) + ai: 7.0.2(zod@4.1.13) '@crawlee/types@3.15.3': dependencies: @@ -11605,7 +11609,7 @@ snapshots: '@s2-dev/streamstore': 0.22.5 dequal: 2.0.3 eventsource: 3.0.7 - eventsource-parser: 3.0.6 + eventsource-parser: 3.0.8 execa: 8.0.1 humanize-duration: 3.33.1 jose: 5.10.0 @@ -11624,7 +11628,7 @@ snapshots: - supports-color - utf-8-validate - '@trigger.dev/sdk@4.4.3(ai@6.0.190(zod@4.1.13))(bufferutil@4.0.9)(utf-8-validate@5.0.10)(zod@4.1.13)': + '@trigger.dev/sdk@4.4.3(ai@7.0.2(zod@4.1.13))(bufferutil@4.0.9)(utf-8-validate@5.0.10)(zod@4.1.13)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.36.0 @@ -11640,7 +11644,7 @@ snapshots: ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) zod: 4.1.13 optionalDependencies: - ai: 6.0.190(zod@4.1.13) + ai: 7.0.2(zod@4.1.13) transitivePeerDependencies: - bufferutil - supports-color @@ -12772,6 +12776,8 @@ snapshots: - aws-crt - supports-color + '@workflow/serde@4.1.0': {} + '@workflow/serde@4.1.0-beta.2': {} '@workflow/serde@4.1.1': {} @@ -13013,12 +13019,11 @@ snapshots: dependencies: humanize-ms: 1.2.1 - ai@6.0.190(zod@4.1.13): + ai@7.0.2(zod@4.1.13): dependencies: - '@ai-sdk/gateway': 3.0.119(zod@4.1.13) - '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@4.1.13) - '@opentelemetry/api': 1.9.0 + '@ai-sdk/gateway': 4.0.2(zod@4.1.13) + '@ai-sdk/provider': 4.0.0 + '@ai-sdk/provider-utils': 5.0.0(zod@4.1.13) zod: 4.1.13 ajv-formats@3.0.1(ajv@8.17.1): @@ -14509,7 +14514,7 @@ snapshots: eventsource@3.0.7: dependencies: - eventsource-parser: 3.0.6 + eventsource-parser: 3.0.8 evt@2.5.9: dependencies: From 3a91b8ec23d338b338cb6c0597ba4bb366eccd2f Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 25 Jun 2026 19:58:36 -0500 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20green=20up=20the=20v7=20PR=20?= =?UTF-8?q?=E2=80=94=20lint,=20format,=20and=20the=20Workflow=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI on the v7 bump surfaced three failures (code was correct; tooling wasn't): - lint: `@typescript-eslint/no-explicit-any` on the `ToolLoopAgent` 4th (runtime-context) generic in RoutingDecision/ChatConfig → use `Record` (which is v7's `Context` constraint, and the source agent's inferred `any` slot is assignable to it). - format: prettier on two v7-updated test fixtures. - Vercel build: `Error: dynamic usage of require is not supported` while collecting page data for the Workflow step route — `@vercel/oidc` (pulled in by `@ai-sdk/gateway@4`) does a dynamic require Turbopack can't bundle. Switch the build to `next build --webpack` (which tolerates it), matching what chat and marketing already use. Verified locally: webpack compiles the Workflow routes; the require error is gone (remaining local failures are env-only, present on Vercel). Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/chat/__tests__/integration/chatEndToEnd.test.ts | 12 ++++++++++-- lib/chat/toolChains/__tests__/toolChains.test.ts | 4 +++- lib/chat/types.ts | 4 ++-- package.json | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/chat/__tests__/integration/chatEndToEnd.test.ts b/lib/chat/__tests__/integration/chatEndToEnd.test.ts index bacfb94ac..d894c2e61 100644 --- a/lib/chat/__tests__/integration/chatEndToEnd.test.ts +++ b/lib/chat/__tests__/integration/chatEndToEnd.test.ts @@ -396,7 +396,11 @@ describe("Chat Integration Tests", () => { mockRecordCreditDeduction.mockResolvedValue({ success: true, newBalance: 332 }); await handleChatCredits({ - usage: { inputTokens: 10, outputTokens: 5, inputTokenDetails: { cacheReadTokens: 0 } } as never, + usage: { + inputTokens: 10, + outputTokens: 5, + inputTokenDetails: { cacheReadTokens: 0 }, + } as never, model: "gpt-4", accountId: "account-123", }); @@ -424,7 +428,11 @@ describe("Chat Integration Tests", () => { mockGetCreditUsage.mockResolvedValue(0.001); await handleChatCredits({ - usage: { inputTokens: 5, outputTokens: 5, inputTokenDetails: { cacheReadTokens: 0 } } as never, + usage: { + inputTokens: 5, + outputTokens: 5, + inputTokenDetails: { cacheReadTokens: 0 }, + } as never, model: "gpt-4", accountId: "account-123", }); diff --git a/lib/chat/toolChains/__tests__/toolChains.test.ts b/lib/chat/toolChains/__tests__/toolChains.test.ts index 519073653..f1ff67afc 100644 --- a/lib/chat/toolChains/__tests__/toolChains.test.ts +++ b/lib/chat/toolChains/__tests__/toolChains.test.ts @@ -117,7 +117,9 @@ describe("toolChains", () => { it("includes update_account_info step with system prompt to link knowledge base", () => { const chain = TOOL_CHAINS.create_new_artist; const updateSteps = chain.filter(item => item.toolName === "update_account_info"); - const knowledgeLinkStep = updateSteps.find(item => item.instructions?.includes("knowledges")); + const knowledgeLinkStep = updateSteps.find(item => + item.instructions?.includes("knowledges"), + ); expect(knowledgeLinkStep).toBeDefined(); expect(knowledgeLinkStep?.instructions).toContain("arweaveUrl"); }); diff --git a/lib/chat/types.ts b/lib/chat/types.ts index 151643596..9bf3eae2f 100644 --- a/lib/chat/types.ts +++ b/lib/chat/types.ts @@ -4,11 +4,11 @@ import { type ModelMessage, type ToolSet, type StopCondition, type ToolLoopAgent export interface RoutingDecision { model: string; instructions: string; - agent: ToolLoopAgent; + agent: ToolLoopAgent, never>; stopWhen?: StopCondition> | StopCondition>[] | undefined; } export interface ChatConfig { - agent: ToolLoopAgent; + agent: ToolLoopAgent, never>; messages: ModelMessage[]; } diff --git a/package.json b/package.json index 82fcb4d6d..6f8f7972d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "next dev", - "build": "next build", + "build": "next build --webpack", "start": "next start", "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", From dfc4803e41c994f7535888ab71b91e49f3f290ba Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 25 Jun 2026 20:06:56 -0500 Subject: [PATCH 3/3] fix: keep @vercel/oidc external so the Workflow build resolves it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Webpack got past Turbopack's "dynamic require" refusal, but then the Workflow step route failed at runtime: bundling `@vercel/oidc/index.js` made its internal `require('./get-vercel-oidc-token.js')` resolve to a relative path that doesn't exist on Vercel ("Cannot find module …/get-vercel-oidc-token.js"). Fix: add `@vercel/oidc` to `serverExternalPackages` (+ as a direct dependency so it's resolvable). Node then loads the package — and its internal require — natively instead of bundling it. Pairs with the `next build --webpack` switch; Turbopack refuses to externalize it ("can't be external"). Verified locally (webpack): the Workflow routes compile and collect page data with no @vercel/oidc error; only env-dependent routes fail locally (env present on Vercel). Co-Authored-By: Claude Opus 4.8 (1M context) --- next.config.ts | 8 ++++++++ package.json | 1 + pnpm-lock.yaml | 3 +++ 3 files changed, 12 insertions(+) diff --git a/next.config.ts b/next.config.ts index becd52cca..0b840c660 100644 --- a/next.config.ts +++ b/next.config.ts @@ -5,6 +5,14 @@ const nextConfig: NextConfig = { env: { RESOURCE_WALLET_ADDRESS: process.env.RESOURCE_WALLET_ADDRESS, }, + // `@vercel/oidc` (pulled in by `@ai-sdk/gateway@4`) does a runtime + // `require('./get-vercel-oidc-token.js')`. If bundled into the Workflow step + // route, that relative require can't resolve at runtime and the build's + // page-data collection throws. Keep the package external so Node resolves it + // (and its internal require) natively. Requires `@vercel/oidc` as a direct + // dependency so it's resolvable, and the webpack builder (Turbopack refuses + // to externalize it). + serverExternalPackages: ["@vercel/oidc"], experimental: { optimizePackageImports: ["date-fns", "@ai-sdk/anthropic", "@ai-sdk/openai", "@ai-sdk/google"], }, diff --git a/package.json b/package.json index 6f8f7972d..47e5acfd0 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@supabase/supabase-js": "^2.86.0", "@trigger.dev/sdk": "^4.4.3", "@vercel/blob": "^2.3.1", + "@vercel/oidc": "^3.2.0", "@vercel/sandbox": "2.0.0-beta.11", "ai": "^7.0.2", "apify-client": "^2.20.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc71991af..01c10b41d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: '@vercel/blob': specifier: ^2.3.1 version: 2.3.1 + '@vercel/oidc': + specifier: ^3.2.0 + version: 3.4.0 '@vercel/sandbox': specifier: 2.0.0-beta.11 version: 2.0.0-beta.11