Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 58 additions & 11 deletions lib/agents/generalAgent/__tests__/getGeneralAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import { ChatRequestBody } from "@/lib/chat/validateChatRequest";
import { ToolLoopAgent, stepCountIs } from "ai";

// Import after mocks
import getGeneralAgent from "../getGeneralAgent";
import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmails";
import { selectAccountInfo } from "@/lib/supabase/account_info/selectAccountInfo";
import { getAccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails";
import { getKnowledgeBaseText } from "@/lib/files/getKnowledgeBaseText";
import { setupToolsForRequest } from "@/lib/chat/setupToolsForRequest";
import { getSystemPrompt } from "@/lib/prompts/getSystemPrompt";
import { extractImageUrlsFromMessages } from "@/lib/messages/extractImageUrlsFromMessages";
import { buildSystemPromptWithImages } from "@/lib/chat/buildSystemPromptWithImages";

// Mock all external dependencies
vi.mock("@/lib/supabase/account_emails/selectAccountEmails", () => ({
default: vi.fn(),
Expand Down Expand Up @@ -35,17 +46,6 @@ vi.mock("@/lib/chat/buildSystemPromptWithImages", () => ({
buildSystemPromptWithImages: vi.fn(),
}));

// Import after mocks
import getGeneralAgent from "../getGeneralAgent";
import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmails";
import { selectAccountInfo } from "@/lib/supabase/account_info/selectAccountInfo";
import { getAccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails";
import { getKnowledgeBaseText } from "@/lib/files/getKnowledgeBaseText";
import { setupToolsForRequest } from "@/lib/chat/setupToolsForRequest";
import { getSystemPrompt } from "@/lib/prompts/getSystemPrompt";
import { extractImageUrlsFromMessages } from "@/lib/messages/extractImageUrlsFromMessages";
import { buildSystemPromptWithImages } from "@/lib/chat/buildSystemPromptWithImages";

const mockSelectAccountEmails = vi.mocked(selectAccountEmails);
const mockSelectAccountInfo = vi.mocked(selectAccountInfo);
const mockGetAccountWithDetails = vi.mocked(getAccountWithDetails);
Expand Down Expand Up @@ -465,5 +465,52 @@ describe("getGeneralAgent", () => {
expect(result.stopWhen).toBeDefined();
expect(typeof result.stopWhen).toBe("function");
});

it("creates ToolLoopAgent with providerOptions for thinking/reasoning", async () => {
const body: ChatRequestBody = {
accountId: "account-123",
orgId: null,
messages: [{ id: "1", role: "user", content: "Hello" }],
};

const result = await getGeneralAgent(body);

// providerOptions should be baked into the agent constructor (stored in settings)
const settings = (result.agent as any).settings;
expect(settings.providerOptions).toBeDefined();
expect(settings.providerOptions.anthropic).toEqual(
expect.objectContaining({
thinking: { type: "enabled", budgetTokens: 12000 },
}),
);
expect(settings.providerOptions.google).toEqual(
expect.objectContaining({
thinkingConfig: expect.objectContaining({
thinkingBudget: 8192,
includeThoughts: true,
}),
}),
);
expect(settings.providerOptions.openai).toEqual(
expect.objectContaining({
reasoningEffort: "medium",
reasoningSummary: "detailed",
}),
);
});

it("creates ToolLoopAgent with prepareStep function", async () => {
const body: ChatRequestBody = {
accountId: "account-123",
orgId: null,
messages: [{ id: "1", role: "user", content: "Hello" }],
};

const result = await getGeneralAgent(body);

// prepareStep should be baked into the agent constructor (stored in settings)
const settings = (result.agent as any).settings;
expect(settings.prepareStep).toBeInstanceOf(Function);
});
});
});
24 changes: 24 additions & 0 deletions lib/agents/generalAgent/getGeneralAgent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { stepCountIs, ToolLoopAgent } from "ai";
import { AnthropicProviderOptions } from "@ai-sdk/anthropic";
import { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google";
import { OpenAIResponsesProviderOptions } from "@ai-sdk/openai";
import { DEFAULT_MODEL } from "@/lib/const";
import { RoutingDecision } from "@/lib/chat/types";
import { extractImageUrlsFromMessages } from "@/lib/messages/extractImageUrlsFromMessages";
Expand All @@ -10,6 +13,7 @@ import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmai
import { selectAccountInfo } from "@/lib/supabase/account_info/selectAccountInfo";
import { getKnowledgeBaseText } from "@/lib/files/getKnowledgeBaseText";
import { getAccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails";
import getPrepareStepResult from "@/lib/chat/toolChains/getPrepareStepResult";

/**
* Gets the general agent for the chat
Expand Down Expand Up @@ -55,6 +59,26 @@ export default async function getGeneralAgent(body: ChatRequestBody): Promise<Ro
instructions,
tools,
stopWhen,
prepareStep: options => {
const next = getPrepareStepResult(options);
if (next) return { ...options, ...next };
return options;
},
Comment on lines +62 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

🏁 Script executed:

find node_modules/ai -name "*.d.ts" 2>/dev/null | head -10

Repository: recoupable/api

Length of output: 40


🏁 Script executed:

# Search for ToolLoopAgent and prepareStep in the codebase
rg "prepareStep" --type ts --type tsx -B 3 -A 5 lib/

Repository: recoupable/api

Length of output: 85


🏁 Script executed:

# Check the actual implementation context
cat -n lib/agents/generalAgent/getGeneralAgent.ts | head -80

Repository: recoupable/api

Length of output: 3818


🏁 Script executed:

# Look for type definitions in node_modules/ai
find node_modules/ai -type f -name "*.d.ts" 2>/dev/null | head -20

Repository: recoupable/api

Length of output: 40


🏁 Script executed:

# Search for ToolLoopAgent in node_modules
rg "ToolLoopAgent" node_modules/ai --type ts -A 10 2>/dev/null | head -50

Repository: recoupable/api

Length of output: 40


🏁 Script executed:

# Check the getPrepareStepResult implementation
cat -n lib/chat/toolChains/getPrepareStepResult.ts

Repository: recoupable/api

Length of output: 3072


🏁 Script executed:

# Search for any other prepareStep usage in the codebase
rg "prepareStep" lib --type ts -B 2 -A 5 2>/dev/null

Repository: recoupable/api

Length of output: 5363


🏁 Script executed:

# Check package.json for ai version
cat package.json | grep -A 2 '"ai"'

Repository: recoupable/api

Length of output: 142


🏁 Script executed:

# Try looking for ToolLoopAgent in the main ai package
ls -la node_modules/ai/ | head -20

Repository: recoupable/api

Length of output: 121


🏁 Script executed:

# Look for any TypeScript definitions in ai
find node_modules/ai -name "*.ts" -o -name "*.d.ts" 2>/dev/null | xargs grep -l "ToolLoopAgent" 2>/dev/null | head -5

Repository: recoupable/api

Length of output: 40


🏁 Script executed:

# Check the main entry point
cat node_modules/ai/package.json | grep -E '"main"|"types"|"exports"' -A 1

Repository: recoupable/api

Length of output: 118


🌐 Web query:

ai sdk 6.0.0 beta ToolLoopAgent prepareStep callback signature

πŸ’‘ Result:

In AI SDK v6 (including the v6.0.0 betas), ToolLoopAgent’s prepareStep uses the PrepareStepFunction type:

type PrepareStepFunction<TOOLS extends Record<string, Tool> = Record<string, Tool>> =
  (options: {
    steps: Array<StepResult<TOOLS>>;
    stepNumber: number;
    model: LanguageModel;
    messages: Array<ModelMessage>;
    experimental_context: unknown;
  }) => PrepareStepResult<TOOLS> | PromiseLike<PrepareStepResult<TOOLS>>;

type PrepareStepResult<TOOLS extends Record<string, Tool> = Record<string, Tool>> =
  | {
      model?: LanguageModel;
      toolChoice?: ToolChoice<TOOLS>;
      activeTools?: Array<keyof TOOLS>;
      system?: string | SystemModelMessage | Array<SystemModelMessage>;
      messages?: Array<ModelMessage>;
      experimental_context?: unknown;
      providerOptions?: ProviderOptions;
    }
  | undefined;

Returning undefined means β€œno per-step overrides; keep using the outer/agent settings.” [1][2]


🏁 Script executed:

# Check if there are any TypeScript type files or documentation in the repo itself
find . -path ./node_modules -prune -o -name "*.d.ts" -type f -print | xargs grep -l "prepareStep" 2>/dev/null

Repository: recoupable/api

Length of output: 42


🏁 Script executed:

# Look for any documentation or comments about the API
rg "prepareStep" . --type md --type txt 2>/dev/null | head -20

Repository: recoupable/api

Length of output: 40


🏁 Script executed:

# Check if there's any version history or migration guide
fd -t f "CHANGELOG|MIGRATION|README" . 2>/dev/null | head -10

Repository: recoupable/api

Length of output: 87


Fix prepareStep return type: should only return override fields, not merged options.

The current implementation returns { ...options, ...next }, which spreads the entire options object (including steps, stepNumber, messages, model) back into the result. However, the ai SDK v6 beta expects prepareStep to return a partial PrepareStepResult containing only override fields (model?, toolChoice?, activeTools?, system?, messages?, experimental_context?, providerOptions?), or undefined for no overrides.

Returning the full options object violates the SDK contract. Simplify to:

prepareStep: options => getPrepareStepResult(options)

This correctly returns either the override object or undefined, matching the expected signature.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/generalAgent/getGeneralAgent.ts` around lines 62 - 66, prepareStep
currently returns a merged object ({ ...options, ...next }) which violates the
ai SDK v6 prepareStep signature; change prepareStep to return only the override
object or undefined by returning the result of getPrepareStepResult(options)
directly (i.e., prepareStep: options => getPrepareStepResult(options)) so it
returns only fields allowed in PrepareStepResult (model?, toolChoice?,
activeTools?, system?, messages?, experimental_context?, providerOptions?)
instead of the entire options object.

providerOptions: {
anthropic: {
thinking: { type: "enabled", budgetTokens: 12000 },
} satisfies AnthropicProviderOptions,
google: {
thinkingConfig: {
thinkingBudget: 8192,
includeThoughts: true,
},
} satisfies GoogleGenerativeAIProviderOptions,
openai: {
reasoningEffort: "medium",
reasoningSummary: "detailed",
} satisfies OpenAIResponsesProviderOptions,
},
});

return {
Expand Down
Loading