Skip to content
Open
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
42 changes: 21 additions & 21 deletions app/lib/workflows/__tests__/runAgentStep.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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",
Expand All @@ -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();
Expand All @@ -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();

Expand Down Expand Up @@ -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 = {
Expand All @@ -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();
Expand All @@ -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 {
Expand All @@ -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();
Expand All @@ -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);
});

Expand Down
8 changes: 7 additions & 1 deletion app/lib/workflows/__tests__/runAgentWorkflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
});
});

Expand Down
9 changes: 5 additions & 4 deletions app/lib/workflows/runAgentStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -132,12 +133,12 @@ export async function runAgentStep(input: RunAgentStepInput): Promise<RunAgentSt
});
const result = streamText({
model: callModel,
system: systemPrompt,
instructions: systemPrompt,
messages: modelMessages,
tools,
stopWhen: CHAT_AGENT_STOP_WHEN,
abortSignal: cancelController.signal,
experimental_context: agentContext,
toolsContext: buildToolsContext(tools, agentContext),
// Mark the LAST message with cacheControl on every step so Anthropic
// incrementally caches the conversation prefix. Mirrors open-agents'
// `prepareStep` in `open-harness-agent.ts:100`.
Expand All @@ -159,11 +160,11 @@ export async function runAgentStep(input: RunAgentStepInput): Promise<RunAgentSt
let responseMessage: UIMessage | undefined;
const uiStream = createUIMessageStream<UIMessage>({
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);
},
Expand Down
6 changes: 4 additions & 2 deletions app/lib/workflows/runAgentWorkflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
2 changes: 0 additions & 2 deletions lib/agent/messageMetadata/addLanguageModelUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
}
14 changes: 7 additions & 7 deletions lib/agent/tools/__tests__/bashTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
});
Expand All @@ -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",
Expand All @@ -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<string, string> };
expect(opts.env).toEqual({ RECOUP_ORG_ID: "org-uuid" });
Expand All @@ -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/);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
10 changes: 5 additions & 5 deletions lib/agent/tools/__tests__/editFileTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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/);
Expand All @@ -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/);
Expand All @@ -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/);
Expand All @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions lib/agent/tools/__tests__/globTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");
});
Expand All @@ -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/);
});

Expand All @@ -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/);
Expand Down
8 changes: 4 additions & 4 deletions lib/agent/tools/__tests__/grepTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>;
Expand Down Expand Up @@ -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);
Expand All @@ -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/);
Expand All @@ -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 ");
});
Expand Down
Loading
Loading