tool.test.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import { expect, test } from "bun:test"
  2. import { Agent } from "@opencode-ai/schema/agent"
  3. import { Session } from "@opencode-ai/schema/session"
  4. import { SessionMessage } from "@opencode-ai/schema/session-message"
  5. import { Effect, Schema } from "effect"
  6. import * as Tool from "../src/v2/effect/tool"
  7. const context = {
  8. sessionID: Session.ID.make("ses_test"),
  9. agent: Agent.ID.make("build"),
  10. messageID: SessionMessage.ID.make("msg_test"),
  11. callID: "call_test",
  12. progress: () => Effect.void,
  13. } satisfies Tool.Context
  14. test("tools remain valid across separate module instances", async () => {
  15. const ForeignTool = await import(`${new URL("../src/v2/effect/tool.ts", import.meta.url).href}?foreign`)
  16. const config = {
  17. description: "Foreign tool",
  18. input: Schema.Struct({ value: Schema.String }),
  19. output: Schema.Struct({ ok: Schema.Boolean }),
  20. execute: () => Effect.succeed({ ok: true }),
  21. }
  22. const tool = ForeignTool.make(config)
  23. expect(Tool.definition("foreign", tool)).toEqual({
  24. name: "foreign",
  25. description: "Foreign tool",
  26. inputSchema: {
  27. type: "object",
  28. properties: { value: { type: "string" } },
  29. required: ["value"],
  30. additionalProperties: false,
  31. },
  32. outputSchema: {
  33. type: "object",
  34. properties: { ok: { type: "boolean" } },
  35. required: ["ok"],
  36. additionalProperties: false,
  37. },
  38. })
  39. expect(await Effect.runPromise(Tool.settle(tool, { input: { value: "input" } }, context))).toEqual({
  40. structured: { ok: true },
  41. content: [],
  42. })
  43. })
  44. test("portable schemas validate and describe typed tools", async () => {
  45. const input: Tool.StandardSchemaType<{ count: string }, { count: number }> = {
  46. "~standard": {
  47. version: 1,
  48. vendor: "test",
  49. validate: (value) => {
  50. if (typeof value !== "object" || value === null || !("count" in value) || typeof value.count !== "string")
  51. return { issues: [{ message: "count must be numeric" }] }
  52. const count = Number(value.count)
  53. return Number.isFinite(count) ? { value: { count } } : { issues: [{ message: "count must be numeric" }] }
  54. },
  55. jsonSchema: {
  56. input: () => ({ type: "object", properties: { count: { type: "string" } } }),
  57. output: () => ({ type: "object", properties: { count: { type: "number" } } }),
  58. },
  59. },
  60. }
  61. const output: Tool.StandardSchemaType<number, string> = {
  62. "~standard": {
  63. version: 1,
  64. vendor: "test",
  65. validate: (value) => ({ value: String(value) }),
  66. jsonSchema: {
  67. input: () => ({ type: "number" }),
  68. output: () => ({ type: "string" }),
  69. },
  70. },
  71. }
  72. const tool = Tool.make({
  73. description: "Portable tool",
  74. input,
  75. output,
  76. execute: ({ count }) => Effect.succeed(count + 1),
  77. })
  78. expect(Tool.definition("portable", tool)).toEqual({
  79. name: "portable",
  80. description: "Portable tool",
  81. inputSchema: { type: "object", properties: { count: { type: "string" } } },
  82. outputSchema: { type: "string" },
  83. })
  84. expect(await Effect.runPromise(Tool.settle(tool, { input: { count: "41" } }, context))).toEqual({
  85. structured: "42",
  86. content: [{ type: "text", text: "42" }],
  87. })
  88. })
  89. test("portable schema failures become tool failures", async () => {
  90. const input: Tool.StandardSchemaType<string> = {
  91. "~standard": {
  92. version: 1,
  93. vendor: "test",
  94. validate: () => ({ issues: [{ message: "expected a string" }] }),
  95. jsonSchema: {
  96. input: () => ({ type: "string" }),
  97. output: () => ({ type: "string" }),
  98. },
  99. },
  100. }
  101. const tool = Tool.make({
  102. description: "Failing tool",
  103. input,
  104. output: input,
  105. execute: Effect.succeed,
  106. })
  107. const error = await Effect.runPromiseExit(Tool.settle(tool, { input: 1 }, context))
  108. expect(error.toString()).toContain("Invalid tool input: expected a string")
  109. })
  110. test("two-parameter Definition annotations retain their original meaning", () => {
  111. const input = Schema.Struct({ value: Schema.String })
  112. const output = Schema.Struct({ value: Schema.String, internal: Schema.Boolean })
  113. const structured = Schema.Struct({ value: Schema.String })
  114. const tool: Tool.Definition<typeof input, typeof structured> = Tool.make({
  115. description: "Annotated tool",
  116. input,
  117. output,
  118. structured,
  119. toStructuredOutput: ({ output }) => ({ value: output.value }),
  120. execute: ({ value }) => Effect.succeed({ value, internal: true }),
  121. })
  122. expect(tool.structured).toBe(structured)
  123. })