@opencode/ai 0.0.0-beta-19275

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/README.md +682 -0
  2. package/dist/cache-policy.d.ts +2 -0
  3. package/dist/cache-policy.js +129 -0
  4. package/dist/image-client.d.ts +19 -0
  5. package/dist/image-client.js +28 -0
  6. package/dist/image.d.ts +110 -0
  7. package/dist/image.js +103 -0
  8. package/dist/index.d.ts +18 -0
  9. package/dist/index.js +12 -0
  10. package/dist/llm.d.ts +252 -0
  11. package/dist/llm.js +74 -0
  12. package/dist/protocols/anthropic-messages.d.ts +957 -0
  13. package/dist/protocols/anthropic-messages.js +1414 -0
  14. package/dist/protocols/bedrock-converse.d.ts +499 -0
  15. package/dist/protocols/bedrock-converse.js +643 -0
  16. package/dist/protocols/bedrock-event-stream.d.ts +9 -0
  17. package/dist/protocols/bedrock-event-stream.js +97 -0
  18. package/dist/protocols/gemini.d.ts +285 -0
  19. package/dist/protocols/gemini.js +671 -0
  20. package/dist/protocols/google-images.d.ts +30 -0
  21. package/dist/protocols/google-images.js +181 -0
  22. package/dist/protocols/index.d.ts +12 -0
  23. package/dist/protocols/index.js +12 -0
  24. package/dist/protocols/meta-images.d.ts +26 -0
  25. package/dist/protocols/meta-images.js +89 -0
  26. package/dist/protocols/meta-messages.d.ts +350 -0
  27. package/dist/protocols/meta-messages.js +41 -0
  28. package/dist/protocols/meta-responses.d.ts +473 -0
  29. package/dist/protocols/meta-responses.js +173 -0
  30. package/dist/protocols/mistral-chat.d.ts +540 -0
  31. package/dist/protocols/mistral-chat.js +624 -0
  32. package/dist/protocols/open-responses-channel.d.ts +23 -0
  33. package/dist/protocols/open-responses-channel.js +141 -0
  34. package/dist/protocols/open-responses-continuation.d.ts +12 -0
  35. package/dist/protocols/open-responses-continuation.js +161 -0
  36. package/dist/protocols/open-responses.d.ts +1583 -0
  37. package/dist/protocols/open-responses.js +1206 -0
  38. package/dist/protocols/openai-chat.d.ts +860 -0
  39. package/dist/protocols/openai-chat.js +966 -0
  40. package/dist/protocols/openai-compatible-chat.d.ts +109 -0
  41. package/dist/protocols/openai-compatible-chat.js +19 -0
  42. package/dist/protocols/openai-compatible-responses.d.ts +172 -0
  43. package/dist/protocols/openai-compatible-responses.js +18 -0
  44. package/dist/protocols/openai-images.d.ts +32 -0
  45. package/dist/protocols/openai-images.js +183 -0
  46. package/dist/protocols/openai-responses.d.ts +1389 -0
  47. package/dist/protocols/openai-responses.js +260 -0
  48. package/dist/protocols/shared.d.ts +193 -0
  49. package/dist/protocols/shared.js +290 -0
  50. package/dist/protocols/utils/bedrock-auth.d.ts +60 -0
  51. package/dist/protocols/utils/bedrock-auth.js +85 -0
  52. package/dist/protocols/utils/bedrock-cache.d.ts +19 -0
  53. package/dist/protocols/utils/bedrock-cache.js +49 -0
  54. package/dist/protocols/utils/bedrock-media.d.ts +55 -0
  55. package/dist/protocols/utils/bedrock-media.js +73 -0
  56. package/dist/protocols/utils/cache.d.ts +6 -0
  57. package/dist/protocols/utils/cache.js +5 -0
  58. package/dist/protocols/utils/gemini-tool-schema.d.ts +2 -0
  59. package/dist/protocols/utils/gemini-tool-schema.js +103 -0
  60. package/dist/protocols/utils/image-input.d.ts +21 -0
  61. package/dist/protocols/utils/image-input.js +20 -0
  62. package/dist/protocols/utils/lifecycle.d.ts +24 -0
  63. package/dist/protocols/utils/lifecycle.js +72 -0
  64. package/dist/protocols/utils/meta-image.d.ts +2 -0
  65. package/dist/protocols/utils/meta-image.js +13 -0
  66. package/dist/protocols/utils/open-responses-options.d.ts +56 -0
  67. package/dist/protocols/utils/open-responses-options.js +61 -0
  68. package/dist/protocols/utils/openai-image.d.ts +5 -0
  69. package/dist/protocols/utils/openai-image.js +18 -0
  70. package/dist/protocols/utils/openai-options.d.ts +16 -0
  71. package/dist/protocols/utils/openai-options.js +14 -0
  72. package/dist/protocols/utils/partial-json-options.d.ts +62 -0
  73. package/dist/protocols/utils/partial-json-options.js +52 -0
  74. package/dist/protocols/utils/partial-json.d.ts +8 -0
  75. package/dist/protocols/utils/partial-json.js +279 -0
  76. package/dist/protocols/utils/responses-checkpoint.d.ts +4 -0
  77. package/dist/protocols/utils/responses-checkpoint.js +107 -0
  78. package/dist/protocols/utils/responses-compaction.d.ts +4 -0
  79. package/dist/protocols/utils/responses-compaction.js +129 -0
  80. package/dist/protocols/utils/responses-hosted-tools.d.ts +26 -0
  81. package/dist/protocols/utils/responses-hosted-tools.js +32 -0
  82. package/dist/protocols/utils/tool-schema.d.ts +8 -0
  83. package/dist/protocols/utils/tool-schema.js +54 -0
  84. package/dist/protocols/utils/tool-stream.d.ts +710 -0
  85. package/dist/protocols/utils/tool-stream.js +155 -0
  86. package/dist/protocols/xai-images.d.ts +26 -0
  87. package/dist/protocols/xai-images.js +106 -0
  88. package/dist/protocols/xai-responses.d.ts +250 -0
  89. package/dist/protocols/xai-responses.js +81 -0
  90. package/dist/protocols/zai-images.d.ts +22 -0
  91. package/dist/protocols/zai-images.js +79 -0
  92. package/dist/protocols.d.ts +1 -0
  93. package/dist/protocols.js +1 -0
  94. package/dist/provider-error.d.ts +15 -0
  95. package/dist/provider-error.js +125 -0
  96. package/dist/provider-package.d.ts +11 -0
  97. package/dist/provider-package.js +1 -0
  98. package/dist/provider.d.ts +23 -0
  99. package/dist/provider.js +2 -0
  100. package/dist/providers/amazon-bedrock/mantle/chat.d.ts +2 -0
  101. package/dist/providers/amazon-bedrock/mantle/chat.js +1 -0
  102. package/dist/providers/amazon-bedrock/mantle/responses.d.ts +2 -0
  103. package/dist/providers/amazon-bedrock/mantle/responses.js +1 -0
  104. package/dist/providers/amazon-bedrock/mantle.d.ts +2 -0
  105. package/dist/providers/amazon-bedrock/mantle.js +1 -0
  106. package/dist/providers/amazon-bedrock-mantle.d.ts +371 -0
  107. package/dist/providers/amazon-bedrock-mantle.js +76 -0
  108. package/dist/providers/amazon-bedrock.d.ts +173 -0
  109. package/dist/providers/amazon-bedrock.js +41 -0
  110. package/dist/providers/anthropic-compatible.d.ts +304 -0
  111. package/dist/providers/anthropic-compatible.js +44 -0
  112. package/dist/providers/anthropic.d.ts +302 -0
  113. package/dist/providers/anthropic.js +39 -0
  114. package/dist/providers/azure/chat.d.ts +2 -0
  115. package/dist/providers/azure/chat.js +1 -0
  116. package/dist/providers/azure/responses.d.ts +2 -0
  117. package/dist/providers/azure/responses.js +1 -0
  118. package/dist/providers/azure.d.ts +599 -0
  119. package/dist/providers/azure.js +115 -0
  120. package/dist/providers/baseten.d.ts +229 -0
  121. package/dist/providers/baseten.js +38 -0
  122. package/dist/providers/cerebras.d.ts +228 -0
  123. package/dist/providers/cerebras.js +40 -0
  124. package/dist/providers/cloudflare-ai-gateway.d.ts +238 -0
  125. package/dist/providers/cloudflare-ai-gateway.js +59 -0
  126. package/dist/providers/cloudflare-workers-ai.d.ts +232 -0
  127. package/dist/providers/cloudflare-workers-ai.js +45 -0
  128. package/dist/providers/deepinfra.d.ts +228 -0
  129. package/dist/providers/deepinfra.js +43 -0
  130. package/dist/providers/deepseek.d.ts +229 -0
  131. package/dist/providers/deepseek.js +41 -0
  132. package/dist/providers/fireworks.d.ts +229 -0
  133. package/dist/providers/fireworks.js +38 -0
  134. package/dist/providers/google-vertex/chat.d.ts +2 -0
  135. package/dist/providers/google-vertex/chat.js +1 -0
  136. package/dist/providers/google-vertex/gemini.d.ts +2 -0
  137. package/dist/providers/google-vertex/gemini.js +1 -0
  138. package/dist/providers/google-vertex/messages.d.ts +2 -0
  139. package/dist/providers/google-vertex/messages.js +1 -0
  140. package/dist/providers/google-vertex/responses.d.ts +2 -0
  141. package/dist/providers/google-vertex/responses.js +1 -0
  142. package/dist/providers/google-vertex-chat.d.ts +133 -0
  143. package/dist/providers/google-vertex-chat.js +55 -0
  144. package/dist/providers/google-vertex-messages.d.ts +301 -0
  145. package/dist/providers/google-vertex-messages.js +75 -0
  146. package/dist/providers/google-vertex-responses.d.ts +198 -0
  147. package/dist/providers/google-vertex-responses.js +56 -0
  148. package/dist/providers/google-vertex-shared.d.ts +22 -0
  149. package/dist/providers/google-vertex-shared.js +61 -0
  150. package/dist/providers/google-vertex.d.ts +120 -0
  151. package/dist/providers/google-vertex.js +92 -0
  152. package/dist/providers/google.d.ts +115 -0
  153. package/dist/providers/google.js +42 -0
  154. package/dist/providers/groq.d.ts +315 -0
  155. package/dist/providers/groq.js +81 -0
  156. package/dist/providers/index.d.ts +28 -0
  157. package/dist/providers/index.js +28 -0
  158. package/dist/providers/meta/chat.d.ts +2 -0
  159. package/dist/providers/meta/chat.js +1 -0
  160. package/dist/providers/meta/messages.d.ts +2 -0
  161. package/dist/providers/meta/messages.js +1 -0
  162. package/dist/providers/meta/responses.d.ts +2 -0
  163. package/dist/providers/meta/responses.js +1 -0
  164. package/dist/providers/meta.d.ts +631 -0
  165. package/dist/providers/meta.js +113 -0
  166. package/dist/providers/minimax/chat.d.ts +4 -0
  167. package/dist/providers/minimax/chat.js +8 -0
  168. package/dist/providers/minimax/messages.d.ts +1 -0
  169. package/dist/providers/minimax/messages.js +1 -0
  170. package/dist/providers/minimax/responses.d.ts +4 -0
  171. package/dist/providers/minimax/responses.js +8 -0
  172. package/dist/providers/minimax.d.ts +593 -0
  173. package/dist/providers/minimax.js +89 -0
  174. package/dist/providers/mistral.d.ts +208 -0
  175. package/dist/providers/mistral.js +28 -0
  176. package/dist/providers/open-responses-options.d.ts +6 -0
  177. package/dist/providers/open-responses-options.js +1 -0
  178. package/dist/providers/openai/chat.d.ts +2 -0
  179. package/dist/providers/openai/chat.js +1 -0
  180. package/dist/providers/openai/responses.d.ts +2 -0
  181. package/dist/providers/openai/responses.js +1 -0
  182. package/dist/providers/openai-compatible/responses.d.ts +1 -0
  183. package/dist/providers/openai-compatible/responses.js +1 -0
  184. package/dist/providers/openai-compatible-responses.d.ts +196 -0
  185. package/dist/providers/openai-compatible-responses.js +32 -0
  186. package/dist/providers/openai-compatible.d.ts +131 -0
  187. package/dist/providers/openai-compatible.js +33 -0
  188. package/dist/providers/openai-options.d.ts +26 -0
  189. package/dist/providers/openai-options.js +41 -0
  190. package/dist/providers/openai.d.ts +405 -0
  191. package/dist/providers/openai.js +82 -0
  192. package/dist/providers/openrouter.d.ts +564 -0
  193. package/dist/providers/openrouter.js +114 -0
  194. package/dist/providers/togetherai.d.ts +228 -0
  195. package/dist/providers/togetherai.js +40 -0
  196. package/dist/providers/xai.d.ts +164 -0
  197. package/dist/providers/xai.js +84 -0
  198. package/dist/providers/zai.d.ts +24 -0
  199. package/dist/providers/zai.js +21 -0
  200. package/dist/providers.d.ts +1 -0
  201. package/dist/providers.js +1 -0
  202. package/dist/route/auth-options.d.ts +34 -0
  203. package/dist/route/auth-options.js +14 -0
  204. package/dist/route/auth.d.ts +51 -0
  205. package/dist/route/auth.js +87 -0
  206. package/dist/route/client.d.ts +430 -0
  207. package/dist/route/client.js +304 -0
  208. package/dist/route/endpoint.d.ts +28 -0
  209. package/dist/route/endpoint.js +21 -0
  210. package/dist/route/executor.d.ts +27 -0
  211. package/dist/route/executor.js +183 -0
  212. package/dist/route/framing.d.ts +29 -0
  213. package/dist/route/framing.js +14 -0
  214. package/dist/route/index.d.ts +16 -0
  215. package/dist/route/index.js +9 -0
  216. package/dist/route/protocol.d.ts +76 -0
  217. package/dist/route/protocol.js +16 -0
  218. package/dist/route/transport/http.d.ts +34 -0
  219. package/dist/route/transport/http.js +68 -0
  220. package/dist/route/transport/index.d.ts +41 -0
  221. package/dist/route/transport/index.js +4 -0
  222. package/dist/route/transport/websocket-channel.d.ts +57 -0
  223. package/dist/route/transport/websocket-channel.js +1 -0
  224. package/dist/route/transport/websocket.d.ts +56 -0
  225. package/dist/route/transport/websocket.js +366 -0
  226. package/dist/route.d.ts +1 -0
  227. package/dist/route.js +1 -0
  228. package/dist/schema/errors.d.ts +158 -0
  229. package/dist/schema/errors.js +120 -0
  230. package/dist/schema/events.d.ts +8123 -0
  231. package/dist/schema/events.js +550 -0
  232. package/dist/schema/ids.d.ts +17 -0
  233. package/dist/schema/ids.js +10 -0
  234. package/dist/schema/index.d.ts +5 -0
  235. package/dist/schema/index.js +5 -0
  236. package/dist/schema/messages.d.ts +646 -0
  237. package/dist/schema/messages.js +301 -0
  238. package/dist/schema/options.d.ts +157 -0
  239. package/dist/schema/options.js +187 -0
  240. package/dist/testing.d.ts +1693 -0
  241. package/dist/testing.js +129 -0
  242. package/dist/tool-history.d.ts +2 -0
  243. package/dist/tool-history.js +84 -0
  244. package/dist/tool-runtime.d.ts +15 -0
  245. package/dist/tool-runtime.js +62 -0
  246. package/dist/tool.d.ts +136 -0
  247. package/dist/tool.js +66 -0
  248. package/dist/utils/record.d.ts +2 -0
  249. package/dist/utils/record.js +2 -0
  250. package/dist/utils/sanitize.d.ts +1 -0
  251. package/dist/utils/sanitize.js +12 -0
  252. package/package.json +48 -0
