Procházet zdrojové kódy

test: add regression test for tool_use ids were found without tool_result blocks immediately after

Aiden Cline před 4 měsíci
rodič
revize
c4d804fb48
1 změnil soubory, kde provedl 264 přidání a 1 odebrání
  1. 264 1
      packages/opencode/test/session/llm.test.ts

+ 264 - 1
packages/opencode/test/session/llm.test.ts

@@ -13,7 +13,7 @@ import { ProviderID, ModelID } from "../../src/provider/schema"
 import { Filesystem } from "../../src/util/filesystem"
 import { tmpdir } from "../fixture/fixture"
 import type { Agent } from "../../src/agent/agent"
-import type { MessageV2 } from "../../src/session/message-v2"
+import { MessageV2 } from "../../src/session/message-v2"
 import { SessionID, MessageID } from "../../src/session/schema"
 import { AppRuntime } from "../../src/effect/app-runtime"
 
@@ -909,6 +909,269 @@ describe("session.llm.stream", () => {
     })
   })
 
+  test("sends anthropic tool_use blocks with tool_result immediately after them", async () => {
+    const server = state.server
+    if (!server) {
+      throw new Error("Server not initialized")
+    }
+
+    const source = await loadFixture("anthropic", "claude-opus-4-6")
+    const model = source.model
+    const chunks = [
+      {
+        type: "message_start",
+        message: {
+          id: "msg-tool-order",
+          model: model.id,
+          usage: {
+            input_tokens: 3,
+            cache_creation_input_tokens: null,
+            cache_read_input_tokens: null,
+          },
+        },
+      },
+      {
+        type: "content_block_start",
+        index: 0,
+        content_block: { type: "text", text: "" },
+      },
+      {
+        type: "content_block_delta",
+        index: 0,
+        delta: { type: "text_delta", text: "ok" },
+      },
+      { type: "content_block_stop", index: 0 },
+      {
+        type: "message_delta",
+        delta: { stop_reason: "end_turn", stop_sequence: null, container: null },
+        usage: {
+          input_tokens: 3,
+          output_tokens: 2,
+          cache_creation_input_tokens: null,
+          cache_read_input_tokens: null,
+        },
+      },
+      { type: "message_stop" },
+    ]
+    const request = waitRequest("/messages", createEventResponse(chunks))
+
+    await using tmp = await tmpdir({
+      init: async (dir) => {
+        await Bun.write(
+          path.join(dir, "opencode.json"),
+          JSON.stringify({
+            $schema: "https://opencode.ai/config.json",
+            enabled_providers: ["anthropic"],
+            provider: {
+              anthropic: {
+                name: "Anthropic",
+                env: ["ANTHROPIC_API_KEY"],
+                npm: "@ai-sdk/anthropic",
+                api: "https://api.anthropic.com/v1",
+                models: {
+                  [model.id]: model,
+                },
+                options: {
+                  apiKey: "test-anthropic-key",
+                  baseURL: `${server.url.origin}/v1`,
+                },
+              },
+            },
+          }),
+        )
+      },
+    })
+
+    await Instance.provide({
+      directory: tmp.path,
+      fn: async () => {
+        const resolved = await getModel(ProviderID.make("anthropic"), ModelID.make(model.id))
+        const sessionID = SessionID.make("session-test-anthropic-tools")
+        const agent = {
+          name: "test",
+          mode: "primary",
+          options: {},
+          permission: [{ permission: "*", pattern: "*", action: "allow" }],
+        } satisfies Agent.Info
+        const user = {
+          id: MessageID.make("user-anthropic-tools"),
+          sessionID,
+          role: "user",
+          time: { created: Date.now() },
+          agent: agent.name,
+          model: { providerID: ProviderID.make("anthropic"), modelID: resolved.id, variant: "max" },
+        } satisfies MessageV2.User
+
+        const input = [
+          {
+            info: {
+              id: "msg_user",
+              sessionID,
+              role: "user",
+              time: { created: 1 },
+              agent: "gentleman",
+              model: { providerID: "anthropic", modelID: "claude-opus-4-6", variant: "max" },
+            },
+            parts: [
+              {
+                id: "p_user",
+                sessionID,
+                messageID: "msg_user",
+                type: "text",
+                text: "Can you check whether there are any PDF files in my home directory?",
+              },
+            ],
+          },
+          {
+            info: {
+              id: "msg_call",
+              sessionID,
+              parentID: "msg_user",
+              role: "assistant",
+              mode: "gentleman",
+              agent: "gentleman",
+              variant: "max",
+              path: { cwd: "/root", root: "/" },
+              cost: 0,
+              tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
+              modelID: "claude-opus-4-6",
+              providerID: "anthropic",
+              time: { created: 2, completed: 3 },
+              finish: "tool-calls",
+            },
+            parts: [
+              {
+                id: "p_step",
+                sessionID,
+                messageID: "msg_call",
+                type: "step-start",
+              },
+              {
+                id: "p_read",
+                sessionID,
+                messageID: "msg_call",
+                type: "tool",
+                tool: "read",
+                callID: "toolu_01N8mDEzG8DSTs7UPHFtmgCT",
+                state: {
+                  status: "completed",
+                  input: { filePath: "/root" },
+                  output: "<path>/root</path>",
+                  metadata: {},
+                  title: "root",
+                  time: { start: 10, end: 11 },
+                },
+              },
+              {
+                id: "p_glob",
+                sessionID,
+                messageID: "msg_call",
+                type: "tool",
+                tool: "glob",
+                callID: "toolu_01APxrADs7VozN8uWzw9WwHr",
+                state: {
+                  status: "completed",
+                  input: { pattern: "**/*.pdf", path: "/root" },
+                  output: "No files found",
+                  metadata: {},
+                  title: "root",
+                  time: { start: 12, end: 13 },
+                },
+              },
+              {
+                id: "p_text",
+                sessionID,
+                messageID: "msg_call",
+                type: "text",
+                text: "I checked your home directory and looked for PDF files.",
+                time: { start: 14, end: 15 },
+              },
+            ],
+          },
+        ] as any[]
+
+        await drain({
+          user,
+          sessionID,
+          model: resolved,
+          agent,
+          system: [],
+          messages: await MessageV2.toModelMessages(input as any, resolved),
+          tools: {
+            read: tool({
+              description: "Stub read tool",
+              inputSchema: z.object({
+                filePath: z.string(),
+              }),
+              execute: async () => ({ output: "stub" }),
+            }),
+            glob: tool({
+              description: "Stub glob tool",
+              inputSchema: z.object({
+                pattern: z.string(),
+                path: z.string().optional(),
+              }),
+              execute: async () => ({ output: "stub" }),
+            }),
+          },
+        })
+
+        const capture = await request
+        const body = capture.body
+
+        expect(capture.url.pathname.endsWith("/messages")).toBe(true)
+        expect(body.messages).toStrictEqual([
+          {
+            role: "user",
+            content: [{ type: "text", text: "Can you check whether there are any PDF files in my home directory?" }],
+          },
+          {
+            role: "assistant",
+            content: [
+              {
+                type: "text",
+                text: "I checked your home directory and looked for PDF files.",
+                cache_control: {
+                  type: "ephemeral",
+                },
+              },
+              {
+                type: "tool_use",
+                id: "toolu_01N8mDEzG8DSTs7UPHFtmgCT",
+                name: "read",
+                input: { filePath: "/root" },
+              },
+              {
+                type: "tool_use",
+                id: "toolu_01APxrADs7VozN8uWzw9WwHr",
+                name: "glob",
+                input: { pattern: "**/*.pdf", path: "/root" },
+              },
+            ],
+          },
+          {
+            role: "user",
+            content: [
+              {
+                type: "tool_result",
+                tool_use_id: "toolu_01N8mDEzG8DSTs7UPHFtmgCT",
+                content: "<path>/root</path>",
+              },
+              {
+                type: "tool_result",
+                tool_use_id: "toolu_01APxrADs7VozN8uWzw9WwHr",
+                content: "No files found",
+                cache_control: {
+                  type: "ephemeral",
+                },
+              },
+            ],
+          },
+        ])
+      },
+    })
+  })
+
   test("sends Google API payload for Gemini models", async () => {
     const server = state.server
     if (!server) {