@opencode-ai/ai 0.0.0-next-17274 → 0.0.0-next-17288

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.
@@ -448,7 +448,11 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (re
448
448
  cache_control: cacheControl(breakpoints, part.cache),
449
449
  });
450
450
  }
451
- messages.push({ role: "user", content });
451
+ const previous = messages.at(-1);
452
+ if (previous?.role === "user" && previous.content.every((block) => block.type === "tool_result"))
453
+ messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] };
454
+ else
455
+ messages.push({ role: "user", content });
452
456
  }
453
457
  return messages;
454
458
  });
@@ -1,4 +1,4 @@
1
- import { Context, Effect, Layer } from "effect";
1
+ import { Context, Effect, Layer, Stream } from "effect";
2
2
  import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
3
3
  import { AIError, TransportReason } from "../schema/index.js";
4
4
  export interface Interface {
@@ -17,6 +17,7 @@ export declare const classifyHttpFailure: (input: {
17
17
  readonly responseHeaders?: Record<string, string> | undefined;
18
18
  readonly responseBody?: string | undefined;
19
19
  }) => import("../schema/errors.js").InvalidRequestReason | import("../schema/errors.js").NoRouteReason | import("../schema/errors.js").AuthenticationReason | import("../schema/errors.js").RateLimitReason | import("../schema/errors.js").QuotaExceededReason | import("../schema/errors.js").ContentPolicyReason | import("../schema/errors.js").ProviderInternalReason | TransportReason | import("../schema/errors.js").InvalidProviderOutputReason | import("../schema/errors.js").UnknownProviderReason;
20
+ export declare const stream: (executor: Interface, request: HttpClientRequest.HttpClientRequest, middleware?: HttpMiddleware) => Stream.Stream<Uint8Array, AIError>;
20
21
  export declare const layer: Layer.Layer<Service, never, HttpClient.HttpClient>;
21
22
  export declare const fetchLayer: Layer.Layer<Service, never, never>;
22
23
  export * as RequestExecutor from "./executor.js";
@@ -1,4 +1,4 @@
1
- import { Cause, Context, Effect, Layer, Option, Schema } from "effect";
1
+ import { Cause, Context, Effect, Layer, Option, Schema, Stream } from "effect";
2
2
  import { FetchHttpClient, Headers, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse, } from "effect/unstable/http";
3
3
  import { HttpContext, HttpRateLimitDetails, HttpRequestDetails, HttpResponseDetails, AIError, TransportReason, } from "../schema/index.js";
4
4
  import { classifyProviderFailure } from "../provider-error.js";
@@ -215,48 +215,74 @@ export const classifyHttpFailure = (input) => {
215
215
  }),
216
216
  });
217
217
  };