@@ -0,0 +1,129 @@
1
+ export * as TestLLM from "./testing.js";
2
+ import { LLMClient, } from "./route/client.js";
3
+ import { LLMEvent, LLMResponse, CompactionResponse, CompactionCheckpointResponse, } from "./schema/index.js";
4
+ import { Context, Deferred, Effect, Latch, Layer, Queue, Scope, Stream } from "effect";
5
+ export class Test extends Context.Service()("@opencode/ai/TestLLM/Test") {
6
+ }
7
+ /** @deprecated Use Test and testLayer for normal client methods and test controls. */
8
+ export class Service extends Context.Service()("@opencode/ai/TestLLM") {
9
+ }
10
+ export const complete = (options, ...events) => [
11
+ LLMEvent.stepStart({ index: 0 }),
12
+ ...events,
13
+ LLMEvent.stepFinish({
14
+ index: 0,
15
+ reason: options.reason,
16
+ usage: options.usage,
17
+ providerMetadata: options.providerMetadata,
18
+ }),
19
+ LLMEvent.finish({ reason: options.reason, providerMetadata: options.providerMetadata }),
20
+ ];
21
+ export const stop = (...events) => complete({ reason: { normalized: "stop" } }, ...events);
22
+ export const toolCalls = (...events) => complete({ reason: { normalized: "tool-calls" } }, ...events);
23
+ const textEvents = (value, id) => [
24
+ LLMEvent.textStart({ id }),
25
+ LLMEvent.textDelta({ id, text: value }),
26
+ LLMEvent.textEnd({ id }),
27
+ ];
28
+ export const text = (value, id) => stop(...textEvents(value, id));
29
+ export const textWithUsage = (value, id, inputTokens) => complete({ reason: { normalized: "stop" }, usage: { inputTokens, nonCachedInputTokens: inputTokens } }, ...textEvents(value, id));
30
+ export const tool = (id, name, input) => toolCalls(LLMEvent.toolCall({ id, name, input }));
31
+ export const failAfter = (error, ...events) => Stream.fromIterable(events).pipe(Stream.concat(Stream.fail(error)));
32
+ export const hangAfter = (...events) => Stream.concat(Stream.fromIterable(events), Stream.never);
33
+ const make = (options) => Effect.sync(() => {
34
+ const requests = [];
35
+ const responses = [];
36
+ let started = Deferred.makeUnsafe();
37
+ let fallback = options.fallback;
38
+ let activeGate;
39
+ const wait = (count) => Effect.suspend(() => requests.length >= count ? Effect.void : Deferred.await(started).pipe(Effect.andThen(wait(count))));
40
+ const take = (request) => Effect.suspend(() => {
41
+ const count = requests.push(options.transformRequest?.(request) ?? request);
42
+ const waiting = started;
43
+ started = Deferred.makeUnsafe();
44
+ const gate = activeGate;
45
+ try {
46
+ const response = responses.shift() ?? (typeof fallback === "function" ? fallback(request) : fallback);
47
+ if (!response)
48
+ return Effect.die(new Error(`TestLLM has no response for request ${count}`));
49
+ if (!gate)
50
+ return Effect.succeed(response);
51
+ return Queue.offer(gate.started, undefined).pipe(Effect.andThen(gate.release.await), Effect.as(response));
52
+ }
53
+ finally {
54
+ // Waiters can resume synchronously; assign the reply and gate before notifying them.
55
+ Deferred.doneUnsafe(waiting, Effect.void);
56
+ }
57
+ });
58
+ const stream = (request) => Stream.unwrap(take(request).pipe(Effect.map((response) => {
59
+ if (response instanceof CompactionResponse || response instanceof CompactionCheckpointResponse)
60
+ return Stream.die("TestLLM generation requires an event response");
61
+ return Stream.isStream(response) ? response : Stream.fromIterable(response);
62
+ })));
63
+ function compact(request, options) {
64
+ return take(request).pipe(Effect.flatMap((response) => {
65
+ if (options?.mechanism === "trigger")
66
+ return response instanceof CompactionCheckpointResponse
67
+ ? Effect.succeed(response)
68
+ : Effect.die("TestLLM trigger compaction requires a CompactionCheckpointResponse");
69
+ return response instanceof CompactionResponse
70
+ ? Effect.succeed(response)
71
+ : Effect.die("TestLLM compaction requires a CompactionResponse");
72
+ }));
73
+ }
74
+ const test = Test.of({
75
+ compact,
76
+ stream,
77
+ generate: (request) => stream(request).pipe(Stream.runFold(LLMResponse.empty, LLMResponse.reduce), Effect.flatMap((state) => {
78
+ const response = LLMResponse.complete(state);
79
+ if (response)
80
+ return Effect.succeed(response);
81
+ return Effect.die("TestLLM response ended without a terminal finish event");
82
+ })),
83
+ requests: () => Effect.sync(() => [...requests]),
84
+ push: (...input) => Effect.sync(() => {
85
+ responses.push(...input);
86
+ }),
87
+ always: (response) => Effect.sync(() => {
88
+ fallback = response;
89
+ }),
90
+ serve: (responder) => Effect.sync(() => {
91
+ fallback = responder;
92
+ }),
93
+ wait,
94
+ gate: () => Effect.gen(function* () {
95
+ const gate = {
96
+ started: yield* Effect.acquireRelease(Queue.unbounded(), Queue.shutdown),
97
+ release: yield* Latch.make(),
98
+ };
99
+ activeGate = gate;
100
+ const release = Effect.sync(() => {
101
+ if (activeGate === gate)
102
+ activeGate = undefined;
103
+ }).pipe(Effect.andThen(gate.release.open), Effect.asVoid);
104
+ yield* Effect.addFinalizer(() => release);
105
+ return {
106
+ started: Queue.take(gate.started),
107
+ release,
108
+ };
109
+ }),
110
+ });
111
+ return { test, requests };
112
+ });
113
+ /** Provides one shared implementation under the normal client and test-control tags. */
114
+ export const testLayer = (options = {}) => Layer.effectContext(Effect.map(make(options), (implementation) => Context.make(LLMClient.Service, implementation.test).pipe(Context.add(Test, implementation.test))));
115
+ /** @deprecated Use testLayer; retained for published callers of the legacy control interface. */
116
+ export const layer = (options = {}) => Layer.effect(Service, Effect.map(make(options), (implementation) => Service.of({
117
+ requests: implementation.requests,
118
+ push: implementation.test.push,
119
+ always: implementation.test.always,
120
+ wait: implementation.test.wait,
121
+ gate: implementation.test.gate(),
122
+ client: implementation.test,
123
+ })));
124
+ /** @deprecated testLayer provides LLMClient.Service directly. */
125
+ export const clientLayer = Layer.effect(LLMClient.Service, Effect.map(Service, (service) => service.client));
126
+ export const push = (...responses) => Service.use((service) => service.push(...responses));
127
+ export const always = (response) => Service.use((service) => service.always(response));
128
+ export const wait = (count) => Service.use((service) => service.wait(count));
129
+ export const gate = Service.use((service) => service.gate);
@@ -0,0 +1,2 @@
1
+ import { Message } from "./schema/messages.js";
2
+ export declare function normalizeToolHistory(messages: ReadonlyArray<Message>): readonly Message[];
@@ -0,0 +1,84 @@
1
+ import { Message, ToolResultPart } from "./schema/messages.js";
2
+ const EMPTY_TOOL_OUTPUT = "(no tool output)";
3
+ const MISSING_TOOL_RESULT = "Tool result missing";
4
+ export function normalizeToolHistory(messages) {
5
+ const normalized = [];
6
+ const pending = new Map();
7
+ const appendMissingResults = () => {
8
+ if (pending.size === 0)
9
+ return;
10
+ normalized.push(missingToolResults(pending.values()));
11
+ pending.clear();
12
+ };
13
+ for (const message of messages) {
14
+ if (message.role === "user" || message.role === "assistant")
15
+ appendMissingResults();
16
+ if (message.role === "tool") {
17
+ const tool = normalizeToolMessage(message, pending);
18
+ if (tool)
19
+ normalized.push(tool);
20
+ continue;
21
+ }
22
+ normalized.push(message);
23
+ if (message.role !== "assistant")
24
+ continue;
25
+ for (const part of message.content) {
26
+ if (part.type === "tool-call" && part.providerExecuted !== true)
27
+ pending.set(part.id, part);
28
+ }
29
+ }
30
+ return normalized.length === messages.length && normalized.every((message, index) => message === messages[index])
31
+ ? messages
32
+ : normalized;
33
+ }
34
+ function missingToolResults(calls) {
35
+ return new Message({
36
+ role: "tool",
37
+ content: [...calls].map((call) => ToolResultPart.make({
38
+ id: call.id,
39
+ name: call.name,
40
+ namespace: call.namespace,
41
+ result: MISSING_TOOL_RESULT,
42
+ resultType: "error",
43
+ })),
44
+ });
45
+ }
46
+ function normalizeToolMessage(message, pending) {
47
+ const content = message.content.map((part) => {
48
+ if (part.type !== "tool-result" || part.providerExecuted === true)
49
+ return part;
50
+ const call = pending.get(part.id);
51
+ if (call)
52
+ pending.delete(part.id);
53
+ return normalizeToolResult(part, call);
54
+ });
55
+ if (content.length === 0)
56
+ return undefined;
57
+ if (content.every((part, index) => part === message.content[index]))
58
+ return message;
59
+ return new Message({
60
+ id: message.id,
61
+ role: message.role,
62
+ content,
63
+ metadata: message.metadata,
64
+ providerMetadata: message.providerMetadata,
65
+ native: message.native,
66
+ });
67
+ }
68
+ function normalizeToolResult(part, call) {
69
+ const named = call === undefined || (part.name === call.name && part.namespace === call.namespace)
70
+ ? part
71
+ : { ...part, name: call.name, namespace: call.namespace };
72
+ if (named.result.type === "text" && named.result.value === "")
73
+ return { ...named, result: { type: "text", value: EMPTY_TOOL_OUTPUT } };
74
+ if (named.result.type === "error" && named.result.value === "")
75
+ return { ...named, result: { type: "error", value: EMPTY_TOOL_OUTPUT } };
76
+ if (named.result.type !== "content")
77
+ return named;
78
+ const value = named.result.value.filter((item) => item.type !== "text" || item.text !== "");
79
+ if (value.length === 0)
80
+ return { ...named, result: { type: "text", value: EMPTY_TOOL_OUTPUT } };
81
+ if (value.length === named.result.value.length)
82
+ return named;
83
+ return { ...named, result: { type: "content", value } };
84
+ }
@@ -0,0 +1,15 @@
1
+ import { Effect } from "effect";
2
+ import { LLMEvent, type ToolCallPart, type ToolOutput as ToolOutputType, type ToolResultValue as ToolResultValueType } from "./schema/index.js";
3
+ import { type Tools } from "./tool.js";
4
+ export interface ToolSettlement {
5
+ readonly result: ToolResultValueType;
6
+ readonly output?: ToolOutputType;
7
+ }
8
+ export interface DispatchResult extends ToolSettlement {
9
+ readonly events: ReadonlyArray<LLMEvent>;
10
+ }
11
+ /** Execute one canonical tool call without owning provider IO or continuation. */
12
+ export declare const dispatch: (tools: Tools, call: ToolCallPart) => Effect.Effect<DispatchResult>;
13
+ export declare const ToolRuntime: {
14
+ readonly dispatch: (tools: Tools, call: ToolCallPart) => Effect.Effect<DispatchResult>;
15
+ };
@@ -0,0 +1,62 @@
1
+ import { Effect } from "effect";
2
+ import { LLMEvent, ToolFailure, ToolOutput, ToolResultValue, } from "./schema/index.js";
3
+ import {} from "./tool.js";
4
+ /** Execute one canonical tool call without owning provider IO or continuation. */
5
+ export const dispatch = (tools, call) => {
6
+ const name = call.namespace === undefined ? call.name : `${call.namespace}.${call.name}`;
7
+ const tool = tools[name];
8
+ if (!tool)
9
+ return Effect.succeed(result(call, { type: "error", value: `Unknown tool: ${name}` }));
10
+ if (!tool.execute)
11
+ return Effect.succeed(result(call, { type: "error", value: `Tool has no execute handler: ${name}` }));
12
+ return decodeAndExecute(tool, call).pipe(Effect.map((value) => result(call, value)), Effect.catchTag("Tool.Error", (failure) => Effect.succeed(result(call, { type: "error", value: failure.message }, failure.error))));
13
+ };
14
+ const decodeAndExecute = (tool, call) => tool._decode(call.input).pipe(Effect.mapError((error) => new ToolFailure({ message: `Invalid tool input: ${error.message}` })), Effect.flatMap((decoded) => tool.execute(decoded, {
15
+ id: call.id,
16
+ name: call.name,
17
+ namespace: call.namespace,
18
+ }).pipe(Effect.flatMap((value) => tool._encode(value).pipe(Effect.mapError((error) => new ToolFailure({
19
+ message: `Tool returned an invalid value for its success schema: ${error.message}`,
20
+ })))), Effect.map((encoded) => {
21
+ if (tool._legacyResult && ToolResultValue.is(encoded))
22
+ return { result: encoded, output: ToolOutput.fromResultValue(encoded) };
23
+ const output = tool._project(decoded, call.id, encoded);
24
+ const result = ToolOutput.toResultValue(output);
25
+ return result.type === "error" ? { result } : { result, output };
26
+ }))));
27
+ const result = (call, value, error) => {
28
+ const settlement = ToolResultValue.is(value) ? { result: value } : value;
29
+ return {
30
+ result: settlement.result,
31
+ output: settlement.output,
32
+ events: settlement.result.type === "error"
33
+ ? [
34
+ LLMEvent.toolError({
35
+ id: call.id,
36
+ name: call.name,
37
+ namespace: call.namespace,
38
+ message: String(settlement.result.value),
39
+ error,
40
+ providerMetadata: call.providerMetadata,
41
+ }),
42
+ LLMEvent.toolResult({
43
+ id: call.id,
44
+ name: call.name,
45
+ namespace: call.namespace,
46
+ result: settlement.result,
47
+ providerMetadata: call.providerMetadata,
48
+ }),
49
+ ]
50
+ : [
51
+ LLMEvent.toolResult({
52
+ id: call.id,
53
+ name: call.name,
54
+ namespace: call.namespace,
55
+ result: settlement.result,
56
+ output: settlement.output,
57
+ providerMetadata: call.providerMetadata,
58
+ }),
59
+ ],
60
+ };
61
+ };
62
+ export const ToolRuntime = { dispatch };
package/dist/tool.d.ts ADDED
@@ -0,0 +1,136 @@
1
+ import { Effect, JsonSchema, Schema } from "effect";
2
+ import { Tool } from "@opencode/schema/tool";
3
+ import type { ToolCallPart, ToolDefinition as ToolDefinitionClass, ToolOutput as ToolOutputType } from "./schema/index.js";
4
+ import { ToolFailure } from "./schema/index.js";
5
+ /**
6
+ * Schema constraint for tool parameters / success values: no decoding or
7
+ * encoding services are allowed. Tools should be self-contained — anything
8
+ * beyond pure data conversion belongs in the handler closure.
9
+ */
10
+ export type ToolSchema<T> = Schema.Codec<T, any, never, never>;
11
+ export interface ToolExecuteContext {
12
+ readonly id: ToolCallPart["id"];
13
+ readonly name: ToolCallPart["name"];
14
+ readonly namespace?: ToolCallPart["namespace"];
15
+ }
16
+ export type ToolExecute<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = (params: Schema.Schema.Type<Parameters>, context?: ToolExecuteContext) => Effect.Effect<Schema.Schema.Type<Success>, ToolFailure>;
17
+ export interface ToolModelOutputInput<Parameters, Output> {
18
+ readonly id: ToolCallPart["id"];
19
+ readonly parameters: Parameters;
20
+ readonly output: Output;
21
+ }
22
+ export type ToolToModelOutput<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = (input: ToolModelOutputInput<Schema.Schema.Type<Parameters>, Success["Encoded"]>) => ReadonlyArray<Tool.Content>;
23
+ /**
24
+ * A type-safe LLM tool. Each tool bundles its own description, parameter
25
+ * Schema and success Schema. The execute handler is optional: omit it when you
26
+ * only want to expose a tool schema to the model and handle tool calls outside
27
+ * this package.
28
+ *
29
+ * Errors must be expressed as `ToolFailure`. Unmapped errors and defects fail
30
+ * the stream.
31
+ *
32
+ * Internally each tool also carries memoized codecs and a precomputed
33
+ * `ToolDefinition` so callers do not rebuild them per invocation.
34
+ */
35
+ export interface Definition<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> {
36
+ readonly description: string;
37
+ readonly parameters: Parameters;
38
+ readonly success: Success;
39
+ readonly execute?: ToolExecute<Parameters, Success>;
40
+ readonly toModelOutput?: ToolToModelOutput<Parameters, Success>;
41
+ readonly toStructuredOutput?: (output: Success["Encoded"]) => unknown;
42
+ /** @internal */
43
+ readonly _decode: (input: unknown) => Effect.Effect<Schema.Schema.Type<Parameters>, Schema.SchemaError>;
44
+ /** @internal */
45
+ readonly _encode: (value: Schema.Schema.Type<Success>) => Effect.Effect<unknown, Schema.SchemaError>;
46
+ /** @internal */
47
+ readonly _project: (parameters: Schema.Schema.Type<Parameters>, id: ToolCallPart["id"], output: unknown) => ToolOutputType;
48
+ /** @internal */
49
+ readonly _legacyResult: boolean;
50
+ /** @internal */
51
+ readonly _definition: ToolDefinitionClass;
52
+ }
53
+ export type AnyTool = Definition<any, any>;
54
+ export type ExecutableTool<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = Definition<Parameters, Success> & {
55
+ readonly execute: ToolExecute<Parameters, Success>;
56
+ };
57
+ export type AnyExecutableTool = ExecutableTool<any, any>;
58
+ export type ExecutableTools = Record<string, AnyExecutableTool>;
59
+ /**
60
+ * Constructs a tool. Two input modes:
61
+ *
62
+ * 1. **Typed** — pass Effect `parameters` and `success` Schemas; inputs and
63
+ * outputs are statically typed and decoded/encoded automatically.
64
+ *
65
+ * ```ts
66
+ * Tool.make({
67
+ * description: "Get current weather",
68
+ * parameters: Schema.Struct({ city: Schema.String }),
69
+ * success: Schema.Struct({ temperature: Schema.Number }),
70
+ * execute: ({ city }) => Effect.succeed({ temperature: 22 }),
71
+ * })
72
+ * ```
73
+ *
74
+ * 2. **Dynamic** — pass raw JSON Schema as `jsonSchema`. Use this when the
75
+ * schema comes from an external source (MCP server, plugin manifest,
76
+ * dynamic config) and is not known at compile time. Inputs are typed as
77
+ * `unknown`; the handler is responsible for any validation it needs.
78
+ *
79
+ * ```ts
80
+ * Tool.make({
81
+ * description: "Look something up",
82
+ * jsonSchema: { type: "object", properties: { ... } },
83
+ * execute: (params) => Effect.succeed(...),
84
+ * })
85
+ * ```
86
+ *
87
+ * In both modes the produced tool flows through `toDefinitions(...)`
88
+ * identically.
89
+ */
90
+ export declare function make<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>>(config: {
91
+ readonly description: string;
92
+ readonly parameters: Parameters;
93
+ readonly success: Success;
94
+ readonly execute: ToolExecute<Parameters, Success>;
95
+ readonly toModelOutput?: ToolToModelOutput<Parameters, Success>;
96
+ readonly toStructuredOutput?: (output: Success["Encoded"]) => unknown;
97
+ }): ExecutableTool<Parameters, Success>;
98
+ export declare function make<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>>(config: {
99
+ readonly description: string;
100
+ readonly parameters: Parameters;
101
+ readonly success: Success;
102
+ readonly execute?: undefined;
103
+ readonly toModelOutput?: ToolToModelOutput<Parameters, Success>;
104
+ readonly toStructuredOutput?: (output: Success["Encoded"]) => unknown;
105
+ }): Definition<Parameters, Success>;
106
+ export declare function make(config: {
107
+ readonly description: string;
108
+ readonly jsonSchema: JsonSchema.JsonSchema;
109
+ readonly outputSchema?: JsonSchema.JsonSchema;
110
+ readonly execute: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure>;
111
+ readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<Tool.Content>;
112
+ readonly toStructuredOutput?: (output: unknown) => unknown;
113
+ }): AnyExecutableTool;
114
+ export declare function make(config: {
115
+ readonly description: string;
116
+ readonly jsonSchema: JsonSchema.JsonSchema;
117
+ readonly outputSchema?: JsonSchema.JsonSchema;
118
+ readonly execute?: undefined;
119
+ readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<Tool.Content>;
120
+ readonly toStructuredOutput?: (output: unknown) => unknown;
121
+ }): AnyTool;
122
+ /**
123
+ * A record of named tools. The record key becomes the tool name on the wire.
124
+ */
125
+ export type Tools = Record<string, AnyTool>;
126
+ /**
127
+ * Convert a tools record into the `ToolDefinition[]` shape that
128
+ * `LLMRequest.tools` expects.
129
+ *
130
+ * Tool names come from the record keys, so the per-tool cached
131
+ * `_definition` is rebuilt with the correct name here. The JSON Schema body
132
+ * is reused.
133
+ */
134
+ export declare const toDefinitions: (tools: Tools) => ReadonlyArray<ToolDefinitionClass>;
135
+ export { ToolFailure };
136
+ export * as Tool from "./tool.js";
package/dist/tool.js ADDED
@@ -0,0 +1,66 @@
1
+ import { Effect, JsonSchema, Schema } from "effect";
2
+ import { Tool } from "@opencode/schema/tool";
3
+ import { ToolDefinition, ToolFailure, ToolOutput } from "./schema/index.js";
4
+ export function make(config) {
5
+ if ("jsonSchema" in config) {
6
+ return {
7
+ description: config.description,
8
+ parameters: Schema.Unknown,
9
+ success: Schema.Unknown,
10
+ execute: config.execute,
11
+ toModelOutput: config.toModelOutput,
12
+ toStructuredOutput: config.toStructuredOutput,
13
+ _decode: Effect.succeed,
14
+ _encode: Effect.succeed,
15
+ _project: (parameters, id, output) => project(config.toModelOutput, config.toStructuredOutput, parameters, id, output),
16
+ _legacyResult: config.toModelOutput === undefined && config.toStructuredOutput === undefined,
17
+ _definition: new ToolDefinition({
18
+ name: "",
19
+ description: config.description,
20
+ inputSchema: config.jsonSchema,
21
+ outputSchema: config.outputSchema,
22
+ }),
23
+ };
24
+ }
25
+ return {
26
+ description: config.description,
27
+ parameters: config.parameters,
28
+ success: config.success,
29
+ execute: config.execute,
30
+ toModelOutput: config.toModelOutput,
31
+ toStructuredOutput: config.toStructuredOutput,
32
+ _decode: Schema.decodeUnknownEffect(config.parameters),
33
+ _encode: Schema.encodeEffect(config.success),
34
+ _project: (parameters, id, output) => project(config.toModelOutput, config.toStructuredOutput, parameters, id, output),
35
+ _legacyResult: false,
36
+ _definition: new ToolDefinition({
37
+ name: "",
38
+ description: config.description,
39
+ inputSchema: toJsonSchema(config.parameters),
40
+ outputSchema: toJsonSchema(config.success),
41
+ }),
42
+ };
43
+ }
44
+ /**
45
+ * Convert a tools record into the `ToolDefinition[]` shape that
46
+ * `LLMRequest.tools` expects.
47
+ *
48
+ * Tool names come from the record keys, so the per-tool cached
49
+ * `_definition` is rebuilt with the correct name here. The JSON Schema body
50
+ * is reused.
51
+ */
52
+ export const toDefinitions = (tools) => Object.entries(tools).map(([name, item]) => new ToolDefinition({
53
+ name,
54
+ description: item._definition.description,
55
+ inputSchema: item._definition.inputSchema,
56
+ outputSchema: item._definition.outputSchema,
57
+ }));
58
+ const toJsonSchema = (schema) => {
59
+ const document = Schema.toJsonSchemaDocument(schema);
60
+ if (Object.keys(document.definitions).length === 0)
61
+ return document.schema;
62
+ return { ...document.schema, $defs: document.definitions };
63
+ };
64
+ const project = (toModelOutput, toStructuredOutput, parameters, id, output) => ToolOutput.make(toStructuredOutput?.(output) ?? output, toModelOutput?.({ id, parameters, output }) ?? (typeof output === "string" ? [{ type: "text", text: output }] : []));
65
+ export { ToolFailure };
66
+ export * as Tool from "./tool.js";
@@ -0,0 +1,2 @@
1
+ /** Plain-record narrowing. Excludes arrays so JSON object checks don't accept tuples as key/value bags. */
2
+ export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
@@ -0,0 +1,2 @@
1
+ /** Plain-record narrowing. Excludes arrays so JSON object checks don't accept tuples as key/value bags. */
2
+ export const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -0,0 +1 @@
1
+ export declare const sanitizeSurrogates: <T>(value: T) => T;
@@ -0,0 +1,12 @@
1
+ import { isRecord } from "./record.js";
2
+ export const sanitizeSurrogates = (value) => {
3
+ if (typeof value === "string")
4
+ return value.toWellFormed();
5
+ if (Array.isArray(value))
6
+ return value.map(sanitizeSurrogates);
7
+ if (value instanceof Uint8Array || value instanceof Error)
8
+ return value;
9
+ if (isRecord(value))
10
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key.toWellFormed(), sanitizeSurrogates(entry)]));
11
+ return value;
12
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/package.json",
3
+ "version": "0.0.0-beta-19275",
4
+ "name": "@opencode/ai",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "scripts": {
8
+ "setup:recording-env": "bun run script/setup-recording-env.ts",
9
+ "test": "bun test --timeout 30000 --only-failures",
10
+ "typecheck": "tsgo --noEmit && tsgo --noEmit -p tsconfig.types.json",
11
+ "build": "tsc -p tsconfig.build.json"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "types": "./dist/index.d.ts"
20
+ },
21
+ "./testing": {
22
+ "import": "./dist/testing.js",
23
+ "types": "./dist/testing.d.ts"
24
+ },
25
+ "./*": {
26
+ "import": "./dist/*.js",
27
+ "types": "./dist/*.d.ts"
28
+ }
29
+ },
30
+ "devDependencies": {
31
+ "@clack/prompts": "1.0.0-alpha.1",
32
+ "@effect/platform-node": "4.0.0-rc.112",
33
+ "@opencode/http-recorder": "0.0.0-beta-19275",
34
+ "@tsconfig/bun": "1.0.9",
35
+ "@types/bun": "1.4.0",
36
+ "@typescript/native-preview": "7.0.0-dev.20251207.1",
37
+ "typescript": "5.8.2"
38
+ },
39
+ "dependencies": {
40
+ "@aws-sdk/credential-providers": "3.1057.0",
41
+ "@smithy/eventstream-codec": "4.2.14",
42
+ "@smithy/util-utf8": "4.2.2",
43
+ "@opencode/schema": "0.0.0-beta-19275",
44
+ "aws4fetch": "1.0.20",
45
+ "effect": "4.0.0-rc.112",
46
+ "google-auth-library": "10.5.0"
47
+ }
48
+ }