218
- const toHttpError = (redactedNames) => (error) => {
219
- const transportError = (input) => new AIError({
218
+ const NativeTransportFailure = Schema.Struct({
219
+ message: Schema.String,
220
+ code: Schema.optionalKey(Schema.String),
221
+ cause: Schema.optionalKey(Schema.Unknown),
222
+ });
223
+ const decodeNativeTransportFailure = Schema.decodeUnknownOption(NativeTransportFailure);
224
+ const nativeTransportFailure = (error) => {
225
+ const failure = Option.getOrUndefined(decodeNativeTransportFailure(error));
226
+ if (!failure)
227
+ return undefined;
228
+ if (failure.code !== undefined)
229
+ return failure;
230
+ const cause = Option.getOrUndefined(decodeNativeTransportFailure(failure.cause));
231
+ if (cause?.code !== undefined)
232
+ return cause;
233
+ return failure;
234
+ };
235
+ const httpError = (input) => {
236
+ const request = HttpClientError.isHttpClientError(input.error) ? input.error.request : input.request;
237
+ const transportError = (failure) => new AIError({
220
238
  module: "RequestExecutor",
221
- method: "execute",
239
+ method: input.operation,
222
240
  reason: new TransportReason({
223
- message: input.message,
224
- kind: input.kind,
225
- url: input.request ? redactUrl(input.request.url) : undefined,
226
- http: input.request ? new HttpContext({ request: requestDetails(input.request, redactedNames) }) : undefined,
241
+ message: failure.message,
242
+ transport: "http",
243
+ operation: input.operation,
244
+ code: failure.code,
245
+ url: redactUrl(request.url),
246
+ http: new HttpContext({ request: requestDetails(request, input.redactedNames) }),
227
247
  }),
228
248
  });
229
- if (Cause.isTimeoutError(error)) {
230
- return transportError({ message: error.message, kind: "Timeout" });
231
- }
232
- if (!HttpClientError.isHttpClientError(error)) {
233
- return transportError({ message: error instanceof Error ? error.message : "HTTP transport failed" });
234
- }
235
- const request = "request" in error ? error.request : undefined;
236
- if (error.reason._tag === "TransportError") {
249
+ const source = HttpClientError.isHttpClientError(input.error) && "cause" in input.error.reason
250
+ ? input.error.reason.cause
251
+ : input.error;
252
+ const native = nativeTransportFailure(source);
253
+ const code = native?.code;
254
+ const raw = native?.message ?? (input.error instanceof Error ? input.error.message : undefined);
255
+ const detail = raw ? redactBody(raw, secretValues(request)) : undefined;
256
+ const message = code && detail && !detail.includes(code) ? `${code}: ${detail}` : detail;
257
+ if (Cause.isTimeoutError(input.error) || Cause.isTimeoutError(source))
258
+ return transportError({ message: message ?? "HTTP transport timed out", code: code ?? "Timeout" });
259
+ if (!HttpClientError.isHttpClientError(input.error))
260
+ return transportError({ message: message ?? "HTTP transport failed", code });
261
+ if (input.error.reason._tag === "TransportError") {
237
262
  return transportError({
238
- message: error.reason.description ?? "HTTP transport failed",
239
- kind: error.reason._tag,
240
- request,
263
+ message: message ?? input.error.reason.description ?? "HTTP transport failed",
264
+ code: code ?? input.error.reason._tag,
241
265
  });
242
266
  }
243
267
  return transportError({
244
- message: `HTTP transport failed: ${error.reason._tag}`,
245
- kind: error.reason._tag,
246
- request,
268
+ message: message ?? `HTTP transport failed: ${input.error.reason._tag}`,
269
+ code: code ?? input.error.reason._tag,
247
270
  });
248
271
  };
272
+ export const stream = (executor, request, middleware) => Stream.unwrap(Effect.gen(function* () {
273
+ const redactedNames = yield* Headers.CurrentRedactedNames;
274
+ const response = yield* executor.execute(request, middleware);
275
+ return response.stream.pipe(Stream.mapError((error) => httpError({ error, request: response.request, operation: "read", redactedNames })));
276
+ }));
249
277
  export const layer = Layer.effect(Service, Effect.gen(function* () {
250
278
  const http = yield* HttpClient.HttpClient;
251
279
  const executeOnce = (request, middleware) => Effect.gen(function* () {
252
280
  const redactedNames = yield* Headers.CurrentRedactedNames;
253
281
  if (!middleware)
254
- return yield* http
255
- .execute(request)
256
- .pipe(Effect.mapError(toHttpError(redactedNames)), Effect.flatMap(statusError(request, redactedNames)));
282
+ return yield* http.execute(request).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request", redactedNames })), Effect.flatMap(statusError(request, redactedNames)));
257
283
  const response = yield* middleware(request, (input) => http
258
284
  .execute(input)
259
- .pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))))).pipe(Effect.mapError(toHttpError(redactedNames)));
285
+ .pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))))).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request", redactedNames })));
260
286
  return yield* statusError(response.request, redactedNames)(response);
261
287
  });
262
288
  return Service.of({
@@ -1,10 +1,11 @@
1
- import { Effect, Stream } from "effect";
1
+ import { Effect } from "effect";
2
2
  import { Headers, HttpClientRequest } from "effect/unstable/http";
3
3
  import { Auth } from "../auth.js";
4
4
  import { render as renderEndpoint } from "../endpoint.js";
5
5
  import { Framing } from "../framing.js";
6
6
  import * as ProviderShared from "../../protocols/shared.js";
7
7
  import { mergeJsonRecords } from "../../schema/index.js";
8
+ import { RequestExecutor } from "../executor.js";
8
9
  const applyQuery = (url, query) => {
9
10
  if (!query)
10
11
  return url;
@@ -52,9 +53,7 @@ export const httpJson = (input) => ({
52
53
  middleware: prepareInput.middleware,
53
54
  };
54
55
  }),
55
- frames: (prepared, request, runtime) => Stream.unwrap(runtime.http
56
- .execute(prepared.request, prepared.middleware)
57
- .pipe(Effect.map((response) => prepared.framing.frame(response.stream.pipe(Stream.mapError((error) => ProviderShared.eventError(`${request.model.provider}/${request.model.route.id}`, `Failed to read ${request.model.provider}/${request.model.route.id} stream`, ProviderShared.errorText(error)))))))),
56
+ frames: (prepared, _request, runtime) => prepared.framing.frame(RequestExecutor.stream(runtime.http, prepared.request, prepared.middleware)),
58
57
  });
59
58
  export const sseJson = {
60
59
  id: "http-json/sse",
@@ -4,10 +4,16 @@ import { AIError, TransportReason } from "../../schema/index.js";
4
4
  import * as HttpTransport from "./http.js";
5
5
  export class Service extends Context.Service()("@opencode/AI/WebSocketExecutor") {
6
6
  }
7
- const transportError = (method, message, input = {}) => new AIError({
7
+ const transportError = (method, message, input) => new AIError({
8
8
  module: "WebSocketExecutor",
9
9
  method,
10
- reason: new TransportReason({ message, url: input.url, kind: input.kind }),
10
+ reason: new TransportReason({
11
+ message,
12
+ transport: "websocket",
13
+ operation: input.operation,
14
+ url: input.url,
15
+ code: input.code,
16
+ }),
11
17
  });
12
18
  const eventMessage = (event) => {
13
19
  if ("message" in event && typeof event.message === "string")
@@ -29,7 +35,8 @@ const waitOpen = (ws, input) => {
29
35
  if (ws.readyState === globalThis.WebSocket.CLOSING || ws.readyState === globalThis.WebSocket.CLOSED) {
30
36
  return Effect.fail(transportError("open", `WebSocket closed before opening (state ${ws.readyState})`, {
31
37
  url: input.url,
32
- kind: "open",
38
+ operation: "request",
39
+ code: "closed",
33
40
  }));
34
41
  }
35
42
  return Effect.callback((resume, signal) => {
@@ -50,13 +57,17 @@ const waitOpen = (ws, input) => {
50
57
  };
51
58
  const onError = (event) => {
52
59
  cleanup();
53
- resume(Effect.fail(transportError("open", `Failed to open WebSocket: ${eventMessage(event)}`, { url: input.url, kind: "open" })));
60
+ resume(Effect.fail(transportError("open", `Failed to open WebSocket: ${eventMessage(event)}`, {
61
+ url: input.url,
62
+ operation: "request",
63
+ })));
54
64
  };
55
65
  const onClose = (event) => {
56
66
  cleanup();
57
67
  resume(Effect.fail(transportError("open", `WebSocket closed before opening with code ${event.code}`, {
58
68
  url: input.url,
59
- kind: "open",
69
+ operation: "request",
70
+ code: String(event.code),
60
71
  })));
61
72
  };
62
73
  ws.addEventListener("open", onOpen, { once: true });
@@ -80,14 +91,15 @@ const webSocketUrl = (value) => Effect.try({
80
91
  },
81
92
  catch: (error) => transportError("prepare", error instanceof Error ? error.message : "Invalid WebSocket URL", {
82
93
  url: value,
83
- kind: "websocket",
94
+ operation: "request",
95
+ code: "invalid-url",
84
96
  }),
85
97
  });
86
98
  export const open = (input) => Effect.try({
87
99
  try: () => new globalThis.WebSocket(input.url, { headers: input.headers }),
88
100
  catch: (error) => transportError("open", error instanceof Error ? error.message : "Failed to construct WebSocket", {
89
101
  url: input.url,
90
- kind: "open",
102
+ operation: "request",
91
103
  }),
92
104
  }).pipe(Effect.flatMap((ws) => fromWebSocket(ws, input)));
93
105
  export const layer = Layer.succeed(Service, Service.of({ open }));
@@ -100,15 +112,25 @@ export const fromWebSocket = (ws, input) => Effect.gen(function* () {
100
112
  const binary = binaryMessage(event.data);
101
113
  if (binary)
102
114
  return Queue.offerUnsafe(messages, binary);
103
- Queue.failCauseUnsafe(messages, Cause.fail(transportError("message", "Unsupported WebSocket message payload", { url: input.url, kind: "message" })));
115
+ Queue.failCauseUnsafe(messages, Cause.fail(transportError("message", "Unsupported WebSocket message payload", {
116
+ url: input.url,
117
+ operation: "read",
118
+ })));
104
119
  };
105
120
  const onError = (event) => {
106
- Queue.failCauseUnsafe(messages, Cause.fail(transportError("message", `WebSocket error: ${eventMessage(event)}`, { url: input.url, kind: "message" })));
121
+ Queue.failCauseUnsafe(messages, Cause.fail(transportError("message", `WebSocket error: ${eventMessage(event)}`, {
122
+ url: input.url,
123
+ operation: "read",
124
+ })));
107
125
  };
108
126
  const onClose = (event) => {
109
127
  if (event.code === 1000 || event.code === 1005)
110
128
  return Queue.endUnsafe(messages);
111
- Queue.failCauseUnsafe(messages, Cause.fail(transportError("message", `WebSocket closed with code ${event.code}`, { url: input.url, kind: "close" })));
129
+ Queue.failCauseUnsafe(messages, Cause.fail(transportError("message", `WebSocket closed with code ${event.code}`, {
130
+ url: input.url,
131
+ operation: "read",
132
+ code: String(event.code),
133
+ })));
112
134
  };
113
135
  const cleanup = Effect.sync(() => {
114
136
  ws.removeEventListener("message", onMessage);
@@ -123,7 +145,7 @@ export const fromWebSocket = (ws, input) => Effect.gen(function* () {
123
145
  try: () => ws.send(message),
124
146
  catch: (error) => transportError("sendText", error instanceof Error ? error.message : "Failed to send WebSocket message", {
125
147
  url: input.url,
126
- kind: "write",
148
+ operation: "write",
127
149
  }),
128
150
  }),
129
151
  messages: Stream.fromQueue(messages),
@@ -153,7 +175,8 @@ export const json = (input) => ({
153
175
  if (!webSocket) {
154
176
  return Stream.fail(transportError("json", "WebSocket JSON transport requires WebSocketExecutor.Service", {
155
177
  url: prepared.url,
156
- kind: "websocket",
178
+ operation: "request",
179
+ code: "unavailable",
157
180
  }));
158
181
  }
159
182
  const decoder = new TextDecoder();
@@ -97,10 +97,16 @@ declare const ProviderInternalReason_base: Schema.Class<ProviderInternalReason,
97
97
  }>, {}>;
98
98
  export declare class ProviderInternalReason extends ProviderInternalReason_base {
99
99
  }
100
+ export declare const TransportType: Schema.Literals<readonly ["http", "websocket"]>;
101
+ export type TransportType = typeof TransportType.Type;
102
+ export declare const TransportOperation: Schema.Literals<readonly ["request", "read", "write"]>;
103
+ export type TransportOperation = typeof TransportOperation.Type;
100
104
  declare const TransportReason_base: Schema.Class<TransportReason, Schema.Struct<{
101
105
  readonly _tag: Schema.tag<"Transport">;
102
106
  readonly message: Schema.String;
103
- readonly kind: Schema.optional<Schema.String>;
107
+ readonly transport: Schema.Literals<readonly ["http", "websocket"]>;
108
+ readonly operation: Schema.Literals<readonly ["request", "read", "write"]>;
109
+ readonly code: Schema.optional<Schema.String>;
104
110
  readonly url: Schema.optional<Schema.String>;
105
111
  readonly http: Schema.optional<typeof HttpContext>;
106
112
  }>, {}>;
@@ -88,10 +88,14 @@ export class ProviderInternalReason extends Schema.Class("AI.Error.ProviderInter
88
88
  http: Schema.optional(HttpContext),
89
89
  }) {
90
90
  }
91
+ export const TransportType = Schema.Literals(["http", "websocket"]);
92
+ export const TransportOperation = Schema.Literals(["request", "read", "write"]);
91
93
  export class TransportReason extends Schema.Class("AI.Error.Transport")({
92
94
  _tag: Schema.tag("Transport"),
93
95
  message: Schema.String,
94
- kind: Schema.optional(Schema.String),
96
+ transport: TransportType,
97
+ operation: TransportOperation,
98
+ code: Schema.optional(Schema.String),
95
99
  url: Schema.optional(Schema.String),
96
100
  http: Schema.optional(HttpContext),
97
101
  }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "0.0.0-next-17274",
3
+ "version": "0.0.0-next-17288",
4
4
  "name": "@opencode-ai/ai",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@clack/prompts": "1.0.0-alpha.1",
32
32
  "@effect/platform-node": "4.0.0-beta.101",
33
- "@opencode-ai/http-recorder": "0.0.0-next-17274",
33
+ "@opencode-ai/http-recorder": "0.0.0-next-17288",
34
34
  "@tsconfig/bun": "1.0.9",
35
35
  "@types/bun": "1.3.13",
36
36
  "@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@smithy/eventstream-codec": "4.2.14",
41
41
  "@smithy/util-utf8": "4.2.2",
42
- "@opencode-ai/schema": "0.0.0-next-17274",
42
+ "@opencode-ai/schema": "0.0.0-next-17288",
43
43
  "aws4fetch": "1.0.20",
44
44
  "effect": "4.0.0-beta.101",
45
45
  "google-auth-library": "10.5.0"