diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..b5093d01 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,12 @@ +FROM oven/bun:debian + +# Config Bun +ENV PATH="~/.bun/bin:${PATH}" +RUN ln -s /usr/local/bin/bun /usr/local/bin/node + +# Update packages +RUN if [ "debian" == "alpine" ] ; then apk update ; else apt-get update ; fi + +# Install Git +RUN if [ "debian" == "alpine" ] ; then apk add git ; else apt-get install -y git ; fi + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..f80bcfa4 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,16 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/marcosgomesneto/bun-devcontainers/tree/main/src/basic-bun +{ + "name": "Bun", + "dockerFile": "Dockerfile", + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "oven.bun-vscode" + ] + } + } +} \ No newline at end of file diff --git a/packages/core/effect-drizzle-sqlite/src/effect-sqlite/driver.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/effect-sqlite/driver.ts.gcov.html new file mode 100644 index 00000000..b3aa72ec --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/effect-sqlite/driver.ts.gcov.html @@ -0,0 +1,153 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/effect-sqlite/driver.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/effect-sqlite - driver.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %3636
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2           42 : import * as Effect from "effect/Effect"
+       3           76 : import * as Layer from "effect/Layer"
+       4          116 : import { SqlClient } from "effect/unstable/sql/SqlClient"
+       5          132 : import { EffectCache } from "drizzle-orm/cache/core/cache-effect"
+       6          110 : import { EffectLogger } from "drizzle-orm/effect-core"
+       7           96 : import { entityKind } from "drizzle-orm/entity"
+       8              : import type { AnyRelations, EmptyRelations } from "drizzle-orm/relations"
+       9          138 : import { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
+      10          128 : import { SQLiteEffectDatabase } from "../sqlite-core/effect/db"
+      11              : import type { DrizzleConfig } from "drizzle-orm/utils"
+      12          118 : import { jitCompatCheck } from "../internal/drizzle-utils"
+      13           96 : import { type EffectSQLiteQueryEffectHKT, type EffectSQLiteRunResult, EffectSQLiteSession } from "./session"
+      14              : 
+      15          124 : export class EffectSQLiteDatabase<TRelations extends AnyRelations = EmptyRelations> extends SQLiteEffectDatabase<
+      16              :   EffectSQLiteQueryEffectHKT,
+      17              :   EffectSQLiteRunResult,
+      18              :   TRelations
+      19           22 : > {
+      20           76 :   static override readonly [entityKind]: string = "EffectSQLiteDatabase"
+      21            2 : }
+      22              : 
+      23              : export type EffectDrizzleSQLiteConfig<TRelations extends AnyRelations = EmptyRelations> = Omit<
+      24              :   DrizzleConfig<Record<string, never>, TRelations>,
+      25              :   "cache" | "logger" | "schema"
+      26              : >
+      27              : 
+      28          172 : export const DefaultServices = Layer.merge(EffectCache.Default, EffectLogger.Default)
+      29              : 
+      30              : /**
+      31              :  * Creates an EffectSQLiteDatabase instance.
+      32              :  *
+      33              :  * Requires a generic Effect `SqlClient`, `EffectLogger`, and `EffectCache` services to be provided.
+      34              :  * Drizzle only depends on the generic `SqlClient`; install and provide a compatible SQLite provider such as
+      35              :  * `@effect/sql-sqlite-node`, `@effect/sql-sqlite-bun`, or another package that exposes `SqlClient`.
+      36              :  *
+      37              :  * @example
+      38              :  * ```ts
+      39              :  * import { SqliteClient } from '@effect/sql-sqlite-node';
+      40              :  * import * as SQLiteDrizzle from 'drizzle-orm/effect-sqlite';
+      41              :  * import * as Effect from 'effect/Effect';
+      42              :  *
+      43              :  * const db = yield* SQLiteDrizzle.make({ relations }).pipe(
+      44              :  *   Effect.provide(SQLiteDrizzle.DefaultServices),
+      45              :  *   Effect.provide(SqliteClient.layer({ filename: 'sqlite.db' })),
+      46              :  * );
+      47              :  * ```
+      48              :  */
+      49          113 : export const make = Effect.fn("SQLiteDrizzle.make")(function* <TRelations extends AnyRelations = EmptyRelations>(
+      50           25 :   config: EffectDrizzleSQLiteConfig<TRelations> = {},
+      51            6 : ) {
+      52           68 :   const client = yield* SqlClient
+      53           70 :   const cache = yield* EffectCache
+      54           74 :   const logger = yield* EffectLogger
+      55              : 
+      56           82 :   const dialect = new SQLiteAsyncDialect()
+      57           86 :   const relations = config.relations ?? ({} as TRelations)
+      58          146 :   const session = new EffectSQLiteSession(client, dialect, relations, {
+      59           22 :     logger,
+      60           20 :     cache,
+      61           86 :     useJitMappers: jitCompatCheck(config.jit),
+      62           10 :   })
+      63          134 :   const db = new EffectSQLiteDatabase(dialect, session, relations) as EffectSQLiteDatabase<TRelations> & {
+      64              :     $client: SqlClient
+      65              :   }
+      66           44 :   db.$client = client
+      67           80 :   db.$cache.invalidate = cache.onMutate
+      68              : 
+      69           20 :   return db
+      70            6 : })
+      71              : 
+      72              : /**
+      73              :  * Convenience function that creates an EffectSQLiteDatabase with `DefaultServices` already provided.
+      74              :  */
+      75           63 : export const makeWithDefaults = <TRelations extends AnyRelations = EmptyRelations>(
+      76           32 :   config: EffectDrizzleSQLiteConfig<TRelations> = {},
+      77          102 : ) => make(config).pipe(Effect.provide(DefaultServices))
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/effect-sqlite/index-sort-f.html b/packages/core/effect-drizzle-sqlite/src/effect-sqlite/index-sort-f.html new file mode 100644 index 00000000..6585d418 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/effect-sqlite/index-sort-f.html @@ -0,0 +1,116 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/effect-sqlite + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/effect-sqliteCoverageTotalHit
Test:opencode-lcov.infoLines:84.5 %194164
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
driver.ts +
100.0%
+
100.0 %3636
migrator.ts +
28.6%28.6%
+
28.6 %72
session.ts +
83.4%83.4%
+
83.4 %151126
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/effect-sqlite/index-sort-l.html b/packages/core/effect-drizzle-sqlite/src/effect-sqlite/index-sort-l.html new file mode 100644 index 00000000..ebbb42f0 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/effect-sqlite/index-sort-l.html @@ -0,0 +1,116 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/effect-sqlite + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/effect-sqliteCoverageTotalHit
Test:opencode-lcov.infoLines:84.5 %194164
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
migrator.ts +
28.6%28.6%
+
28.6 %72
session.ts +
83.4%83.4%
+
83.4 %151126
driver.ts +
100.0%
+
100.0 %3636
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/effect-sqlite/index.html b/packages/core/effect-drizzle-sqlite/src/effect-sqlite/index.html new file mode 100644 index 00000000..1f7f6157 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/effect-sqlite/index.html @@ -0,0 +1,116 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/effect-sqlite + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/effect-sqliteCoverageTotalHit
Test:opencode-lcov.infoLines:84.5 %194164
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
driver.ts +
100.0%
+
100.0 %3636
migrator.ts +
28.6%28.6%
+
28.6 %72
session.ts +
83.4%83.4%
+
83.4 %151126
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/effect-sqlite/migrator.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/effect-sqlite/migrator.ts.gcov.html new file mode 100644 index 00000000..3a527169 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/effect-sqlite/migrator.ts.gcov.html @@ -0,0 +1,90 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/effect-sqlite/migrator.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/effect-sqlite - migrator.tsCoverageTotalHit
Test:opencode-lcov.infoLines:28.6 %72
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2              : import type { MigrationConfig } from "drizzle-orm/migrator"
+       3           58 : import { readMigrationFiles } from "drizzle-orm/migrator"
+       4              : import type { AnyRelations } from "drizzle-orm/relations"
+       5           71 : import { migrate as coreMigrate } from "../sqlite-core/effect/session"
+       6              : import type { EffectSQLiteDatabase } from "./driver"
+       7              : 
+       8            0 : export function migrate<TRelations extends AnyRelations>(
+       9            0 :   db: EffectSQLiteDatabase<TRelations>,
+      10            0 :   config: MigrationConfig,
+      11            0 : ) {
+      12            0 :   const migrations = readMigrationFiles(config)
+      13              :   return coreMigrate(migrations, db.session, config)
+      14              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/effect-sqlite/session.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/effect-sqlite/session.ts.gcov.html new file mode 100644 index 00000000..0be69652 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/effect-sqlite/session.ts.gcov.html @@ -0,0 +1,294 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/effect-sqlite/session.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/effect-sqlite - session.tsCoverageTotalHit
Test:opencode-lcov.infoLines:83.4 %151126
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2           46 : import * as Context from "effect/Context"
+       3           80 : import * as Effect from "effect/Effect"
+       4           72 : import * as Exit from "effect/Exit"
+       5           76 : import * as Scope from "effect/Scope"
+       6              : import type { SqlClient } from "effect/unstable/sql/SqlClient"
+       7              : import type { SqlError } from "effect/unstable/sql/SqlError"
+       8              : import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect"
+       9              : import type { WithCacheConfig } from "drizzle-orm/cache/core/types"
+      10              : import type { EffectDrizzleQueryError } from "drizzle-orm/effect-core/errors"
+      11              : import type { EffectLoggerShape } from "drizzle-orm/effect-core/logger"
+      12              : import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+      13           96 : import { entityKind } from "drizzle-orm/entity"
+      14              : import type { AnyRelations } from "drizzle-orm/relations"
+      15              : import type { RelationalQueryMapperConfig } from "drizzle-orm/relations"
+      16              : import type { Query } from "drizzle-orm/sql/sql"
+      17              : import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
+      18          240 : import { SQLiteEffectPreparedQuery, SQLiteEffectSession, SQLiteEffectTransaction } from "../sqlite-core/effect/session"
+      19              : import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types"
+      20              : import type { PreparedQueryConfig, SQLiteExecuteMethod, SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session"
+      21              : 
+      22              : export interface EffectSQLiteQueryEffectHKT extends QueryEffectHKTBase {
+      23              :   readonly error: EffectDrizzleQueryError
+      24              :   readonly context: never
+      25              : }
+      26              : 
+      27              : export type EffectSQLiteRunResult = readonly never[]
+      28              : 
+      29              : export interface EffectSQLiteSessionOptions {
+      30              :   logger: EffectLoggerShape
+      31              :   cache: EffectCacheShape
+      32              :   useJitMappers?: boolean
+      33              : }
+      34              : 
+      35          120 : export class EffectSQLiteSession<TRelations extends AnyRelations> extends SQLiteEffectSession<
+      36              :   EffectSQLiteQueryEffectHKT,
+      37              :   EffectSQLiteRunResult,
+      38              :   TRelations
+      39            6 : > {
+      40           75 :   static override readonly [entityKind]: string = "EffectSQLiteSession"
+      41              : 
+      42           13 :   constructor(
+      43           84 :     private client: SqlClient,
+      44           18 :     dialect: SQLiteAsyncDialect,
+      45          108 :     protected relations: TRelations,
+      46          108 :     private options: EffectSQLiteSessionOptions,
+      47           10 :   ) {
+      48           38 :     super(dialect)
+      49              :   }
+      50              : 
+      51           14 :   override prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
+      52           14 :     query: Query,
+      53           16 :     fields: SelectedFieldsOrdered | undefined,
+      54           30 :     executeMethod: SQLiteExecuteMethod,
+      55           40 :     customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown,
+      56           30 :     queryMetadata?: {
+      57              :       type: "select" | "update" | "delete" | "insert"
+      58              :       tables: string[]
+      59              :     },
+      60           26 :     cacheConfig?: WithCacheConfig,
+      61           10 :   ): SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT> {
+      62           73 :     return new SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT>(
+      63          113 :       (params, method) => this.execute(query, params, method),
+      64           14 :       query,
+      65           42 :       this.options.logger,
+      66           40 :       this.options.cache,
+      67           30 :       queryMetadata,
+      68           26 :       cacheConfig,
+      69           16 :       fields,
+      70           30 :       executeMethod,
+      71           56 :       this.options.useJitMappers,
+      72           40 :       customResultMapper,
+      73           22 :       undefined,
+      74           22 :       undefined,
+      75           44 :       this.isInTransaction(),
+      76            9 :     )
+      77              :   }
+      78              : 
+      79            0 :   override prepareRelationalQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
+      80            0 :     query: Query,
+      81            0 :     fields: SelectedFieldsOrdered | undefined,
+      82            0 :     executeMethod: SQLiteExecuteMethod,
+      83            0 :     customResultMapper: (rows: Record<string, unknown>[], mapColumnValue?: (value: unknown) => unknown) => unknown,
+      84            0 :     config: RelationalQueryMapperConfig,
+      85            0 :   ): SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT, true> {
+      86            0 :     return new SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT, true>(
+      87            0 :       (params, method) => this.execute(query, params, method),
+      88            0 :       query,
+      89            0 :       this.options.logger,
+      90            0 :       this.options.cache,
+      91            0 :       undefined,
+      92            0 :       undefined,
+      93            0 :       fields,
+      94            0 :       executeMethod,
+      95            0 :       this.options.useJitMappers,
+      96            0 :       customResultMapper,
+      97            0 :       true,
+      98            0 :       config,
+      99            0 :       this.isInTransaction(),
+     100            8 :     )
+     101              :   }
+     102              : 
+     103           65 :   private execute(query: Query, params: unknown[], method: SQLiteExecuteMethod | "values") {
+     104          120 :     const statement = this.client.unsafe(query.sql, params)
+     105          114 :     if (method === "values") return statement.values
+     106          198 :     if (method === "get") return statement.withoutTransform.pipe(Effect.map((rows) => rows[0]))
+     107           73 :     return statement.withoutTransform
+     108              :   }
+     109              : 
+     110           31 :   private isInTransaction() {
+     111          229 :     return Effect.serviceOption(this.client.transactionService).pipe(Effect.map((option) => option._tag === "Some"))
+     112              :   }
+     113              : 
+     114           77 :   private executeTransactionStatement(connection: Effect.Success<SqlClient["reserve"]>, query: string) {
+     115          161 :     return connection.executeUnprepared(query, [], undefined).pipe(Effect.asVoid)
+     116              :   }
+     117              : 
+     118           59 :   private withTransaction<A, E, R>(effect: Effect.Effect<A, E, R>, config: SQLiteTransactionConfig | undefined) {
+     119           93 :     return Effect.uninterruptibleMask((restore) =>
+     120           69 :       Effect.withFiber<A, E | SqlError, R>((fiber) => {
+     121           74 :         const services = fiber.context
+     122          182 :         const connectionOption = Context.getOption(services, this.client.transactionService)
+     123           38 :         const connection: Effect.Effect<
+     124              :           readonly [Scope.Closeable | undefined, Effect.Success<SqlClient["reserve"]>],
+     125              :           SqlError
+     126              :         > =
+     127           69 :           connectionOption._tag === "Some"
+     128           59 :             ? Effect.succeed([undefined, connectionOption.value[0]] as const)
+     129           36 :             : Scope.make().pipe(
+     130           51 :                 Effect.flatMap((scope) =>
+     131           94 :                   Scope.provide(this.client.reserve, scope).pipe(
+     132           96 :                     Effect.map((connection) => [scope, connection] as const),
+     133            0 :                     Effect.catch((error) =>
+     134           77 :                       Scope.close(scope, Exit.fail(error)).pipe(Effect.andThen(Effect.fail(error))),
+     135            2 :                     ),
+     136            1 :                   ),
+     137            2 :                 ),
+     138           15 :               )
+     139          140 :         const id = connectionOption._tag === "Some" ? connectionOption.value[1] + 1 : 0
+     140              : 
+     141           46 :         return connection.pipe(
+     142           97 :           Effect.flatMap(([scope, connection]) => {
+     143          106 :             const transaction = this.executeTransactionStatement(
+     144           24 :               connection,
+     145          134 :               id === 0 ? `begin ${config?.behavior ?? "deferred"}` : `savepoint effect_sql_${id}`,
+     146           13 :             ).pipe(
+     147           41 :               Effect.flatMap(() =>
+     148           44 :                 Effect.provideContext(
+     149           34 :                   restore(effect),
+     150          142 :                   Context.add(services, this.client.transactionService, [connection, id]),
+     151           14 :                 ).pipe(
+     152           26 :                   Effect.exit,
+     153           71 :                   Effect.flatMap((exit) => {
+     154           79 :                     const finalize = Exit.isSuccess(exit)
+     155           21 :                       ? id === 0
+     156          120 :                         ? this.executeTransactionStatement(connection, "commit").pipe(
+     157              :                             // SQLite keeps the transaction open after deferred constraint commit failures.
+     158            0 :                             Effect.catch((error) =>
+     159            0 :                               this.executeTransactionStatement(connection, "rollback").pipe(
+     160            0 :                                 Effect.catch(() => Effect.void),
+     161           34 :                                 Effect.andThen(Effect.fail(error)),
+     162            1 :                               ),
+     163            2 :                             ),
+     164            5 :                           )
+     165           87 :                         : this.executeTransactionStatement(connection, `release savepoint effect_sql_${id}`)
+     166           21 :                       : id === 0
+     167          115 :                         ? this.executeTransactionStatement(connection, "rollback")
+     168           92 :                         : this.executeTransactionStatement(connection, `rollback to savepoint effect_sql_${id}`).pipe(
+     169           15 :                             Effect.andThen(
+     170           82 :                               this.executeTransactionStatement(connection, `release savepoint effect_sql_${id}`),
+     171            1 :                             ),
+     172           22 :                           )
+     173              : 
+     174          104 :                     return finalize.pipe(Effect.flatMap(() => exit))
+     175            2 :                   }),
+     176            1 :                 ),
+     177            2 :               ),
+     178           20 :             )
+     179              : 
+     180           57 :             return scope === undefined
+     181           16 :               ? transaction
+     182          140 :               : transaction.pipe(Effect.onExit((exit) => Scope.close(scope, exit)))
+     183            2 :           }),
+     184            8 :         )
+     185            1 :       }),
+     186            9 :     )
+     187              :   }
+     188              : 
+     189           13 :   override transaction<A, E, R>(
+     190           26 :     transaction: (tx: EffectSQLiteTransaction<TRelations>) => Effect.Effect<A, E, R>,
+     191           16 :     config?: SQLiteTransactionConfig,
+     192           10 :   ): Effect.Effect<A, E | SqlError, R> {
+     193           80 :     const { dialect, relations } = this
+     194              : 
+     195           56 :     return this.withTransaction(
+     196           82 :       Effect.gen({ self: this }, function* () {
+     197          142 :         const tx = new EffectSQLiteTransaction<TRelations>(dialect, this, relations)
+     198              : 
+     199           65 :         return yield* transaction(tx)
+     200            6 :       }),
+     201           12 :       config,
+     202            8 :     )
+     203              :   }
+     204            2 : }
+     205              : 
+     206          136 : export class EffectSQLiteTransaction<TRelations extends AnyRelations> extends SQLiteEffectTransaction<
+     207              :   EffectSQLiteQueryEffectHKT,
+     208              :   EffectSQLiteRunResult,
+     209              :   TRelations
+     210           22 : > {
+     211           84 :   static override readonly [entityKind]: string = "EffectSQLiteTransaction"
+     212              : 
+     213           27 :   override transaction: <A, E, R>(
+     214              :     transaction: (
+     215              :       tx: SQLiteEffectTransaction<EffectSQLiteQueryEffectHKT, EffectSQLiteRunResult, TRelations>,
+     216              :     ) => Effect.Effect<A, E, R>,
+     217           40 :   ) => Effect.Effect<A, SqlError | E, R> = (tx) => this.session.transaction(tx)
+     218            1 : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/index.html b/packages/core/effect-drizzle-sqlite/src/index.html new file mode 100644 index 00000000..70c75683 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/index.html @@ -0,0 +1,98 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/srcCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %55
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
index.ts +
100.0%
+
100.0 %55
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/index.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/index.ts.gcov.html new file mode 100644 index 00000000..d9a648db --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/index.ts.gcov.html @@ -0,0 +1,82 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/index.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src - index.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %55
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           55 : export { EffectLogger } from "drizzle-orm/effect-core"
+       2           39 : export * from "./effect-sqlite/driver"
+       3           40 : export * from "./effect-sqlite/session"
+       4           51 : export { migrate } from "./effect-sqlite/migrator"
+       5              : 
+       6           40 : export * as EffectDrizzleSqlite from "."
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/internal/drizzle-utils.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/internal/drizzle-utils.ts.gcov.html new file mode 100644 index 00000000..72623c1a --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/internal/drizzle-utils.ts.gcov.html @@ -0,0 +1,203 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/internal/drizzle-utils.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/internal - drizzle-utils.tsCoverageTotalHit
Test:opencode-lcov.infoLines:67.4 %8960
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2           60 : import { Column, getColumnTable } from "drizzle-orm/column"
+       3           40 : import { is } from "drizzle-orm/entity"
+       4              : import type { JoinNullability } from "drizzle-orm/query-builders/select.types"
+       5           49 : import { Param, SQL } from "drizzle-orm/sql/sql"
+       6              : import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types"
+       7              : import type { SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update"
+       8              : import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+       9           67 : import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base"
+      10           48 : import { Subquery } from "drizzle-orm/subquery"
+      11           56 : import { Table, getTableName } from "drizzle-orm/table"
+      12              : import type { UpdateSet } from "drizzle-orm/utils"
+      13           57 : import { ViewBaseConfig } from "drizzle-orm/view-common"
+      14              : 
+      15           19 : const TableSymbol = (
+      16            6 :   Table as unknown as {
+      17              :     Symbol: { Columns: symbol; IsAlias: symbol; Name: symbol; BaseName: symbol }
+      18              :   }
+      19            8 : ).Symbol
+      20              : 
+      21           16 : export function getTableColumnsRuntime(table: SQLiteTable) {
+      22           34 :   return (table as unknown as Record<symbol, Record<string, Column>>)[TableSymbol.Columns]
+      23              : }
+      24              : 
+      25            0 : export function getViewSelectedFieldsRuntime(view: SQLiteViewBase) {
+      26            1 :   return (view as unknown as Record<symbol, { selectedFields: Record<string, unknown>; name: string }>)[ViewBaseConfig]
+      27              : }
+      28              : 
+      29           20 : export function jitCompatCheck(isEnabled: boolean | undefined) {
+      30           30 :   if (!isEnabled) return false
+      31            0 :   try {
+      32            0 :     return new Function("input", '"use strict"; return input;')(true) === true
+      33            0 :   } catch {
+      34            1 :     return false
+      35              :   }
+      36              : }
+      37              : 
+      38            6 : export function orderSelectedFields<TColumn extends Column>(
+      39            8 :   fields: Record<string, unknown>,
+      40           12 :   pathPrefix?: string[],
+      41            3 : ): SelectedFieldsOrdered {
+      42           61 :   return Object.entries(fields).flatMap(([name, field]) => {
+      43           37 :     const path = pathPrefix ? [...pathPrefix, name] : [name]
+      44           96 :     if (is(field, Column) || is(field, SQL) || is(field, SQL.Aliased) || is(field, Subquery)) {
+      45           24 :       return [{ path, field }] as SelectedFieldsOrdered
+      46            0 :     }
+      47            0 :     if (is(field, Table)) return orderSelectedFields(getTableColumnsRuntime(field as SQLiteTable), path)
+      48            0 :     return orderSelectedFields(field as Record<string, unknown>, path)
+      49            2 :   }) as SelectedFieldsOrdered
+      50              : }
+      51              : 
+      52           24 : export function mapUpdateSet<TTable extends SQLiteTable>(table: TTable, values: SQLiteUpdateSetSource<TTable>) {
+      53           82 :   const entries = Object.entries(values).filter(([, value]) => value !== undefined)
+      54           30 :   if (entries.length === 0) throw new Error("No values to set")
+      55              : 
+      56           26 :   return Object.fromEntries(
+      57           34 :     entries.map(([key, value]) => [
+      58            8 :       key,
+      59           92 :       is(value, SQL) || is(value, Column) ? value : new Param(value, getTableColumnsRuntime(table)[key]),
+      60            1 :     ]),
+      61            2 :   ) as UpdateSet
+      62              : }
+      63              : 
+      64            6 : export function mapResultRow(
+      65            9 :   columns: SelectedFieldsOrdered,
+      66            5 :   row: unknown[],
+      67           21 :   joinsNotNullableMap: Record<string, boolean> | undefined,
+      68            3 : ) {
+      69           24 :   const nullifyMap: Record<string, string | false> = {}
+      70           20 :   const result: Record<string, unknown> = {}
+      71              : 
+      72           45 :   columns.forEach((column, columnIndex) => {
+      73           16 :     const decoder = (
+      74           26 :       is(column.field, Column)
+      75           12 :         ? column.field
+      76            0 :         : is(column.field, SQL)
+      77            0 :           ? (column.field as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder
+      78            0 :           : is(column.field, Subquery)
+      79            0 :             ? (column.field._.sql as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder
+      80            4 :             : (column.field.sql as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder
+      81              :     ) as {
+      82              :       mapFromDriverValue(value: unknown): unknown
+      83              :     }
+      84           38 :     const rawValue = row[columnIndex]
+      85           79 :     const value = rawValue === null ? null : decoder.mapFromDriverValue(rawValue)
+      86           38 :     const objectName = column.path[0]
+      87           22 :     let node = result
+      88              : 
+      89           57 :     column.path.forEach((pathChunk, pathChunkIndex) => {
+      90           55 :       if (pathChunkIndex === column.path.length - 1) {
+      91           32 :         node[pathChunk] = value
+      92            6 :         return
+      93            0 :       }
+      94            0 :       node[pathChunk] = (node[pathChunk] ?? {}) as Record<string, unknown>
+      95            0 :       node = node[pathChunk] as Record<string, unknown>
+      96            6 :     })
+      97              : 
+      98           94 :     if (joinsNotNullableMap && is(column.field, Column) && column.path.length === 2 && objectName) {
+      99            0 :       const tableName = getTableName(getColumnTable(column.field))
+     100            0 :       nullifyMap[objectName] =
+     101            0 :         !(objectName in nullifyMap) && value === null
+     102            0 :           ? tableName
+     103            0 :           : typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== tableName
+     104            0 :             ? false
+     105            0 :             : nullifyMap[objectName]
+     106            2 :     }
+     107            4 :   })
+     108              : 
+     109            0 :   Object.entries(nullifyMap).forEach(([objectName, tableName]) => {
+     110              :     if (typeof tableName === "string" && !joinsNotNullableMap?.[tableName]) result[objectName] = null
+     111            4 :   })
+     112              : 
+     113           14 :   return result
+     114              : }
+     115              : 
+     116            0 : export function getTableLikeName(table: SQLiteTable | Subquery | SQLiteViewBase | SQL) {
+     117            0 :   if (is(table, Subquery)) return table._.alias
+     118            0 :   if (is(table, SQLiteViewBase)) return getViewSelectedFieldsRuntime(table).name
+     119            0 :   if (is(table, SQL)) return undefined
+     120            0 :   return (table as unknown as Record<symbol, string | boolean>)[
+     121            0 :     (table as unknown as Record<symbol, string | boolean>)[TableSymbol.IsAlias]
+     122            0 :       ? TableSymbol.Name
+     123              :       : TableSymbol.BaseName
+     124              :   ] as string
+     125              : }
+     126              : 
+     127              : export type { JoinNullability }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/internal/index.html b/packages/core/effect-drizzle-sqlite/src/internal/index.html new file mode 100644 index 00000000..0ded186d --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/internal/index.html @@ -0,0 +1,98 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/internal + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/internalCoverageTotalHit
Test:opencode-lcov.infoLines:67.4 %8960
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
drizzle-utils.ts +
67.4%67.4%
+
67.4 %8960
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/count.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/count.ts.gcov.html new file mode 100644 index 00000000..dd1cd6ff --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/count.ts.gcov.html @@ -0,0 +1,134 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/sqlite-core/effect/count.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/sqlite-core/effect - count.tsCoverageTotalHit
Test:opencode-lcov.infoLines:31.4 %3511
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2              : import type * as Effect from "effect/Effect"
+       3              : import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+       4              : import { entityKind } from "drizzle-orm/entity"
+       5            1 : import { SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql"
+       6              : import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+       7              : import type { SQLiteView } from "drizzle-orm/sqlite-core/view"
+       8              : import type { SQLiteEffectSession } from "./session"
+       9              : 
+      10            0 : function buildSQLiteEmbeddedCount(source: SQLiteTable | SQLiteView | SQL | SQLWrapper, filters?: SQL<unknown>) {
+      11           90 :   return sql<number>`(select count(*) from ${source}${sql.raw(" where ").if(filters)}${filters})`
+      12              : }
+      13              : 
+      14            0 : function buildSQLiteCount(source: SQLiteTable | SQLiteView | SQL | SQLWrapper, filters?: SQL<unknown>) {
+      15           89 :   return sql<number>`select count(*) from ${source}${sql.raw(" where ").if(filters)}${filters}`
+      16              : }
+      17              : 
+      18              : export interface SQLiteEffectCountBuilder<TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
+      19              :   extends SQL<number>,
+      20              :     SQLWrapper<number>,
+      21              :     Effect.Effect<number, TEffectHKT["error"], TEffectHKT["context"]> {}
+      22              : 
+      23          120 : export class SQLiteEffectCountBuilder<TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase> extends SQL<number> {
+      24           86 :   static override readonly [entityKind]: string = "SQLiteEffectCountBuilder"
+      25              : 
+      26           12 :   private sql: SQL<number>
+      27           19 :   private session: SQLiteEffectSession<TEffectHKT, any, any>
+      28              : 
+      29            0 :   constructor(params: {
+      30            0 :     source: SQLiteTable | SQLiteView | SQL | SQLWrapper
+      31            0 :     filters?: SQL<unknown>
+      32            0 :     session: SQLiteEffectSession<TEffectHKT, any, any>
+      33            0 :   }) {
+      34            0 :     super(buildSQLiteEmbeddedCount(params.source, params.filters).queryChunks)
+      35            0 : 
+      36            0 :     this.session = params.session
+      37           65 :     this.sql = buildSQLiteCount(params.source, params.filters)
+      38              :   }
+      39              : 
+      40            0 :   execute(placeholderValues?: Record<string, unknown>) {
+      41            0 :     return this.session
+      42            0 :       .prepareQuery<{
+      43            0 :         type: "async"
+      44            0 :         execute: number
+      45            0 :         run: unknown
+      46            0 :         all: unknown
+      47            0 :         get: unknown
+      48            0 :         values: unknown
+      49            0 :       }>(this.session.dialect.sqlToQuery(this.sql), undefined, "all", (rows) => {
+      50            0 :         const v = rows[0]?.[0]
+      51            0 :         if (typeof v === "number") return v
+      52            0 :         return v ? Number(v) : 0
+      53            0 :       })
+      54           32 :       .execute(placeholderValues)
+      55              :   }
+      56            2 : }
+      57              : 
+      58           89 : applyEffectWrapper(SQLiteEffectCountBuilder)
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/db.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/db.ts.gcov.html new file mode 100644 index 00000000..e92d111b --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/db.ts.gcov.html @@ -0,0 +1,372 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/sqlite-core/effect/db.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/sqlite-core/effect - db.tsCoverageTotalHit
Test:opencode-lcov.infoLines:33.2 %20267
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2              : import { Effect } from "effect"
+       3              : import type { SqlError } from "effect/unstable/sql/SqlError"
+       4              : import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect"
+       5              : import type { MutationOption } from "drizzle-orm/cache/core/cache"
+       6              : import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+       7              : import { entityKind } from "drizzle-orm/entity"
+       8              : import type { TypedQueryBuilder } from "drizzle-orm/query-builders/query-builder"
+       9              : import type { AnyRelations, EmptyRelations } from "drizzle-orm/relations"
+      10              : import { SelectionProxyHandler } from "drizzle-orm/selection-proxy"
+      11              : import { type ColumnsSelection, type SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql"
+      12              : import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
+      13              : import { QueryBuilder } from "drizzle-orm/sqlite-core/query-builders/query-builder"
+      14              : import type { SelectedFields } from "drizzle-orm/sqlite-core/query-builders/select.types"
+      15              : import type { SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session"
+      16              : import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+      17              : import type { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base"
+      18              : import { WithSubquery } from "drizzle-orm/subquery"
+      19              : import type { WithBuilder } from "drizzle-orm/sqlite-core/subquery"
+      20              : import { SQLiteEffectCountBuilder } from "./count"
+      21              : import { SQLiteEffectDeleteBase } from "./delete"
+      22              : import { SQLiteEffectInsertBuilder } from "./insert"
+      23              : import { SQLiteEffectRelationalQueryBuilder } from "./query"
+      24              : import { SQLiteEffectRaw } from "./raw"
+      25              : import { SQLiteEffectSelectBuilder } from "./select"
+      26              : import type { SQLiteEffectSelectBase } from "./select"
+      27              : import type { SQLiteEffectSession, SQLiteEffectTransaction } from "./session"
+      28              : import { SQLiteEffectUpdateBuilder } from "./update"
+      29              : 
+      30              : export class SQLiteEffectDatabase<
+      31              :   TEffectHKT extends QueryEffectHKTBase,
+      32              :   TRunResult,
+      33              :   TRelations extends AnyRelations = EmptyRelations,
+      34              : > {
+      35              :   static readonly [entityKind]: string = "SQLiteEffectDatabase"
+      36              : 
+      37              :   declare readonly _: {
+      38              :     readonly relations: TRelations
+      39              :     readonly session: SQLiteEffectSession<TEffectHKT, TRunResult, TRelations>
+      40              :   }
+      41              : 
+      42            1 :   query: {
+      43              :     [K in keyof TRelations]: SQLiteEffectRelationalQueryBuilder<TRelations, TRelations[K], TEffectHKT>
+      44              :   }
+      45              : 
+      46           13 :   constructor(
+      47              :     /** @internal */
+      48           72 :     readonly dialect: SQLiteAsyncDialect,
+      49              :     /** @internal */
+      50           72 :     readonly session: SQLiteEffectSession<TEffectHKT, TRunResult, TRelations>,
+      51           22 :     relations: TRelations,
+      52           90 :     readonly rowModeRQB?: boolean,
+      53           96 :     readonly forbidJsonb?: boolean,
+      54           10 :   ) {
+      55           32 :     this._ = {
+      56           32 :       relations,
+      57           22 :       session,
+      58           12 :     }
+      59              : 
+      60           40 :     this.query = {} as (typeof this)["query"]
+      61          132 :     for (const [tableName, relation] of Object.entries(relations)) {
+      62           24 :       ;(this.query as SQLiteEffectDatabase<TEffectHKT, TRunResult, AnyRelations>["query"])[tableName] =
+      63           39 :         new SQLiteEffectRelationalQueryBuilder(
+      64           11 :           relations,
+      65           32 :           relations[relation.name]!.table as SQLiteTable,
+      66           10 :           relation,
+      67            9 :           dialect,
+      68            9 :           session,
+      69           12 :           rowModeRQB,
+      70           11 :           forbidJsonb,
+      71            6 :         )
+      72            9 :     }
+      73              : 
+      74           42 :     this.$cache = {
+      75           55 :       invalidate: (_params: MutationOption) => Effect.void,
+      76           13 :     }
+      77              :   }
+      78              : 
+      79            0 :   $with: WithBuilder = (alias: string, selection?: ColumnsSelection) => {
+      80            0 :     const self = this
+      81            0 :     const as = (
+      82            0 :       qb:
+      83            0 :         | TypedQueryBuilder<ColumnsSelection | undefined>
+      84            0 :         | SQL
+      85            0 :         | ((qb: QueryBuilder) => TypedQueryBuilder<ColumnsSelection | undefined> | SQL),
+      86            0 :     ) => {
+      87            0 :       if (typeof qb === "function") {
+      88            0 :         qb = qb(new QueryBuilder(self.dialect))
+      89            0 :       }
+      90            0 : 
+      91            0 :       return new Proxy(
+      92            0 :         new WithSubquery(
+      93            0 :           qb.getSQL(),
+      94            0 :           selection ??
+      95            0 :             (("getSelectedFields" in qb
+      96            0 :               ? ((qb as { getSelectedFields(): SelectedFields | undefined }).getSelectedFields() ?? {})
+      97            0 :               : {}) as SelectedFields),
+      98            0 :           alias,
+      99            0 :           true,
+     100            0 :         ),
+     101            0 :         new SelectionProxyHandler({ alias, sqlAliasedBehavior: "alias", sqlBehavior: "error" }),
+     102            0 :       )
+     103            0 :     }
+     104           23 :     return { as }
+     105              :   }
+     106              : 
+     107           17 :   $cache: { invalidate: EffectCacheShape["onMutate"] }
+     108              : 
+     109            0 :   $count(source: SQLiteTable | SQLiteViewBase | SQL | SQLWrapper, filters?: SQL<unknown>) {
+     110           86 :     return new SQLiteEffectCountBuilder({ source, filters, session: this.session })
+     111              :   }
+     112              : 
+     113            0 :   with(...queries: WithSubquery[]) {
+     114            0 :     const self = this
+     115            0 : 
+     116            0 :     function select(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT>
+     117            0 :     function select<TSelection extends SelectedFields>(
+     118            0 :       fields: TSelection,
+     119            0 :     ): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT>
+     120            0 :     function select(
+     121            0 :       fields?: SelectedFields,
+     122            0 :     ): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> {
+     123            0 :       return new SQLiteEffectSelectBuilder({
+     124            0 :         fields: fields ?? undefined,
+     125            0 :         session: self.session,
+     126            0 :         dialect: self.dialect,
+     127            0 :         withList: queries,
+     128            0 :       })
+     129            0 :     }
+     130            0 : 
+     131            0 :     function selectDistinct(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT>
+     132            0 :     function selectDistinct<TSelection extends SelectedFields>(
+     133            0 :       fields: TSelection,
+     134            0 :     ): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT>
+     135            0 :     function selectDistinct(
+     136            0 :       fields?: SelectedFields,
+     137            0 :     ): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> {
+     138            0 :       return new SQLiteEffectSelectBuilder({
+     139            0 :         fields: fields ?? undefined,
+     140            0 :         session: self.session,
+     141            0 :         dialect: self.dialect,
+     142            0 :         withList: queries,
+     143            0 :         distinct: true,
+     144            0 :       })
+     145            0 :     }
+     146            0 : 
+     147            0 :     function update<TTable extends SQLiteTable>(
+     148            0 :       table: TTable,
+     149            0 :     ): SQLiteEffectUpdateBuilder<TTable, TRunResult, TEffectHKT> {
+     150            0 :       return new SQLiteEffectUpdateBuilder(table, self.session, self.dialect, queries)
+     151            0 :     }
+     152            0 : 
+     153            0 :     function insert<TTable extends SQLiteTable>(
+     154            0 :       into: TTable,
+     155            0 :     ): SQLiteEffectInsertBuilder<TTable, TRunResult, TEffectHKT> {
+     156            0 :       return new SQLiteEffectInsertBuilder(into, self.session, self.dialect, queries)
+     157            0 :     }
+     158            0 : 
+     159            0 :     function delete_<TTable extends SQLiteTable>(
+     160            0 :       from: TTable,
+     161            0 :     ): SQLiteEffectDeleteBase<TTable, TRunResult, undefined, false, never, TEffectHKT> {
+     162            0 :       return new SQLiteEffectDeleteBase(from, self.session, self.dialect, queries)
+     163            0 :     }
+     164            0 : 
+     165           73 :     return { select, selectDistinct, update, insert, delete: delete_ }
+     166              :   }
+     167              : 
+     168              :   select(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT>
+     169              :   select<TSelection extends SelectedFields>(
+     170              :     fields: TSelection,
+     171              :   ): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT>
+     172           34 :   select(fields?: SelectedFields): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> {
+     173          237 :     return new SQLiteEffectSelectBuilder({ fields: fields ?? undefined, session: this.session, dialect: this.dialect })
+     174              :   }
+     175              : 
+     176              :   selectDistinct(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT>
+     177              :   selectDistinct<TSelection extends SelectedFields>(
+     178              :     fields: TSelection,
+     179              :   ): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT>
+     180            0 :   selectDistinct(
+     181            0 :     fields?: SelectedFields,
+     182            0 :   ): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> {
+     183            0 :     return new SQLiteEffectSelectBuilder({
+     184            0 :       fields: fields ?? undefined,
+     185            0 :       session: this.session,
+     186            0 :       dialect: this.dialect,
+     187            0 :       distinct: true,
+     188            9 :     })
+     189              :   }
+     190              : 
+     191           32 :   update<TTable extends SQLiteTable>(table: TTable): SQLiteEffectUpdateBuilder<TTable, TRunResult, TEffectHKT> {
+     192          149 :     return new SQLiteEffectUpdateBuilder(table, this.session, this.dialect)
+     193              :   }
+     194              : 
+     195           30 :   insert<TTable extends SQLiteTable>(into: TTable): SQLiteEffectInsertBuilder<TTable, TRunResult, TEffectHKT> {
+     196          147 :     return new SQLiteEffectInsertBuilder(into, this.session, this.dialect)
+     197              :   }
+     198              : 
+     199            8 :   delete<TTable extends SQLiteTable>(
+     200           12 :     from: TTable,
+     201           10 :   ): SQLiteEffectDeleteBase<TTable, TRunResult, undefined, false, never, TEffectHKT> {
+     202          141 :     return new SQLiteEffectDeleteBase(from, this.session, this.dialect)
+     203              :   }
+     204              : 
+     205            5 :   private raw<TResult>(
+     206           14 :     query: SQLWrapper | string,
+     207           16 :     action: "all" | "get" | "run" | "values",
+     208           18 :     execute: (query: SQL) => Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>,
+     209           10 :   ): SQLiteEffectRaw<TResult, TEffectHKT> {
+     210          155 :     const sequel = typeof query === "string" ? sql.raw(query) : query.getSQL()
+     211           53 :     return new SQLiteEffectRaw(
+     212           44 :       () => execute(sequel),
+     213           16 :       () => sequel,
+     214           16 :       action,
+     215           27 :       this.dialect,
+     216           18 :       (result) => result,
+     217            9 :     )
+     218              :   }
+     219              : 
+     220           29 :   run(query: SQLWrapper | string): SQLiteEffectRaw<TRunResult, TEffectHKT> {
+     221          139 :     return this.raw(query, "run", (sequel) => this.session.run(sequel))
+     222              :   }
+     223              : 
+     224           29 :   all<T = unknown>(query: SQLWrapper | string): SQLiteEffectRaw<T[], TEffectHKT> {
+     225          139 :     return this.raw(query, "all", (sequel) => this.session.all(sequel))
+     226              :   }
+     227              : 
+     228           29 :   get<T = unknown>(query: SQLWrapper | string): SQLiteEffectRaw<T | undefined, TEffectHKT> {
+     229          139 :     return this.raw(query, "get", (sequel) => this.session.get(sequel))
+     230              :   }
+     231              : 
+     232            0 :   values<T extends unknown[] = unknown[]>(query: SQLWrapper | string): SQLiteEffectRaw<T[], TEffectHKT> {
+     233           81 :     return this.raw(query, "values", (sequel) => this.session.values(sequel))
+     234              :   }
+     235              : 
+     236           27 :   transaction: <A, E, R>(
+     237              :     transaction: (tx: SQLiteEffectTransaction<TEffectHKT, TRunResult, TRelations>) => Effect.Effect<A, E, R>,
+     238              :     config?: SQLiteTransactionConfig,
+     239          107 :   ) => Effect.Effect<A, E | SqlError, R> = (tx, config) => this.session.transaction(tx, config)
+     240            2 : }
+     241              : 
+     242              : export type SQLiteEffectWithReplicas<Q> = Q & { $primary: Q; $replicas: Q[] }
+     243              : 
+     244            0 : export const withReplicas = <
+     245            0 :   TEffectHKT extends QueryEffectHKTBase,
+     246            0 :   TRunResult,
+     247            0 :   TRelations extends AnyRelations,
+     248            0 :   Q extends SQLiteEffectDatabase<TEffectHKT, TRunResult, TRelations>,
+     249            0 : >(
+     250            0 :   primary: Q,
+     251            0 :   replicas: [Q, ...Q[]],
+     252            0 :   getReplica: (replicas: Q[]) => Q = () => replicas[Math.floor(Math.random() * replicas.length)]!,
+     253            0 : ): SQLiteEffectWithReplicas<Q> => {
+     254            0 :   const select: Q["select"] = (...args: []) => getReplica(replicas).select(...args)
+     255            0 :   const selectDistinct: Q["selectDistinct"] = (...args: []) => getReplica(replicas).selectDistinct(...args)
+     256            0 :   const $count: Q["$count"] = (...args: [any]) => getReplica(replicas).$count(...args)
+     257            0 :   const _with: Q["with"] = (...args: []) => getReplica(replicas).with(...args)
+     258            0 :   const $with = ((...args: [string] | [string, ColumnsSelection]) =>
+     259            0 :     args.length === 1
+     260            0 :       ? getReplica(replicas).$with(args[0])
+     261            0 :       : getReplica(replicas).$with(args[0], args[1])) as Q["$with"]
+     262            0 : 
+     263            0 :   const update: Q["update"] = (...args: [any]) => primary.update(...args)
+     264            0 :   const insert: Q["insert"] = (...args: [any]) => primary.insert(...args)
+     265            0 :   const $delete: Q["delete"] = (...args: [any]) => primary.delete(...args)
+     266            0 :   const run: Q["run"] = (...args: [any]) => primary.run(...args)
+     267            0 :   const all: Q["all"] = (...args: [any]) => primary.all(...args)
+     268            0 :   const get: Q["get"] = (...args: [any]) => primary.get(...args)
+     269            0 :   const values: Q["values"] = (...args: [any]) => primary.values(...args)
+     270            0 :   const transaction: Q["transaction"] = (...args: [any]) => primary.transaction(...args)
+     271            0 : 
+     272            0 :   return {
+     273            0 :     ...primary,
+     274            0 :     update,
+     275            0 :     insert,
+     276            0 :     delete: $delete,
+     277            0 :     run,
+     278            0 :     all,
+     279            0 :     get,
+     280            0 :     values,
+     281            0 :     transaction,
+     282            0 :     $primary: primary,
+     283            0 :     $replicas: replicas,
+     284            0 :     select,
+     285            0 :     selectDistinct,
+     286            0 :     $count,
+     287            0 :     $with,
+     288            0 :     with: _with,
+     289            0 :     get query() {
+     290            0 :       return getReplica(replicas).query
+     291            0 :     },
+     292            6 :   }
+     293              : }
+     294              : 
+     295              : export type AnySQLiteEffectDatabase = SQLiteEffectDatabase<any, any, any>
+     296              : export type AnySQLiteEffectSelectBase = SQLiteEffectSelectBase<any, any, any, any, any, any, any, any, any, any>
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/delete.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/delete.ts.gcov.html new file mode 100644 index 00000000..79072bf6 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/delete.ts.gcov.html @@ -0,0 +1,337 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/sqlite-core/effect/delete.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/sqlite-core/effect - delete.tsCoverageTotalHit
Test:opencode-lcov.infoLines:62.3 %6943
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2              : import type * as Effect from "effect/Effect"
+       3              : import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+       4              : import { entityKind } from "drizzle-orm/entity"
+       5              : import type { SelectResultFields } from "drizzle-orm/query-builders/select.types"
+       6              : import type { RunnableQuery } from "drizzle-orm/runnable-query"
+       7              : import { SelectionProxyHandler } from "drizzle-orm/selection-proxy"
+       8              : import type { Placeholder, Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql"
+       9              : import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
+      10              : import type { SQLiteDeleteConfig } from "drizzle-orm/sqlite-core/query-builders/delete"
+      11              : import type { SelectedFieldsFlat } from "drizzle-orm/sqlite-core/query-builders/select.types"
+      12              : import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session"
+      13              : import { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+      14              : import { extractUsedTable } from "drizzle-orm/sqlite-core/utils"
+      15              : import type { Subquery } from "drizzle-orm/subquery"
+      16              : import { type DrizzleTypeError, type ValueOrArray } from "drizzle-orm/utils"
+      17              : import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common"
+      18              : import { getTableColumnsRuntime, orderSelectedFields } from "../../internal/drizzle-utils"
+      19              : import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
+      20              : 
+      21              : export type SQLiteEffectDeleteWithout<
+      22              :   T extends AnySQLiteEffectDelete,
+      23              :   TDynamic extends boolean,
+      24              :   K extends keyof T & string,
+      25              : > = TDynamic extends true
+      26              :   ? T
+      27              :   : Omit<
+      28              :       SQLiteEffectDeleteBase<
+      29              :         T["_"]["table"],
+      30              :         T["_"]["runResult"],
+      31              :         T["_"]["returning"],
+      32              :         TDynamic,
+      33              :         T["_"]["excludedMethods"] | K,
+      34              :         T["_"]["effectHKT"]
+      35              :       >,
+      36              :       T["_"]["excludedMethods"] | K
+      37              :     >
+      38              : 
+      39              : export type SQLiteEffectDeleteReturningAll<
+      40              :   T extends AnySQLiteEffectDelete,
+      41              :   TDynamic extends boolean,
+      42              : > = SQLiteEffectDeleteWithout<
+      43              :   SQLiteEffectDeleteBase<
+      44              :     T["_"]["table"],
+      45              :     T["_"]["runResult"],
+      46              :     T["_"]["table"]["$inferSelect"],
+      47              :     T["_"]["dynamic"],
+      48              :     T["_"]["excludedMethods"],
+      49              :     T["_"]["effectHKT"]
+      50              :   >,
+      51              :   TDynamic,
+      52              :   "returning"
+      53              : >
+      54              : 
+      55              : export type SQLiteEffectDeleteReturning<
+      56              :   T extends AnySQLiteEffectDelete,
+      57              :   TDynamic extends boolean,
+      58              :   TSelectedFields extends SelectedFieldsFlat,
+      59              : > = SQLiteEffectDeleteWithout<
+      60              :   SQLiteEffectDeleteBase<
+      61              :     T["_"]["table"],
+      62              :     T["_"]["runResult"],
+      63              :     SelectResultFields<TSelectedFields>,
+      64              :     T["_"]["dynamic"],
+      65              :     T["_"]["excludedMethods"],
+      66              :     T["_"]["effectHKT"]
+      67              :   >,
+      68              :   TDynamic,
+      69              :   "returning"
+      70              : >
+      71              : 
+      72              : export type SQLiteEffectDeleteExecute<T extends AnySQLiteEffectDelete> = T["_"]["returning"] extends undefined
+      73              :   ? T["_"]["runResult"]
+      74              :   : T["_"]["returning"][]
+      75              : 
+      76              : export type SQLiteEffectDeletePrepare<
+      77              :   T extends AnySQLiteEffectDelete,
+      78              :   TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"],
+      79              : > = SQLiteEffectPreparedQuery<
+      80              :   PreparedQueryConfig & {
+      81              :     run: T["_"]["runResult"]
+      82              :     all: T["_"]["returning"] extends undefined
+      83              :       ? DrizzleTypeError<".all() cannot be used without .returning()">
+      84              :       : T["_"]["returning"][]
+      85              :     get: T["_"]["returning"] extends undefined
+      86              :       ? DrizzleTypeError<".get() cannot be used without .returning()">
+      87              :       : T["_"]["returning"] | undefined
+      88              :     values: T["_"]["returning"] extends undefined
+      89              :       ? DrizzleTypeError<".values() cannot be used without .returning()">
+      90              :       : any[][]
+      91              :     execute: SQLiteEffectDeleteExecute<T>
+      92              :   },
+      93              :   TEffectHKT
+      94              : >
+      95              : 
+      96              : export type SQLiteEffectDeleteDynamic<T extends AnySQLiteEffectDelete> = SQLiteEffectDelete<
+      97              :   T["_"]["table"],
+      98              :   T["_"]["runResult"],
+      99              :   T["_"]["returning"],
+     100              :   T["_"]["effectHKT"]
+     101              : >
+     102              : 
+     103              : export type SQLiteEffectDelete<
+     104              :   TTable extends SQLiteTable = SQLiteTable,
+     105              :   TRunResult = unknown,
+     106              :   TReturning extends Record<string, unknown> | undefined = undefined,
+     107              :   TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+     108              : > = SQLiteEffectDeleteBase<TTable, TRunResult, TReturning, true, never, TEffectHKT>
+     109              : 
+     110              : export type AnySQLiteEffectDelete = SQLiteEffectDeleteBase<any, any, any, any, any, any>
+     111              : 
+     112              : export interface SQLiteEffectDeleteBase<
+     113              :   TTable extends SQLiteTable,
+     114              :   TRunResult,
+     115              :   TReturning extends Record<string, unknown> | undefined = undefined,
+     116              :   TDynamic extends boolean = false,
+     117              :   _TExcludedMethods extends string = never,
+     118              :   TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+     119              : > extends RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">,
+     120              :     SQLWrapper,
+     121              :     Effect.Effect<
+     122              :       TReturning extends undefined ? TRunResult : TReturning[],
+     123              :       TEffectHKT["error"],
+     124              :       TEffectHKT["context"]
+     125              :     > {
+     126              :   readonly _: {
+     127              :     dialect: "sqlite"
+     128              :     readonly table: TTable
+     129              :     readonly resultType: "async"
+     130              :     readonly runResult: TRunResult
+     131              :     readonly returning: TReturning
+     132              :     readonly dynamic: TDynamic
+     133              :     readonly excludedMethods: _TExcludedMethods
+     134              :     readonly result: TReturning extends undefined ? TRunResult : TReturning[]
+     135              :     readonly effectHKT: TEffectHKT
+     136              :   }
+     137              : }
+     138              : 
+     139              : export class SQLiteEffectDeleteBase<
+     140              :     TTable extends SQLiteTable,
+     141              :     TRunResult,
+     142              :     TReturning extends Record<string, unknown> | undefined = undefined,
+     143              :     TDynamic extends boolean = false,
+     144              :     _TExcludedMethods extends string = never,
+     145              :     TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+     146              :   >
+     147              :   implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper
+     148              : {
+     149              :   static readonly [entityKind]: string = "SQLiteEffectDelete"
+     150              : 
+     151              :   /** @internal */
+     152            1 :   config: SQLiteDeleteConfig
+     153              : 
+     154           13 :   constructor(
+     155           60 :     private table: TTable,
+     156          108 :     private effectSession: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
+     157          108 :     private effectDialect: SQLiteDialect,
+     158           20 :     withList?: Subquery[],
+     159           10 :   ) {
+     160           76 :     this.config = { table, withList }
+     161              :   }
+     162              : 
+     163           31 :   where(where: SQL | undefined): SQLiteEffectDeleteWithout<this, TDynamic, "where"> {
+     164           60 :     this.config.where = where
+     165           29 :     return this as any
+     166              :   }
+     167              : 
+     168              :   orderBy(
+     169              :     builder: (deleteTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>,
+     170              :   ): SQLiteEffectDeleteWithout<this, TDynamic, "orderBy">
+     171              :   orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): SQLiteEffectDeleteWithout<this, TDynamic, "orderBy">
+     172            0 :   orderBy(
+     173            0 :     ...columns:
+     174            0 :       | [(deleteTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>]
+     175            0 :       | (SQLiteColumn | SQL | SQL.Aliased)[]
+     176            0 :   ): SQLiteEffectDeleteWithout<this, TDynamic, "orderBy"> {
+     177            0 :     if (typeof columns[0] === "function") {
+     178            0 :       const orderBy = columns[0](
+     179            0 :         new Proxy(
+     180            0 :           getTableColumnsRuntime(this.config.table),
+     181            0 :           new SelectionProxyHandler({ sqlAliasedBehavior: "alias", sqlBehavior: "sql" }),
+     182            0 :         ) as any,
+     183            0 :       )
+     184            0 : 
+     185            0 :       this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy]
+     186            0 :       return this as any
+     187            0 :     }
+     188            0 : 
+     189            0 :     this.config.orderBy = columns as (SQLiteColumn | SQL | SQL.Aliased)[]
+     190           18 :     return this as any
+     191              :   }
+     192              : 
+     193            0 :   limit(limit: number | Placeholder): SQLiteEffectDeleteWithout<this, TDynamic, "limit"> {
+     194            0 :     this.config.limit = limit
+     195           18 :     return this as any
+     196              :   }
+     197              : 
+     198              :   returning(): SQLiteEffectDeleteReturningAll<this, TDynamic>
+     199              :   returning<TSelectedFields extends SelectedFieldsFlat>(
+     200              :     fields: TSelectedFields,
+     201              :   ): SQLiteEffectDeleteReturning<this, TDynamic, TSelectedFields>
+     202           11 :   returning(
+     203           90 :     fields: SelectedFieldsFlat = getTableColumnsRuntime(this.table),
+     204           10 :   ): SQLiteEffectDeleteReturning<this, TDynamic, any> | SQLiteEffectDeleteReturningAll<this, TDynamic> {
+     205          112 :     this.config.returning = orderSelectedFields<SQLiteColumn>(fields)
+     206           29 :     return this as any
+     207              :   }
+     208              : 
+     209              :   /** @internal */
+     210           22 :   getSQL(): SQL {
+     211          117 :     return this.effectDialect.buildDeleteQuery(this.config)
+     212              :   }
+     213              : 
+     214            0 :   toSQL(): Query {
+     215           58 :     return this.effectDialect.sqlToQuery(this.getSQL())
+     216              :   }
+     217              : 
+     218              :   /** @internal */
+     219           66 :   _prepare(isOneTimeQuery = true): SQLiteEffectDeletePrepare<this, TEffectHKT> {
+     220          147 :     return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"](
+     221           92 :       this.effectDialect.sqlToQuery(this.getSQL()),
+     222           46 :       this.config.returning,
+     223           75 :       this.config.returning ? "all" : "run",
+     224           22 :       undefined,
+     225           14 :       {
+     226           42 :         type: "delete",
+     227           94 :         tables: extractUsedTable(this.config.table),
+     228            2 :       },
+     229            9 :     ) as SQLiteEffectDeletePrepare<this, TEffectHKT>
+     230              :   }
+     231              : 
+     232            0 :   prepare(): SQLiteEffectDeletePrepare<this, TEffectHKT> {
+     233           35 :     return this._prepare(false)
+     234              :   }
+     235              : 
+     236           67 :   run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => {
+     237          100 :     return this._prepare().run(placeholderValues)
+     238              :   }
+     239              : 
+     240            0 :   all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => {
+     241           55 :     return this._prepare().all(placeholderValues)
+     242              :   }
+     243              : 
+     244           67 :   get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => {
+     245          100 :     return this._prepare().get(placeholderValues)
+     246              :   }
+     247              : 
+     248            0 :   values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => {
+     249           58 :     return this._prepare().values(placeholderValues)
+     250              :   }
+     251              : 
+     252            0 :   execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => {
+     253           58 :     return this._prepare().execute(placeholderValues)
+     254              :   }
+     255              : 
+     256            0 :   $dynamic(): SQLiteEffectDeleteDynamic<this> {
+     257           17 :     return this as any
+     258              :   }
+     259            2 : }
+     260              : 
+     261           85 : applyEffectWrapper(SQLiteEffectDeleteBase)
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/index-sort-f.html b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/index-sort-f.html new file mode 100644 index 00000000..b15f8f40 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/index-sort-f.html @@ -0,0 +1,170 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/sqlite-core/effect + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/sqlite-core/effectCoverageTotalHit
Test:opencode-lcov.infoLines:55.9 %1114623
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
count.ts +
31.4%31.4%
+
31.4 %3511
db.ts +
33.2%33.2%
+
33.2 %20267
delete.ts +
62.3%62.3%
+
62.3 %6943
insert.ts +
83.1%83.1%
+
83.1 %11898
query.ts +
15.5%15.5%
+
15.5 %12920
raw.ts +
81.2%81.2%
+
81.2 %1613
select.ts +
87.1%87.1%
+
87.1 %10188
session.ts +
67.9%67.9%
+
67.9 %308209
update.ts +
54.4%54.4%
+
54.4 %13674
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/index-sort-l.html b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/index-sort-l.html new file mode 100644 index 00000000..81181d2e --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/index-sort-l.html @@ -0,0 +1,170 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/sqlite-core/effect + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/sqlite-core/effectCoverageTotalHit
Test:opencode-lcov.infoLines:55.9 %1114623
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
query.ts +
15.5%15.5%
+
15.5 %12920
count.ts +
31.4%31.4%
+
31.4 %3511
db.ts +
33.2%33.2%
+
33.2 %20267
update.ts +
54.4%54.4%
+
54.4 %13674
delete.ts +
62.3%62.3%
+
62.3 %6943
session.ts +
67.9%67.9%
+
67.9 %308209
raw.ts +
81.2%81.2%
+
81.2 %1613
insert.ts +
83.1%83.1%
+
83.1 %11898
select.ts +
87.1%87.1%
+
87.1 %10188
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/index.html b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/index.html new file mode 100644 index 00000000..7b79ee18 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/index.html @@ -0,0 +1,170 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/sqlite-core/effect + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/sqlite-core/effectCoverageTotalHit
Test:opencode-lcov.infoLines:55.9 %1114623
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
count.ts +
31.4%31.4%
+
31.4 %3511
db.ts +
33.2%33.2%
+
33.2 %20267
delete.ts +
62.3%62.3%
+
62.3 %6943
insert.ts +
83.1%83.1%
+
83.1 %11898
query.ts +
15.5%15.5%
+
15.5 %12920
raw.ts +
81.2%81.2%
+
81.2 %1613
select.ts +
87.1%87.1%
+
87.1 %10188
session.ts +
67.9%67.9%
+
67.9 %308209
update.ts +
54.4%54.4%
+
54.4 %13674
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/insert.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/insert.ts.gcov.html new file mode 100644 index 00000000..37d2f905 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/insert.ts.gcov.html @@ -0,0 +1,425 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/sqlite-core/effect/insert.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/sqlite-core/effect - insert.tsCoverageTotalHit
Test:opencode-lcov.infoLines:83.1 %11898
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2              : import type * as Effect from "effect/Effect"
+       3              : import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+       4              : import { entityKind, is } from "drizzle-orm/entity"
+       5              : import type { SelectResultFields } from "drizzle-orm/query-builders/select.types"
+       6              : import type { RunnableQuery } from "drizzle-orm/runnable-query"
+       7              : import type { Query, SQLWrapper } from "drizzle-orm/sql/sql"
+       8              : import { Param, SQL, sql } from "drizzle-orm/sql/sql"
+       9              : import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
+      10              : import type { IndexColumn } from "drizzle-orm/sqlite-core/indexes"
+      11              : import type {
+      12              :   SQLiteInsertConfig,
+      13              :   SQLiteInsertSelectQueryBuilder,
+      14              :   SQLiteInsertValue,
+      15              : } from "drizzle-orm/sqlite-core/query-builders/insert"
+      16              : import type { SelectedFieldsFlat } from "drizzle-orm/sqlite-core/query-builders/select.types"
+      17              : import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session"
+      18              : import { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+      19              : import { extractUsedTable } from "drizzle-orm/sqlite-core/utils"
+      20              : import type { Subquery } from "drizzle-orm/subquery"
+      21              : import { type DrizzleTypeError, haveSameKeys } from "drizzle-orm/utils"
+      22              : import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common"
+      23              : import { QueryBuilder } from "drizzle-orm/sqlite-core/query-builders/query-builder"
+      24              : import type { SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update"
+      25              : import { getTableColumnsRuntime, mapUpdateSet, orderSelectedFields } from "../../internal/drizzle-utils"
+      26              : import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
+      27              : 
+      28              : export type SQLiteEffectInsertWithout<
+      29              :   T extends AnySQLiteEffectInsert,
+      30              :   TDynamic extends boolean,
+      31              :   K extends keyof T & string,
+      32              : > = TDynamic extends true
+      33              :   ? T
+      34              :   : Omit<
+      35              :       SQLiteEffectInsertBase<
+      36              :         T["_"]["table"],
+      37              :         T["_"]["runResult"],
+      38              :         T["_"]["returning"],
+      39              :         TDynamic,
+      40              :         T["_"]["excludedMethods"] | K,
+      41              :         T["_"]["effectHKT"]
+      42              :       >,
+      43              :       T["_"]["excludedMethods"] | K
+      44              :     >
+      45              : 
+      46              : export type SQLiteEffectInsertReturning<
+      47              :   T extends AnySQLiteEffectInsert,
+      48              :   TDynamic extends boolean,
+      49              :   TSelectedFields extends SelectedFieldsFlat,
+      50              : > = SQLiteEffectInsertWithout<
+      51              :   SQLiteEffectInsertBase<
+      52              :     T["_"]["table"],
+      53              :     T["_"]["runResult"],
+      54              :     SelectResultFields<TSelectedFields>,
+      55              :     TDynamic,
+      56              :     T["_"]["excludedMethods"],
+      57              :     T["_"]["effectHKT"]
+      58              :   >,
+      59              :   TDynamic,
+      60              :   "returning"
+      61              : >
+      62              : 
+      63              : export type SQLiteEffectInsertReturningAll<
+      64              :   T extends AnySQLiteEffectInsert,
+      65              :   TDynamic extends boolean,
+      66              : > = SQLiteEffectInsertWithout<
+      67              :   SQLiteEffectInsertBase<
+      68              :     T["_"]["table"],
+      69              :     T["_"]["runResult"],
+      70              :     T["_"]["table"]["$inferSelect"],
+      71              :     TDynamic,
+      72              :     T["_"]["excludedMethods"],
+      73              :     T["_"]["effectHKT"]
+      74              :   >,
+      75              :   TDynamic,
+      76              :   "returning"
+      77              : >
+      78              : 
+      79              : export type SQLiteEffectInsertDynamic<T extends AnySQLiteEffectInsert> = SQLiteEffectInsert<
+      80              :   T["_"]["table"],
+      81              :   T["_"]["runResult"],
+      82              :   T["_"]["returning"],
+      83              :   T["_"]["effectHKT"]
+      84              : >
+      85              : 
+      86              : export type SQLiteEffectInsertOnConflictDoUpdateConfig<T extends AnySQLiteEffectInsert> = {
+      87              :   target: IndexColumn | IndexColumn[]
+      88              :   /** @deprecated - use either `targetWhere` or `setWhere` */
+      89              :   where?: SQL
+      90              :   targetWhere?: SQL
+      91              :   setWhere?: SQL
+      92              :   set: SQLiteUpdateSetSource<T["_"]["table"]>
+      93              : }
+      94              : 
+      95              : export type SQLiteEffectInsertExecute<T extends AnySQLiteEffectInsert> = T["_"]["returning"] extends undefined
+      96              :   ? T["_"]["runResult"]
+      97              :   : T["_"]["returning"][]
+      98              : 
+      99              : export type SQLiteEffectInsertPrepare<
+     100              :   T extends AnySQLiteEffectInsert,
+     101              :   TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"],
+     102              : > = SQLiteEffectPreparedQuery<
+     103              :   PreparedQueryConfig & {
+     104              :     run: T["_"]["runResult"]
+     105              :     all: T["_"]["returning"] extends undefined
+     106              :       ? DrizzleTypeError<".all() cannot be used without .returning()">
+     107              :       : T["_"]["returning"][]
+     108              :     get: T["_"]["returning"] extends undefined
+     109              :       ? DrizzleTypeError<".get() cannot be used without .returning()">
+     110              :       : T["_"]["returning"]
+     111              :     values: T["_"]["returning"] extends undefined
+     112              :       ? DrizzleTypeError<".values() cannot be used without .returning()">
+     113              :       : any[][]
+     114              :     execute: SQLiteEffectInsertExecute<T>
+     115              :   },
+     116              :   TEffectHKT
+     117              : >
+     118              : 
+     119              : export type SQLiteEffectInsert<
+     120              :   TTable extends SQLiteTable = SQLiteTable,
+     121              :   TRunResult = unknown,
+     122              :   TReturning = any,
+     123              :   TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+     124              : > = SQLiteEffectInsertBase<TTable, TRunResult, TReturning, true, never, TEffectHKT>
+     125              : 
+     126              : export type AnySQLiteEffectInsert = SQLiteEffectInsertBase<any, any, any, any, any, any>
+     127              : 
+     128              : export class SQLiteEffectInsertBuilder<
+     129              :   TTable extends SQLiteTable,
+     130              :   TRunResult,
+     131              :   TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+     132              : > {
+     133            1 :   static readonly [entityKind]: string = "SQLiteEffectInsertBuilder"
+     134              : 
+     135           13 :   constructor(
+     136           60 :     protected table: TTable,
+     137           72 :     protected session: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
+     138           72 :     protected dialect: SQLiteDialect,
+     139           78 :     private withList?: Subquery[],
+     140           10 :   ) {}
+     141              : 
+     142              :   values(
+     143              :     value: SQLiteInsertValue<TTable>,
+     144              :   ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
+     145              :   values(
+     146              :     values: SQLiteInsertValue<TTable>[],
+     147              :   ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
+     148            8 :   values(
+     149           16 :     values: SQLiteInsertValue<TTable> | SQLiteInsertValue<TTable>[],
+     150           10 :   ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT> {
+     151          107 :     values = Array.isArray(values) ? values : [values]
+     152           56 :     if (values.length === 0) {
+     153           71 :       throw new Error("values() must be called with at least one value")
+     154            9 :     }
+     155           99 :     const mappedValues = values.map((entry) => {
+     156           48 :       const result: Record<string, Param | SQL> = {}
+     157          108 :       const cols = getTableColumnsRuntime(this.table)
+     158           99 :       for (const colKey of Object.keys(entry)) {
+     159           78 :         const colValue = entry[colKey as keyof typeof entry]
+     160          165 :         result[colKey] = is(colValue, SQL) ? colValue : new Param(colValue, cols[colKey])
+     161           13 :       }
+     162           32 :       return result
+     163           12 :     })
+     164              : 
+     165          211 :     return new SQLiteEffectInsertBase(this.table, mappedValues, this.session, this.dialect, this.withList)
+     166              :   }
+     167              : 
+     168              :   select(
+     169              :     selectQuery: (qb: QueryBuilder) => SQLiteInsertSelectQueryBuilder<TTable>,
+     170              :   ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
+     171              :   select(
+     172              :     selectQuery: (qb: QueryBuilder) => SQL,
+     173              :   ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
+     174              :   select(selectQuery: SQL): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
+     175              :   select(
+     176              :     selectQuery: SQLiteInsertSelectQueryBuilder<TTable>,
+     177              :   ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
+     178            0 :   select(
+     179            0 :     selectQuery:
+     180            0 :       | SQL
+     181            0 :       | SQLiteInsertSelectQueryBuilder<TTable>
+     182            0 :       | ((qb: QueryBuilder) => SQLiteInsertSelectQueryBuilder<TTable> | SQL),
+     183            0 :   ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT> {
+     184            0 :     const select = typeof selectQuery === "function" ? selectQuery(new QueryBuilder()) : selectQuery
+     185            0 : 
+     186            0 :     if (!is(select, SQL) && !haveSameKeys(getTableColumnsRuntime(this.table), select._.selectedFields)) {
+     187            0 :       throw new Error(
+     188            0 :         "Insert select error: selected fields are not the same or are in a different order compared to the table definition",
+     189            0 :       )
+     190            0 :     }
+     191            0 : 
+     192          108 :     return new SQLiteEffectInsertBase(this.table, select, this.session, this.dialect, this.withList, true)
+     193              :   }
+     194            2 : }
+     195              : 
+     196              : export interface SQLiteEffectInsertBase<
+     197              :   TTable extends SQLiteTable,
+     198              :   TRunResult,
+     199              :   TReturning = undefined,
+     200              :   TDynamic extends boolean = false,
+     201              :   _TExcludedMethods extends string = never,
+     202              :   TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+     203              : > extends SQLWrapper,
+     204              :     RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">,
+     205              :     Effect.Effect<
+     206              :       TReturning extends undefined ? TRunResult : TReturning[],
+     207              :       TEffectHKT["error"],
+     208              :       TEffectHKT["context"]
+     209              :     > {
+     210              :   readonly _: {
+     211              :     readonly dialect: "sqlite"
+     212              :     readonly table: TTable
+     213              :     readonly resultType: "async"
+     214              :     readonly runResult: TRunResult
+     215              :     readonly returning: TReturning
+     216              :     readonly dynamic: TDynamic
+     217              :     readonly excludedMethods: _TExcludedMethods
+     218              :     readonly result: TReturning extends undefined ? TRunResult : TReturning[]
+     219              :     readonly effectHKT: TEffectHKT
+     220              :   }
+     221              : }
+     222              : 
+     223           70 : export class SQLiteEffectInsertBase<
+     224              :     TTable extends SQLiteTable,
+     225              :     TRunResult,
+     226              :     TReturning = undefined,
+     227              :     TDynamic extends boolean = false,
+     228              :     _TExcludedMethods extends string = never,
+     229              :     TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+     230              :   >
+     231              :   implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper
+     232            6 : {
+     233           74 :   static readonly [entityKind]: string = "SQLiteEffectInsert"
+     234              : 
+     235              :   /** @internal */
+     236           17 :   config: SQLiteInsertConfig<TTable>
+     237              : 
+     238           13 :   constructor(
+     239           76 :     private table: TTable,
+     240           16 :     values: SQLiteInsertConfig["values"],
+     241          140 :     private effectSession: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
+     242          156 :     private effectDialect: SQLiteDialect,
+     243           20 :     withList?: Subquery[],
+     244           16 :     select?: boolean,
+     245           10 :   ) {
+     246          108 :     this.config = { table, values: values as any, withList, select }
+     247              :   }
+     248              : 
+     249              :   returning(): SQLiteEffectInsertReturningAll<this, TDynamic>
+     250              :   returning<TSelectedFields extends SelectedFieldsFlat>(
+     251              :     fields: TSelectedFields,
+     252              :   ): SQLiteEffectInsertReturning<this, TDynamic, TSelectedFields>
+     253           11 :   returning(
+     254          104 :     fields: SelectedFieldsFlat = getTableColumnsRuntime(this.config.table),
+     255           10 :   ): SQLiteEffectInsertWithout<AnySQLiteEffectInsert, TDynamic, "returning"> {
+     256          112 :     this.config.returning = orderSelectedFields<SQLiteColumn>(fields)
+     257           29 :     return this as any
+     258              :   }
+     259              : 
+     260           57 :   onConflictDoNothing(config: { target?: IndexColumn | IndexColumn[]; where?: SQL } = {}): this {
+     261          130 :     if (!this.config.onConflict) this.config.onConflict = []
+     262              : 
+     263           79 :     if (config.target === undefined) {
+     264          128 :       this.config.onConflict.push(sql` on conflict do nothing`)
+     265           27 :       return this
+     266            5 :     }
+     267              : 
+     268          101 :     const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}`
+     269           72 :     const whereSql = config.where ? sql` where ${config.where}` : sql``
+     270           86 :     this.config.onConflict.push(sql` on conflict ${targetSql} do nothing${whereSql}`)
+     271           18 :     return this
+     272              :   }
+     273              : 
+     274           46 :   onConflictDoUpdate(config: SQLiteEffectInsertOnConflictDoUpdateConfig<this>): this {
+     275          128 :     if (config.where && (config.targetWhere || config.setWhere)) {
+     276           16 :       throw new Error(
+     277          141 :         'You cannot use both "where" and "targetWhere"/"setWhere" at the same time - "where" is deprecated, use "targetWhere" or "setWhere" instead.',
+     278            6 :       )
+     279            9 :     }
+     280              : 
+     281          130 :     if (!this.config.onConflict) this.config.onConflict = []
+     282              : 
+     283          122 :     const whereSql = config.where ? sql` where ${config.where}` : undefined
+     284          152 :     const targetWhereSql = config.targetWhere ? sql` where ${config.targetWhere}` : undefined
+     285          167 :     const setWhereSql = config.setWhere ? sql` where ${config.setWhere}` : undefined
+     286          199 :     const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}`
+     287           98 :     const setSql = this.effectDialect.buildUpdateSet(
+     288           38 :       this.config.table,
+     289           86 :       mapUpdateSet(this.config.table, config.set as SQLiteUpdateSetSource<TTable>),
+     290           12 :     )
+     291           56 :     this.config.onConflict.push(
+     292          192 :       sql` on conflict ${targetSql}${targetWhereSql} do update set ${setSql}${whereSql}${setWhereSql}`,
+     293           12 :     )
+     294           29 :     return this
+     295              :   }
+     296              : 
+     297              :   /** @internal */
+     298           22 :   getSQL(): SQL {
+     299          117 :     return this.effectDialect.buildInsertQuery(this.config)
+     300              :   }
+     301              : 
+     302            0 :   toSQL(): Query {
+     303           58 :     return this.effectDialect.sqlToQuery(this.getSQL())
+     304              :   }
+     305              : 
+     306              :   /** @internal */
+     307           66 :   _prepare(isOneTimeQuery = true): SQLiteEffectInsertPrepare<this, TEffectHKT> {
+     308          147 :     return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"](
+     309           92 :       this.effectDialect.sqlToQuery(this.getSQL()),
+     310           46 :       this.config.returning,
+     311           75 :       this.config.returning ? "all" : "run",
+     312           22 :       undefined,
+     313           14 :       {
+     314           42 :         type: "insert",
+     315           94 :         tables: extractUsedTable(this.config.table),
+     316            2 :       },
+     317            9 :     ) as SQLiteEffectInsertPrepare<this, TEffectHKT>
+     318              :   }
+     319              : 
+     320            0 :   prepare(): SQLiteEffectInsertPrepare<this, TEffectHKT> {
+     321           35 :     return this._prepare(false)
+     322              :   }
+     323              : 
+     324           67 :   run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => {
+     325          100 :     return this._prepare().run(placeholderValues)
+     326              :   }
+     327              : 
+     328            0 :   all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => {
+     329           55 :     return this._prepare().all(placeholderValues)
+     330              :   }
+     331              : 
+     332           67 :   get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => {
+     333          100 :     return this._prepare().get(placeholderValues)
+     334              :   }
+     335              : 
+     336            0 :   values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => {
+     337           58 :     return this._prepare().values(placeholderValues)
+     338              :   }
+     339              : 
+     340            0 :   execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => {
+     341           58 :     return this._prepare().execute(placeholderValues)
+     342              :   }
+     343              : 
+     344            0 :   $dynamic(): SQLiteEffectInsertDynamic<this> {
+     345           17 :     return this as any
+     346              :   }
+     347            2 : }
+     348              : 
+     349           85 : applyEffectWrapper(SQLiteEffectInsertBase)
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/query.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/query.ts.gcov.html new file mode 100644 index 00000000..6d290cfb --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/query.ts.gcov.html @@ -0,0 +1,274 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/sqlite-core/effect/query.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/sqlite-core/effect - query.tsCoverageTotalHit
Test:opencode-lcov.infoLines:15.5 %12920
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2              : import type * as Effect from "effect/Effect"
+       3              : import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+       4              : import { entityKind } from "drizzle-orm/entity"
+       5              : import {
+       6              :   type BuildQueryResult,
+       7              :   type BuildRelationalQueryResult,
+       8              :   type DBQueryConfig,
+       9              :   makeDefaultRqbMapper,
+      10              :   type TableRelationalConfig,
+      11              :   type TablesRelationalConfig,
+      12              : } from "drizzle-orm/relations"
+      13              : import type { RunnableQuery } from "drizzle-orm/runnable-query"
+      14              : import { type Query, type SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql"
+      15              : import type { KnownKeysOnly } from "drizzle-orm/utils"
+      16              : import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
+      17              : import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session"
+      18              : import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+      19              : import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
+      20              : 
+      21              : export class SQLiteEffectRelationalQueryBuilder<
+      22              :   TSchema extends TablesRelationalConfig,
+      23              :   TFields extends TableRelationalConfig,
+      24              :   TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+      25              : > {
+      26            1 :   static readonly [entityKind]: string = "SQLiteEffectRelationalQueryBuilderV2"
+      27              : 
+      28            0 :   constructor(
+      29            0 :     private schema: TSchema,
+      30            0 :     private table: SQLiteTable,
+      31            0 :     private tableConfig: TableRelationalConfig,
+      32            0 :     private dialect: SQLiteDialect,
+      33            0 :     private session: SQLiteEffectSession<TEffectHKT, any, any>,
+      34            0 :     private rowMode?: boolean,
+      35            0 :     private forbidJsonb?: boolean,
+      36            5 :   ) {}
+      37              : 
+      38            0 :   findMany<TConfig extends DBQueryConfig<"many", TSchema, TFields>>(
+      39            0 :     config?: KnownKeysOnly<TConfig, DBQueryConfig<"many", TSchema, TFields>>,
+      40            0 :   ): SQLiteEffectRelationalQuery<BuildQueryResult<TSchema, TFields, TConfig>[], TEffectHKT> {
+      41            0 :     return new SQLiteEffectRelationalQuery(
+      42            0 :       this.schema,
+      43            0 :       this.table,
+      44            0 :       this.tableConfig,
+      45            0 :       this.dialect,
+      46            0 :       this.session,
+      47            0 :       (config as DBQueryConfig<"many"> | undefined) ?? true,
+      48            0 :       "many",
+      49            0 :       this.rowMode,
+      50            0 :       this.forbidJsonb,
+      51            8 :     )
+      52              :   }
+      53              : 
+      54            0 :   findFirst<TConfig extends DBQueryConfig<"one", TSchema, TFields>>(
+      55            0 :     config?: KnownKeysOnly<TConfig, DBQueryConfig<"one", TSchema, TFields>>,
+      56            0 :   ): SQLiteEffectRelationalQuery<BuildQueryResult<TSchema, TFields, TConfig> | undefined, TEffectHKT> {
+      57            0 :     return new SQLiteEffectRelationalQuery(
+      58            0 :       this.schema,
+      59            0 :       this.table,
+      60            0 :       this.tableConfig,
+      61            0 :       this.dialect,
+      62            0 :       this.session,
+      63            0 :       (config as DBQueryConfig<"one"> | undefined) ?? true,
+      64            0 :       "first",
+      65            0 :       this.rowMode,
+      66            0 :       this.forbidJsonb,
+      67            7 :     )
+      68              :   }
+      69            2 : }
+      70              : 
+      71              : export interface SQLiteEffectRelationalQuery<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
+      72              :   extends Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>,
+      73              :     RunnableQuery<TResult, "sqlite">,
+      74              :     SQLWrapper {}
+      75              : 
+      76           80 : export class SQLiteEffectRelationalQuery<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
+      77              :   implements RunnableQuery<TResult, "sqlite">, SQLWrapper
+      78            6 : {
+      79           96 :   static readonly [entityKind]: string = "SQLiteEffectRelationalQueryV2"
+      80              : 
+      81              :   declare readonly _: {
+      82              :     readonly dialect: "sqlite"
+      83              :     readonly type: "async"
+      84              :     readonly result: TResult
+      85              :   }
+      86              : 
+      87              :   /** @internal */
+      88           14 :   mode: "many" | "first"
+      89              :   /** @internal */
+      90           15 :   table: SQLiteTable
+      91              : 
+      92            0 :   constructor(
+      93            0 :     private schema: TablesRelationalConfig,
+      94            0 :     table: SQLiteTable,
+      95            0 :     private tableConfig: TableRelationalConfig,
+      96            0 :     private dialect: SQLiteDialect,
+      97            0 :     private session: SQLiteEffectSession<TEffectHKT, any, any>,
+      98            0 :     private config: DBQueryConfig<"many" | "one"> | true,
+      99            0 :     mode: "many" | "first",
+     100            0 :     private rowMode?: boolean,
+     101            0 :     private forbidJsonb?: boolean,
+     102            0 :   ) {
+     103            0 :     this.mode = mode
+     104           25 :     this.table = table
+     105              :   }
+     106              : 
+     107              :   /** @internal */
+     108            0 :   getSQL(): SQL {
+     109           34 :     return this._getQuery().sql
+     110              :   }
+     111              : 
+     112              :   /** @internal */
+     113            0 :   _prepare(
+     114            0 :     isOneTimeQuery = true,
+     115            0 :   ): SQLiteEffectPreparedQuery<
+     116            0 :     PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult },
+     117            0 :     TEffectHKT,
+     118            0 :     true
+     119            0 :   > {
+     120            0 :     const { query, builtQuery } = this._toSQL()
+     121            0 :     const mapperConfig = {
+     122            0 :       isFirst: this.mode === "first",
+     123            0 :       parseJson: !this.rowMode,
+     124            0 :       parseJsonIfString: false,
+     125            0 :       rootJsonMappers: true,
+     126            0 :       selection: query.selection,
+     127            0 :     }
+     128            0 : 
+     129            0 :     return this.session[isOneTimeQuery ? "prepareOneTimeRelationalQuery" : "prepareRelationalQuery"](
+     130            0 :       builtQuery,
+     131            0 :       undefined,
+     132            0 :       this.mode === "first" ? "get" : "all",
+     133            0 :       makeDefaultRqbMapper(mapperConfig),
+     134            0 :       mapperConfig,
+     135            8 :     ) as SQLiteEffectPreparedQuery<
+     136              :       PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult },
+     137              :       TEffectHKT,
+     138              :       true
+     139              :     >
+     140              :   }
+     141              : 
+     142            0 :   prepare(): SQLiteEffectPreparedQuery<
+     143            0 :     PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult },
+     144            0 :     TEffectHKT,
+     145            0 :     true
+     146            0 :   > {
+     147           34 :     return this._prepare(false)
+     148              :   }
+     149              : 
+     150            0 :   private _getQuery() {
+     151            0 :     const jsonb = this.forbidJsonb ? sql`json` : sql`jsonb`
+     152            0 : 
+     153            0 :     const query = this.dialect.buildRelationalQuery({
+     154            0 :       schema: this.schema,
+     155            0 :       table: this.table,
+     156            0 :       tableConfig: this.tableConfig,
+     157            0 :       queryConfig: this.config,
+     158            0 :       mode: this.mode,
+     159            0 :       isNested: this.rowMode,
+     160            0 :       jsonb,
+     161            0 :     })
+     162            0 : 
+     163            0 :     if (this.rowMode) {
+     164            0 :       const jsonColumns = sql.join(
+     165            0 :         query.selection.map((s) => {
+     166            0 :           return sql`${sql.raw(this.dialect.escapeString(s.key))}, ${
+     167            0 :             s.selection ? sql`${jsonb}(${sql.identifier(s.key)})` : sql.identifier(s.key)
+     168            0 :           }`
+     169            0 :         }),
+     170            0 :         sql`, `,
+     171            0 :       )
+     172            0 : 
+     173            0 :       query.sql = sql`select json_object(${jsonColumns}) as ${sql.identifier("r")} from (${query.sql}) as ${sql.identifier(
+     174            0 :         "t",
+     175            0 :       )}`
+     176            0 :     }
+     177            0 : 
+     178           19 :     return query
+     179              :   }
+     180              : 
+     181            0 :   private _toSQL(): { query: BuildRelationalQueryResult; builtQuery: Query } {
+     182            0 :     const query = this._getQuery()
+     183            0 : 
+     184            0 :     const builtQuery = this.dialect.sqlToQuery(query.sql)
+     185            0 : 
+     186           35 :     return { query, builtQuery }
+     187              :   }
+     188              : 
+     189            0 :   toSQL(): Query {
+     190           38 :     return this._toSQL().builtQuery
+     191              :   }
+     192              : 
+     193            0 :   execute(placeholderValues?: Record<string, unknown>) {
+     194          116 :     return this.mode === "first" ? this._prepare().get(placeholderValues) : this._prepare().all(placeholderValues)
+     195              :   }
+     196            2 : }
+     197              : 
+     198           95 : applyEffectWrapper(SQLiteEffectRelationalQuery)
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/raw.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/raw.ts.gcov.html new file mode 100644 index 00000000..8f940fef --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/raw.ts.gcov.html @@ -0,0 +1,125 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/sqlite-core/effect/raw.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/sqlite-core/effect - raw.tsCoverageTotalHit
Test:opencode-lcov.infoLines:81.2 %1613
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2              : import type * as Effect from "effect/Effect"
+       3              : import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+       4              : import { entityKind } from "drizzle-orm/entity"
+       5              : import type { RunnableQuery } from "drizzle-orm/runnable-query"
+       6              : import type { PreparedQuery } from "drizzle-orm/session"
+       7              : import type { Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql"
+       8              : import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
+       9              : 
+      10              : type SQLiteEffectRawAction = "all" | "get" | "values" | "run"
+      11              : 
+      12              : export interface SQLiteEffectRaw<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
+      13              :   extends Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>,
+      14              :     RunnableQuery<TResult, "sqlite">,
+      15              :     SQLWrapper {}
+      16              : 
+      17              : export class SQLiteEffectRaw<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
+      18              :   implements RunnableQuery<TResult, "sqlite">, SQLWrapper, PreparedQuery
+      19              : {
+      20            1 :   static readonly [entityKind]: string = "SQLiteEffectRaw"
+      21              : 
+      22              :   declare readonly _: {
+      23              :     readonly dialect: "sqlite"
+      24              :     readonly result: TResult
+      25              :   }
+      26              : 
+      27           13 :   constructor(
+      28           72 :     public execute: () => Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>,
+      29              :     /** @internal */
+      30           66 :     public getSQL: () => SQL,
+      31           66 :     private action: SQLiteEffectRawAction,
+      32           72 :     private dialect: SQLiteAsyncDialect,
+      33          114 :     private mapBatchResult: (result: unknown) => unknown,
+      34           10 :   ) {}
+      35              : 
+      36            0 :   getQuery(): Query & { method: SQLiteEffectRawAction } {
+      37           80 :     return { ...this.dialect.sqlToQuery(this.getSQL()), method: this.action }
+      38              :   }
+      39              : 
+      40            0 :   mapResult(result: unknown, isFromBatch?: boolean) {
+      41           64 :     return isFromBatch ? this.mapBatchResult(result) : result
+      42              :   }
+      43              : 
+      44            0 :   _prepare(): PreparedQuery {
+      45           17 :     return this
+      46              :   }
+      47            2 : }
+      48              : 
+      49           71 : applyEffectWrapper(SQLiteEffectRaw)
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/select.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/select.ts.gcov.html new file mode 100644 index 00000000..05e931ce --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/select.ts.gcov.html @@ -0,0 +1,355 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/sqlite-core/effect/select.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/sqlite-core/effect - select.tsCoverageTotalHit
Test:opencode-lcov.infoLines:87.1 %10188
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2              : import type * as Effect from "effect/Effect"
+       3              : import type { CacheConfig } from "drizzle-orm/cache/core/types"
+       4          110 : import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+       5          104 : import { entityKind, is } from "drizzle-orm/entity"
+       6              : import type {
+       7              :   BuildSubquerySelection,
+       8              :   GetSelectTableName,
+       9              :   GetSelectTableSelection,
+      10              :   JoinNullability,
+      11              :   SelectMode,
+      12              :   SelectResult,
+      13              : } from "drizzle-orm/query-builders/select.types"
+      14           84 : import { SQL } from "drizzle-orm/sql/sql"
+      15              : import type { ColumnsSelection, SQLWrapper } from "drizzle-orm/sql/sql"
+      16              : import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns"
+      17              : import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
+      18          186 : import { SQLiteSelectQueryBuilderBase } from "drizzle-orm/sqlite-core/query-builders/select"
+      19              : import type {
+      20              :   CreateSQLiteSelectFromBuilderMode,
+      21              :   SelectedFields,
+      22              :   SQLiteSelectConfig,
+      23              :   SQLiteSelectHKTBase,
+      24              : } from "drizzle-orm/sqlite-core/query-builders/select.types"
+      25              : import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+      26          134 : import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base"
+      27           96 : import { Subquery } from "drizzle-orm/subquery"
+      28          104 : import { type Assume, getTableColumns } from "drizzle-orm/utils"
+      29          194 : import { getViewSelectedFieldsRuntime, orderSelectedFields } from "../../internal/drizzle-utils"
+      30              : import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
+      31              : 
+      32              : export type SQLiteEffectSelectPrepare<
+      33              :   T extends AnySQLiteEffectSelect,
+      34              :   TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+      35              : > = SQLiteEffectPreparedQuery<
+      36              :   {
+      37              :     type: "async"
+      38              :     run: T["_"]["runResult"]
+      39              :     all: T["_"]["result"]
+      40              :     get: T["_"]["result"][number] | undefined
+      41              :     values: any[][]
+      42              :     execute: T["_"]["result"]
+      43              :   },
+      44              :   TEffectHKT
+      45              : >
+      46              : 
+      47           76 : export class SQLiteEffectSelectBuilder<
+      48              :   TSelection extends SelectedFields | undefined,
+      49              :   TRunResult,
+      50              :   TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+      51              :   TBuilderMode extends "db" | "qb" = "db",
+      52           22 : > {
+      53           88 :   static readonly [entityKind]: string = "SQLiteEffectSelectBuilder"
+      54              : 
+      55           18 :   private fields: TSelection
+      56           20 :   private session: SQLiteEffectSession<TEffectHKT, TRunResult, any> | undefined
+      57           20 :   private dialect: SQLiteDialect
+      58           22 :   private withList: Subquery[] | undefined
+      59           21 :   private distinct: boolean | undefined
+      60              : 
+      61           29 :   constructor(config: {
+      62              :     fields: TSelection
+      63              :     session: SQLiteEffectSession<TEffectHKT, TRunResult, any> | undefined
+      64              :     dialect: SQLiteDialect
+      65              :     withList?: Subquery[]
+      66              :     distinct?: boolean
+      67           10 :   }) {
+      68           64 :     this.fields = config.fields
+      69           68 :     this.session = config.session
+      70           68 :     this.dialect = config.dialect
+      71           72 :     this.withList = config.withList
+      72           72 :     this.distinct = config.distinct
+      73              :   }
+      74              : 
+      75            6 :   from<TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL>(
+      76           16 :     source: TFrom,
+      77              :   ): TBuilderMode extends "db"
+      78              :     ? SQLiteEffectSelectBase<
+      79              :         GetSelectTableName<TFrom>,
+      80              :         TRunResult,
+      81              :         TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection,
+      82              :         TSelection extends undefined ? "single" : "partial",
+      83              :         GetSelectTableName<TFrom> extends string ? Record<GetSelectTableName<TFrom>, "not-null"> : {},
+      84              :         false,
+      85              :         never,
+      86              :         SelectResult<
+      87              :           TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection,
+      88              :           TSelection extends undefined ? "single" : "partial",
+      89              :           GetSelectTableName<TFrom> extends string ? Record<GetSelectTableName<TFrom>, "not-null"> : {}
+      90              :         >[],
+      91              :         BuildSubquerySelection<
+      92              :           TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection,
+      93              :           GetSelectTableName<TFrom> extends string ? Record<GetSelectTableName<TFrom>, "not-null"> : {}
+      94              :         >,
+      95              :         TEffectHKT
+      96              :       >
+      97              :     : CreateSQLiteSelectFromBuilderMode<
+      98              :         TBuilderMode,
+      99              :         GetSelectTableName<TFrom>,
+     100              :         "async",
+     101              :         TRunResult,
+     102              :         TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection,
+     103              :         TSelection extends undefined ? "single" : "partial"
+     104           10 :       > {
+     105           84 :     const isPartialSelect = !!this.fields
+     106              : 
+     107           30 :     let fields: SelectedFields
+     108           47 :     if (this.fields) {
+     109           50 :       fields = this.fields
+     110           71 :     } else if (is(source, Subquery)) {
+     111           28 :       fields = Object.fromEntries(
+     112            0 :         Object.keys(source._.selectedFields).map((key) => [
+     113            0 :           key,
+     114           17 :           source[key as unknown as keyof typeof source] as unknown as SelectedFields[string],
+     115            2 :         ]),
+     116            6 :       )
+     117           83 :     } else if (is(source, SQLiteViewBase)) {
+     118           65 :       fields = getViewSelectedFieldsRuntime(source).selectedFields as SelectedFields
+     119           61 :     } else if (is(source, SQL)) {
+     120           16 :       fields = {}
+     121           27 :     } else {
+     122           84 :       fields = getTableColumns<SQLiteTable>(source)
+     123              :     }
+     124              : 
+     125           82 :     return new SQLiteEffectSelectBase({
+     126           40 :       table: source,
+     127           26 :       fields,
+     128           44 :       isPartialSelect,
+     129           56 :       session: this.session as any,
+     130           56 :       dialect: this.dialect,
+     131           60 :       withList: this.withList,
+     132           54 :       distinct: this.distinct,
+     133           10 :     }) as any
+     134              :   }
+     135            2 : }
+     136              : 
+     137              : export interface SQLiteEffectSelectHKT<TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
+     138              :   extends SQLiteSelectHKTBase {
+     139              :   _type: SQLiteEffectSelectBase<
+     140              :     this["tableName"],
+     141              :     this["runResult"],
+     142              :     Assume<this["selection"], ColumnsSelection>,
+     143              :     this["selectMode"],
+     144              :     Assume<this["nullabilityMap"], Record<string, JoinNullability>>,
+     145              :     this["dynamic"],
+     146              :     this["excludedMethods"],
+     147              :     Assume<this["result"], any[]>,
+     148              :     Assume<this["selectedFields"], ColumnsSelection>,
+     149              :     TEffectHKT
+     150              :   >
+     151              : }
+     152              : 
+     153              : export interface SQLiteEffectSelectBase<
+     154              :   TTableName extends string | undefined,
+     155              :   TRunResult,
+     156              :   TSelection extends ColumnsSelection,
+     157              :   TSelectMode extends SelectMode = "single",
+     158              :   TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string
+     159              :     ? Record<TTableName, "not-null">
+     160              :     : {},
+     161              :   TDynamic extends boolean = false,
+     162              :   TExcludedMethods extends string = never,
+     163              :   TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
+     164              :   TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>,
+     165              :   TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+     166              : > extends SQLiteSelectQueryBuilderBase<
+     167              :       SQLiteEffectSelectHKT<TEffectHKT>,
+     168              :       TTableName,
+     169              :       "async",
+     170              :       TRunResult,
+     171              :       TSelection,
+     172              :       TSelectMode,
+     173              :       TNullabilityMap,
+     174              :       TDynamic,
+     175              :       TExcludedMethods,
+     176              :       TResult,
+     177              :       TSelectedFields
+     178              :     >,
+     179              :     Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]> {}
+     180              : 
+     181           86 : export class SQLiteEffectSelectBase<
+     182              :     TTableName extends string | undefined,
+     183              :     TRunResult,
+     184              :     TSelection extends ColumnsSelection,
+     185              :     TSelectMode extends SelectMode = "single",
+     186              :     TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string
+     187              :       ? Record<TTableName, "not-null">
+     188              :       : {},
+     189              :     TDynamic extends boolean = false,
+     190              :     TExcludedMethods extends string = never,
+     191              :     TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
+     192              :     TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>,
+     193              :     TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+     194              :   >
+     195           58 :   extends SQLiteSelectQueryBuilderBase<
+     196              :     SQLiteEffectSelectHKT<TEffectHKT>,
+     197              :     TTableName,
+     198              :     "async",
+     199              :     TRunResult,
+     200              :     TSelection,
+     201              :     TSelectMode,
+     202              :     TNullabilityMap,
+     203              :     TDynamic,
+     204              :     TExcludedMethods,
+     205              :     TResult,
+     206              :     TSelectedFields
+     207              :   >
+     208              :   implements SQLWrapper
+     209           22 : {
+     210           77 :   static override readonly [entityKind]: string = "SQLiteEffectSelect"
+     211              : 
+     212           28 :   private get effectConfig() {
+     213           43 :     return (this as unknown as { config: SQLiteSelectConfig }).config
+     214              :   }
+     215              : 
+     216              :   /** @internal */
+     217           22 :   getSQL(): SQL {
+     218          117 :     return this.dialect.buildSelectQuery(this.effectConfig)
+     219              :   }
+     220              : 
+     221              :   /** @internal */
+     222           66 :   _prepare(isOneTimeQuery = true): SQLiteEffectSelectPrepare<this, TEffectHKT> {
+     223           44 :     if (!this.session) {
+     224          106 :       throw new Error("Cannot execute a query on a query builder. Please use a database instance instead.")
+     225            9 :     }
+     226           66 :     const session = this.session as unknown as SQLiteEffectSession<TEffectHKT, TRunResult, any>
+     227          139 :     const query = session[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"](
+     228           80 :       this.dialect.sqlToQuery(this.getSQL()),
+     229           94 :       orderSelectedFields<SQLiteColumn>(this.effectConfig.fields),
+     230           14 :       "all",
+     231           22 :       undefined,
+     232           14 :       {
+     233           42 :         type: "select",
+     234           64 :         tables: [...this.usedTables],
+     235            6 :       },
+     236           32 :       this.cacheConfig,
+     237           12 :     )
+     238          114 :     query.joinsNotNullableMap = this.joinsNotNullableMap
+     239           31 :     return query as ReturnType<this["prepare"]>
+     240              :   }
+     241              : 
+     242            0 :   $withCache(config?: { config?: CacheConfig; tag?: string; autoInvalidate?: boolean } | false) {
+     243            0 :     this.cacheConfig =
+     244            0 :       config === undefined
+     245            0 :         ? { config: {}, enabled: true, autoInvalidate: true }
+     246            0 :         : config === false
+     247            0 :           ? { enabled: false }
+     248            0 :           : { enabled: true, autoInvalidate: true, ...config }
+     249           18 :     return this
+     250              :   }
+     251              : 
+     252            0 :   prepare(): SQLiteEffectSelectPrepare<this, TEffectHKT> {
+     253           35 :     return this._prepare(false)
+     254              :   }
+     255              : 
+     256            0 :   run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => {
+     257           55 :     return this._prepare().run(placeholderValues)
+     258              :   }
+     259              : 
+     260           67 :   all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => {
+     261          100 :     return this._prepare().all(placeholderValues)
+     262              :   }
+     263              : 
+     264           67 :   get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => {
+     265          100 :     return this._prepare().get(placeholderValues)
+     266              :   }
+     267              : 
+     268            0 :   values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => {
+     269           58 :     return this._prepare().values(placeholderValues)
+     270              :   }
+     271              : 
+     272            0 :   execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => {
+     273           57 :     return this._prepare().execute(placeholderValues)
+     274              :   }
+     275            2 : }
+     276              : 
+     277           85 : applyEffectWrapper(SQLiteEffectSelectBase)
+     278              : 
+     279              : export type AnySQLiteEffectSelect = SQLiteEffectSelectBase<any, any, any, any, any, any, any, any, any, any>
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/session.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/session.ts.gcov.html new file mode 100644 index 00000000..a72374cc --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/session.ts.gcov.html @@ -0,0 +1,566 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/sqlite-core/effect/session.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/sqlite-core/effect - session.tsCoverageTotalHit
Test:opencode-lcov.infoLines:67.9 %308209
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2              : import * as Cause from "effect/Cause"
+       3              : import * as Effect from "effect/Effect"
+       4              : import type { SqlError } from "effect/unstable/sql/SqlError"
+       5              : import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect"
+       6              : import { NoopCache, strategyFor } from "drizzle-orm/cache/core/cache"
+       7              : import type { WithCacheConfig } from "drizzle-orm/cache/core/types"
+       8              : import { MigratorInitError } from "drizzle-orm/effect-core/errors"
+       9              : import { EffectDrizzleQueryError, EffectTransactionRollbackError } from "drizzle-orm/effect-core/errors"
+      10              : import type { EffectLoggerShape } from "drizzle-orm/effect-core/logger"
+      11              : import type { QueryEffectHKTBase, QueryEffectKind } from "drizzle-orm/effect-core/query-effect"
+      12              : import { entityKind, is } from "drizzle-orm/entity"
+      13              : import type { MigrationConfig, MigrationMeta } from "drizzle-orm/migrator"
+      14              : import { getMigrationsToRun } from "drizzle-orm/migrator.utils"
+      15              : import type {
+      16              :   AnyRelations,
+      17              :   EmptyRelations,
+      18              :   RelationalQueryMapperConfig,
+      19              :   RelationalRowsMapper,
+      20              : } from "drizzle-orm/relations"
+      21              : import { makeJitRqbMapper } from "drizzle-orm/relations"
+      22              : import type { PreparedQuery } from "drizzle-orm/session"
+      23              : import { fillPlaceholders, type Query, type SQL, sql } from "drizzle-orm/sql/sql"
+      24              : import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
+      25              : import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types"
+      26              : import type { PreparedQueryConfig, SQLiteExecuteMethod, SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session"
+      27              : import { upgradeIfNeeded } from "../../up-migrations/effect-sqlite"
+      28              : import { assertUnreachable, makeJitQueryMapper, type RowsMapper } from "drizzle-orm/utils"
+      29              : import { mapResultRow } from "../../internal/drizzle-utils"
+      30              : import { SQLiteEffectDatabase } from "./db"
+      31              : 
+      32              : type MigrationConfigWithInit = MigrationConfig & { init?: boolean }
+      33              : 
+      34              : type SQLiteEffectExecuteMethod = SQLiteExecuteMethod | "values"
+      35              : 
+      36              : export class SQLiteEffectPreparedQuery<
+      37              :   T extends PreparedQueryConfig,
+      38              :   TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+      39              :   TIsRqbV2 extends boolean = false,
+      40              : > implements PreparedQuery
+      41              : {
+      42              :   static readonly [entityKind]: string = "SQLiteEffectPreparedQuery"
+      43              : 
+      44              :   /** @internal */
+      45              :   joinsNotNullableMap?: Record<string, boolean>
+      46              :   private jitMapper?: RowsMapper<any> | RelationalRowsMapper<any>
+      47              :   private cacheConfig: WithCacheConfig | undefined
+      48            1 :   private effectExecuteMethod: SQLiteExecuteMethod
+      49              : 
+      50           13 :   constructor(
+      51           78 :     private executor: (
+      52              :       params: unknown[],
+      53              :       executeMethod: SQLiteEffectExecuteMethod,
+      54              :     ) => Effect.Effect<unknown, unknown, unknown>,
+      55           60 :     protected query: Query,
+      56           66 :     private logger: EffectLoggerShape,
+      57           60 :     private cache: EffectCacheShape,
+      58          108 :     private queryMetadata:
+      59              :       | {
+      60              :           type: "select" | "update" | "delete" | "insert"
+      61              :           tables: string[]
+      62              :         }
+      63              :       | undefined,
+      64           26 :     cacheConfig: WithCacheConfig | undefined,
+      65           66 :     private fields: SelectedFieldsOrdered | undefined,
+      66           30 :     executeMethod: SQLiteExecuteMethod,
+      67          108 :     private useJitMappers: boolean | undefined,
+      68          138 :     private customResultMapper?: (
+      69              :       rows: TIsRqbV2 extends true ? Record<string, unknown>[] : unknown[][],
+      70              :       mapColumnValue?: (value: unknown) => unknown,
+      71              :     ) => unknown,
+      72          102 :     private isRqbV2Query?: TIsRqbV2,
+      73           84 :     private rqbConfig?: RelationalQueryMapperConfig,
+      74          168 :     private isInTransaction: Effect.Effect<boolean> = Effect.succeed(false),
+      75           10 :   ) {
+      76           90 :     this.effectExecuteMethod = executeMethod
+      77           38 :     this.cacheConfig =
+      78          216 :       cache.strategy() === "all" && cacheConfig === undefined ? { enabled: true, autoInvalidate: true } : cacheConfig
+      79           70 :     if (!this.cacheConfig?.enabled) {
+      80           33 :       this.cacheConfig = undefined
+      81            9 :     }
+      82              :   }
+      83              : 
+      84              :   run(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["run"]>
+      85           53 :   run(placeholderValues?: Record<string, unknown>): any {
+      86          115 :     return this.executeWithCache<T["run"]>(placeholderValues, "run")
+      87              :   }
+      88              : 
+      89              :   all(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["all"]>
+      90           53 :   all(placeholderValues?: Record<string, unknown>): any {
+      91          103 :     if (this.isRqbV2Query) return this.allRqbV2(placeholderValues)
+      92              : 
+      93          105 :     if (!this.fields && !this.customResultMapper) {
+      94          113 :       return this.executeWithCache<T["all"]>(placeholderValues, "all")
+      95            9 :     }
+      96              : 
+      97           58 :     return this.executeWithCache<T["values"], T["all"]>(
+      98           38 :       placeholderValues,
+      99           19 :       "values",
+     100           65 :       (rows) => this.mapAllResult(rows) as T["all"],
+     101            9 :     )
+     102              :   }
+     103              : 
+     104              :   get(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["get"]>
+     105           53 :   get(placeholderValues?: Record<string, unknown>): any {
+     106          103 :     if (this.isRqbV2Query) return this.getRqbV2(placeholderValues)
+     107              : 
+     108          105 :     if (!this.fields && !this.customResultMapper) {
+     109          113 :       return this.executeWithCache<T["get"]>(placeholderValues, "get")
+     110            9 :     }
+     111              : 
+     112           58 :     return this.executeWithCache<T["values"], T["get"]>(
+     113           38 :       placeholderValues,
+     114           19 :       "values",
+     115           65 :       (rows) => this.mapGetResult(rows) as T["get"],
+     116            9 :     )
+     117              :   }
+     118              : 
+     119              :   values(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["values"]>
+     120            0 :   values(placeholderValues?: Record<string, unknown>): any {
+     121           64 :     return this.executeWithCache<T["values"]>(placeholderValues, "values")
+     122              :   }
+     123              : 
+     124              :   execute(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["execute"]>
+     125            0 :   execute(placeholderValues?: Record<string, unknown>): any {
+     126           63 :     return this[this.effectExecuteMethod](placeholderValues) as QueryEffectKind<TEffectHKT, T["execute"]>
+     127              :   }
+     128              : 
+     129            0 :   mapRunResult(result: unknown, _isFromBatch?: boolean): unknown {
+     130           20 :     return result
+     131              :   }
+     132              : 
+     133           62 :   mapAllResult(rows: unknown, isFromBatch?: boolean): unknown {
+     134           40 :     if (isFromBatch) {
+     135           43 :       rows = Array.isArray(rows) ? rows : []
+     136            9 :     }
+     137              : 
+     138           98 :     if (!this.fields && !this.customResultMapper) {
+     139           16 :       return rows
+     140            9 :     }
+     141              : 
+     142           52 :     if (this.isRqbV2Query) {
+     143           29 :       return this.useJitMappers
+     144           17 :         ? (this.jitMapper =
+     145           52 :             (this.jitMapper as RelationalRowsMapper<T["all"]>) ?? makeJitRqbMapper<T["all"]>(this.rqbConfig!))(
+     146            4 :             rows as Record<string, unknown>[],
+     147            4 :           )
+     148           34 :         : (this.customResultMapper as (rows: Record<string, unknown>[]) => unknown)(rows as Record<string, unknown>[])
+     149            9 :     }
+     150              : 
+     151           64 :     if (this.customResultMapper) {
+     152           41 :       return (this.customResultMapper as (rows: unknown[][]) => unknown)(rows as unknown[][]) as T["all"]
+     153            9 :     }
+     154              : 
+     155           56 :     return this.useJitMappers
+     156           17 :       ? (this.jitMapper =
+     157           18 :           (this.jitMapper as RowsMapper<T["all"]>) ??
+     158           69 :           makeJitQueryMapper<T["all"]>(this.fields!, this.joinsNotNullableMap))(rows as unknown[][])
+     159          155 :       : (rows as unknown[][]).map((row) => mapResultRow(this.fields!, row, this.joinsNotNullableMap))
+     160              :   }
+     161              : 
+     162           62 :   mapGetResult(rows: unknown, isFromBatch?: boolean): unknown {
+     163           40 :     if (isFromBatch) {
+     164           43 :       rows = Array.isArray(rows) ? rows : []
+     165            9 :     }
+     166              : 
+     167           98 :     if (!this.fields && !this.customResultMapper) {
+     168           48 :       return Array.isArray(rows) ? rows[0] : rows
+     169            9 :     }
+     170              : 
+     171           97 :     const row = Array.isArray(rows) ? rows[0] : rows
+     172           50 :     if (!row) return undefined
+     173              : 
+     174           52 :     if (this.isRqbV2Query) {
+     175           29 :       return this.useJitMappers
+     176           17 :         ? (this.jitMapper =
+     177           61 :             (this.jitMapper as RelationalRowsMapper<T["get"][]>) ?? makeJitRqbMapper<T["get"][]>(this.rqbConfig!))([
+     178            9 :             row as Record<string, unknown>,
+     179            5 :           ])
+     180           35 :         : (this.customResultMapper as (rows: Record<string, unknown>[]) => unknown)([row as Record<string, unknown>])
+     181            9 :     }
+     182              : 
+     183           64 :     if (this.customResultMapper) {
+     184           42 :       return (this.customResultMapper as (rows: unknown[][]) => unknown)([row as unknown[]]) as T["get"]
+     185            9 :     }
+     186              : 
+     187           56 :     return this.useJitMappers
+     188           17 :       ? (this.jitMapper =
+     189           18 :           (this.jitMapper as RowsMapper<T["get"][]>) ??
+     190           73 :           makeJitQueryMapper<T["get"][]>(this.fields!, this.joinsNotNullableMap))([row as unknown[]])[0]
+     191          119 :       : mapResultRow(this.fields!, row as unknown[], this.joinsNotNullableMap)
+     192              :   }
+     193              : 
+     194            0 :   private allRqbV2(placeholderValues?: Record<string, unknown>) {
+     195            0 :     return this.executeWithCache<unknown[], T["all"]>(
+     196            0 :       placeholderValues,
+     197            0 :       "all",
+     198            0 :       (rows) => this.mapAllResult(rows) as T["all"],
+     199            8 :     )
+     200              :   }
+     201              : 
+     202            0 :   private getRqbV2(placeholderValues?: Record<string, unknown>) {
+     203            0 :     return this.executeWithCache<unknown, T["get"] | undefined>(placeholderValues, "get", (row) =>
+     204            0 :       row === undefined ? undefined : (this.mapGetResult(row) as T["get"]),
+     205            8 :     )
+     206              :   }
+     207              : 
+     208           18 :   private executeWithCache<A, B = A>(
+     209           38 :     placeholderValues: Record<string, unknown> | undefined,
+     210           30 :     executeMethod: SQLiteEffectExecuteMethod,
+     211           22 :     mapResult?: (result: A) => B,
+     212           10 :   ) {
+     213           96 :     return Effect.gen({ self: this }, function* () {
+     214          164 :       const params = fillPlaceholders(this.query.params, placeholderValues ?? {})
+     215              : 
+     216          116 :       yield* this.logger.logQuery(this.query.sql, params)
+     217              : 
+     218           68 :       return yield* this.queryWithCache(
+     219           32 :         this.query.sql,
+     220           16 :         params,
+     221          118 :         Effect.suspend(() => this.executor(params, executeMethod) as Effect.Effect<A, unknown, unknown>),
+     222           18 :         mapResult,
+     223            9 :       )
+     224            9 :     })
+     225              :   }
+     226              : 
+     227           65 :   private mapCachedResult<A, B>(result: A, mapResult: ((result: A) => B) | undefined) {
+     228          108 :     if (!mapResult) return Effect.succeed(result as unknown as B)
+     229           50 :     return Effect.try({
+     230           68 :       try: () => mapResult(result),
+     231           37 :       catch: (cause) => cause,
+     232           11 :     })
+     233              :   }
+     234              : 
+     235           16 :   private queryWithCache<A, E, R, B = A>(
+     236           26 :     queryString: string,
+     237           16 :     params: unknown[],
+     238           14 :     query: Effect.Effect<A, E, R>,
+     239           22 :     mapResult?: (result: A) => B,
+     240           10 :   ) {
+     241           96 :     return Effect.gen({ self: this }, function* () {
+     242          227 :       if (this.queryMetadata?.type === "select" && this.cacheConfig?.enabled && (yield* this.isInTransaction)) {
+     243          125 :         return yield* this.mapCachedResult(yield* query, mapResult)
+     244           13 :       }
+     245              : 
+     246          107 :       const cacheStrat: Awaited<ReturnType<typeof strategyFor>> = !is(this.cache.cache, NoopCache)
+     247          107 :         ? yield* Effect.tryPromise(() => strategyFor(queryString, params, this.queryMetadata, this.cacheConfig))
+     248           45 :         : { type: "skip" as const }
+     249              : 
+     250           81 :       if (cacheStrat.type === "skip") {
+     251          125 :         return yield* this.mapCachedResult(yield* query, mapResult)
+     252            7 :       }
+     253              : 
+     254           47 :       if (cacheStrat.type === "invalidate") {
+     255           36 :         const result = yield* query
+     256           66 :         yield* this.cache.onMutate({ tables: cacheStrat.tables })
+     257           60 :         return yield* this.mapCachedResult(result, mapResult)
+     258            7 :       }
+     259              : 
+     260           40 :       if (cacheStrat.type === "try") {
+     261           44 :         if (yield* this.isInTransaction) {
+     262           68 :           return yield* this.mapCachedResult(yield* query, mapResult)
+     263            9 :         }
+     264              : 
+     265           74 :         const { tables, key, isTag, autoInvalidate, config } = cacheStrat
+     266           84 :         const fromCache: any[] | undefined = yield* this.cache.get(key, tables, isTag, autoInvalidate)
+     267              : 
+     268           49 :         if (typeof fromCache !== "undefined") {
+     269           65 :           return yield* this.mapCachedResult(fromCache as unknown as A, mapResult)
+     270            9 :         }
+     271              : 
+     272           36 :         const result = yield* query
+     273              : 
+     274           88 :         yield* this.cache.put(key, result, autoInvalidate ? tables : [], isTag, config)
+     275              : 
+     276           60 :         return yield* this.mapCachedResult(result, mapResult)
+     277            7 :       }
+     278              : 
+     279           36 :       assertUnreachable(cacheStrat)
+     280           14 :     }).pipe(
+     281           53 :       Effect.catch((e) => {
+     282          208 :         return Effect.fail(new EffectDrizzleQueryError({ query: queryString, params, cause: Cause.fail(e) }))
+     283            2 :       }),
+     284            9 :     )
+     285              :   }
+     286              : 
+     287            0 :   getQuery(): Query {
+     288           24 :     return this.query
+     289              :   }
+     290              : 
+     291            0 :   mapResult(response: unknown, isFromBatch?: boolean) {
+     292            0 :     switch (this.effectExecuteMethod) {
+     293            0 :       case "run": {
+     294            0 :         return this.mapRunResult(response, isFromBatch)
+     295            0 :       }
+     296            0 :       case "all": {
+     297            0 :         return this.mapAllResult(response, isFromBatch)
+     298            0 :       }
+     299            0 :       case "get": {
+     300            0 :         return this.mapGetResult(response, isFromBatch)
+     301           11 :       }
+     302              :     }
+     303              :   }
+     304            2 : }
+     305              : 
+     306           64 : export abstract class SQLiteEffectSession<
+     307              :   TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+     308              :   TRunResult = unknown,
+     309              :   TRelations extends AnyRelations = EmptyRelations,
+     310            6 : > {
+     311           75 :   static readonly [entityKind]: string = "SQLiteEffectSession"
+     312              : 
+     313          131 :   constructor(readonly dialect: SQLiteAsyncDialect) {}
+     314              : 
+     315              :   abstract prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
+     316              :     query: Query,
+     317              :     fields: SelectedFieldsOrdered | undefined,
+     318              :     executeMethod: SQLiteExecuteMethod,
+     319              :     customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown,
+     320              :     queryMetadata?: {
+     321              :       type: "select" | "update" | "delete" | "insert"
+     322              :       tables: string[]
+     323              :     },
+     324              :     cacheConfig?: WithCacheConfig,
+     325              :   ): SQLiteEffectPreparedQuery<T, TEffectHKT>
+     326              : 
+     327           21 :   prepareOneTimeQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
+     328           14 :     query: Query,
+     329           16 :     fields: SelectedFieldsOrdered | undefined,
+     330           30 :     executeMethod: SQLiteExecuteMethod,
+     331           40 :     customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown,
+     332           30 :     queryMetadata?: {
+     333              :       type: "select" | "update" | "delete" | "insert"
+     334              :       tables: string[]
+     335              :     },
+     336           26 :     cacheConfig?: WithCacheConfig,
+     337           10 :   ): SQLiteEffectPreparedQuery<T, TEffectHKT> {
+     338          211 :     return this.prepareQuery(query, fields, executeMethod, customResultMapper, queryMetadata, cacheConfig)
+     339              :   }
+     340              : 
+     341              :   abstract prepareRelationalQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
+     342              :     query: Query,
+     343              :     fields: SelectedFieldsOrdered | undefined,
+     344              :     executeMethod: SQLiteExecuteMethod,
+     345              :     customResultMapper: (rows: Record<string, unknown>[], mapColumnValue?: (value: unknown) => unknown) => unknown,
+     346              :     config: RelationalQueryMapperConfig,
+     347              :   ): SQLiteEffectPreparedQuery<T, TEffectHKT, true>
+     348              : 
+     349            0 :   prepareOneTimeRelationalQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
+     350            0 :     query: Query,
+     351            0 :     fields: SelectedFieldsOrdered | undefined,
+     352            0 :     executeMethod: SQLiteExecuteMethod,
+     353            0 :     customResultMapper: (rows: Record<string, unknown>[], mapColumnValue?: (value: unknown) => unknown) => unknown,
+     354            0 :     config: RelationalQueryMapperConfig,
+     355            0 :   ): SQLiteEffectPreparedQuery<T, TEffectHKT, true> {
+     356           99 :     return this.prepareRelationalQuery(query, fields, executeMethod, customResultMapper, config)
+     357              :   }
+     358              : 
+     359              :   run(query: SQL): QueryEffectKind<TEffectHKT, TRunResult>
+     360           29 :   run(query: SQL): any {
+     361           50 :     return this.prepareQuery<PreparedQueryConfig & { run: TRunResult; execute: TRunResult }>(
+     362           64 :       this.dialect.sqlToQuery(query),
+     363           22 :       undefined,
+     364           10 :       "run",
+     365           21 :     ).run()
+     366              :   }
+     367              : 
+     368              :   all<T = unknown>(query: SQL): QueryEffectKind<TEffectHKT, T[]>
+     369           29 :   all<T = unknown>(query: SQL): any {
+     370           50 :     return this.prepareQuery<PreparedQueryConfig & { all: T[]; execute: T[] }>(
+     371           64 :       this.dialect.sqlToQuery(query),
+     372           22 :       undefined,
+     373           10 :       "all",
+     374           21 :     ).all()
+     375              :   }
+     376              : 
+     377              :   get<T = unknown>(query: SQL): QueryEffectKind<TEffectHKT, T | undefined>
+     378           29 :   get<T = unknown>(query: SQL): any {
+     379           50 :     return this.prepareQuery<PreparedQueryConfig & { get: T | undefined; execute: T | undefined }>(
+     380           64 :       this.dialect.sqlToQuery(query),
+     381           22 :       undefined,
+     382           10 :       "get",
+     383           21 :     ).get()
+     384              :   }
+     385              : 
+     386              :   values<T extends unknown[] = unknown[]>(query: SQL): QueryEffectKind<TEffectHKT, T[]>
+     387            0 :   values<T extends unknown[] = unknown[]>(query: SQL): any {
+     388            0 :     return this.prepareQuery<PreparedQueryConfig & { values: T[]; execute: T[] }>(
+     389            0 :       this.dialect.sqlToQuery(query),
+     390            0 :       undefined,
+     391            0 :       "all",
+     392           17 :     ).values()
+     393              :   }
+     394              : 
+     395              :   count(query: SQL): QueryEffectKind<TEffectHKT, number>
+     396            0 :   count(query: SQL): any {
+     397           81 :     return this.values<[number]>(query).pipe(Effect.map((result) => result[0]?.[0] ?? 0))
+     398              :   }
+     399              : 
+     400              :   abstract transaction<A, E, R>(
+     401              :     transaction: (tx: SQLiteEffectTransaction<TEffectHKT, TRunResult, TRelations>) => Effect.Effect<A, E, R>,
+     402              :     config?: SQLiteTransactionConfig,
+     403              :   ): Effect.Effect<A, E | SqlError, R>
+     404            2 : }
+     405              : 
+     406           88 : export abstract class SQLiteEffectTransaction<
+     407              :   TEffectHKT extends QueryEffectHKTBase,
+     408              :   TRunResult,
+     409              :   TRelations extends AnyRelations = EmptyRelations,
+     410           48 : > extends SQLiteEffectDatabase<TEffectHKT, TRunResult, TRelations> {
+     411           83 :   static override readonly [entityKind]: string = "SQLiteEffectTransaction"
+     412              : 
+     413           13 :   constructor(
+     414           18 :     dialect: SQLiteAsyncDialect,
+     415           18 :     session: SQLiteEffectSession<TEffectHKT, TRunResult, TRelations>,
+     416          124 :     protected relations: TRelations,
+     417           10 :   ) {
+     418           78 :     super(dialect, session, relations)
+     419              :   }
+     420              : 
+     421            0 :   rollback() {
+     422           47 :     return new EffectTransactionRollbackError()
+     423              :   }
+     424            2 : }
+     425              : 
+     426            0 : export const migrate = Effect.fn("migrate")(function* <TEffectHKT extends QueryEffectHKTBase>(
+     427            0 :   migrations: MigrationMeta[],
+     428            0 :   session: SQLiteEffectSession<TEffectHKT>,
+     429            0 :   config: string | MigrationConfigWithInit,
+     430            0 : ) {
+     431            0 :   const migrationsTable =
+     432            0 :     typeof config === "string" ? "__drizzle_migrations" : (config.migrationsTable ?? "__drizzle_migrations")
+     433            0 : 
+     434            0 :   const { newDb } = yield* upgradeIfNeeded(migrationsTable, session, migrations)
+     435            0 : 
+     436            0 :   if (newDb) {
+     437            0 :     yield* session.run(sql`
+     438            0 :                 CREATE TABLE IF NOT EXISTS ${sql.identifier(migrationsTable)} (
+     439            0 :                         id INTEGER PRIMARY KEY,
+     440            0 :                         hash text NOT NULL,
+     441            0 :                         created_at numeric,
+     442            0 :                         name text,
+     443            0 :                         applied_at TEXT
+     444            0 :                 )
+     445            0 :         `)
+     446            0 :   }
+     447            0 : 
+     448            0 :   const dbMigrations = yield* session.all<{ id: number; hash: string; created_at: string; name: string | null }>(
+     449            0 :     sql`SELECT id, hash, created_at, name FROM ${sql.identifier(migrationsTable)}`,
+     450            0 :   )
+     451            0 : 
+     452            0 :   if (typeof config === "object" && config.init) {
+     453            0 :     if (dbMigrations.length) {
+     454            0 :       return yield* new MigratorInitError({ exitCode: "databaseMigrations" })
+     455            0 :     }
+     456            0 : 
+     457            0 :     if (migrations.length > 1) {
+     458            0 :       return yield* new MigratorInitError({ exitCode: "localMigrations" })
+     459            0 :     }
+     460            0 : 
+     461            0 :     const [migration] = migrations
+     462            0 :     if (!migration) return
+     463            0 : 
+     464            0 :     yield* session.run(
+     465            0 :       sql`insert into ${sql.identifier(
+     466            0 :         migrationsTable,
+     467            0 :       )} ("hash", "created_at", "name", "applied_at") values(${migration.hash}, ${migration.folderMillis}, ${migration.name}, ${new Date().toISOString()})`,
+     468            0 :     )
+     469            0 : 
+     470            0 :     return
+     471            0 :   }
+     472            0 : 
+     473            0 :   const migrationsToRun = getMigrationsToRun({ localMigrations: migrations, dbMigrations })
+     474            0 :   if (migrationsToRun.length === 0) return
+     475            0 : 
+     476            0 :   yield* session.transaction((tx) =>
+     477            0 :     Effect.gen(function* () {
+     478            0 :       for (const migration of migrationsToRun) {
+     479            0 :         for (const stmt of migration.sql) {
+     480            0 :           yield* tx.run(sql.raw(stmt))
+     481            0 :         }
+     482            0 :         yield* tx.run(
+     483            0 :           sql`insert into ${sql.identifier(
+     484            0 :             migrationsTable,
+     485            0 :           )} ("hash", "created_at", "name", "applied_at") values(${migration.hash}, ${migration.folderMillis}, ${migration.name}, ${new Date().toISOString()})`,
+     486            0 :         )
+     487            0 :       }
+     488            0 :     }),
+     489            3 :   )
+     490            5 : })
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/update.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/update.ts.gcov.html new file mode 100644 index 00000000..a405aca5 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/sqlite-core/effect/update.ts.gcov.html @@ -0,0 +1,478 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/sqlite-core/effect/update.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/sqlite-core/effect - update.tsCoverageTotalHit
Test:opencode-lcov.infoLines:54.4 %13674
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2              : import type * as Effect from "effect/Effect"
+       3              : import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+       4              : import { entityKind, is } from "drizzle-orm/entity"
+       5              : import type { SelectResultFields } from "drizzle-orm/query-builders/select.types"
+       6              : import type { RunnableQuery } from "drizzle-orm/runnable-query"
+       7              : import { SelectionProxyHandler } from "drizzle-orm/selection-proxy"
+       8              : import type { Placeholder, Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql"
+       9              : import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
+      10              : import type { SelectedFields, SQLiteSelectJoinConfig } from "drizzle-orm/sqlite-core/query-builders/select.types"
+      11              : import type { SQLiteUpdateConfig, SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update"
+      12              : import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session"
+      13              : import { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+      14              : import { extractUsedTable } from "drizzle-orm/sqlite-core/utils"
+      15              : import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base"
+      16              : import { Subquery } from "drizzle-orm/subquery"
+      17              : import { type DrizzleTypeError, type UpdateSet, type ValueOrArray } from "drizzle-orm/utils"
+      18              : import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common"
+      19              : import {
+      20              :   getTableColumnsRuntime,
+      21              :   getTableLikeName,
+      22              :   getViewSelectedFieldsRuntime,
+      23              :   mapUpdateSet,
+      24              :   orderSelectedFields,
+      25              : } from "../../internal/drizzle-utils"
+      26              : import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
+      27              : 
+      28              : export type SQLiteEffectUpdateWithout<
+      29              :   T extends AnySQLiteEffectUpdate,
+      30              :   TDynamic extends boolean,
+      31              :   K extends keyof T & string,
+      32              : > = TDynamic extends true
+      33              :   ? T
+      34              :   : Omit<
+      35              :       SQLiteEffectUpdateBase<
+      36              :         T["_"]["table"],
+      37              :         T["_"]["runResult"],
+      38              :         T["_"]["from"],
+      39              :         T["_"]["returning"],
+      40              :         TDynamic,
+      41              :         T["_"]["excludedMethods"] | K,
+      42              :         T["_"]["effectHKT"]
+      43              :       >,
+      44              :       T["_"]["excludedMethods"] | K
+      45              :     >
+      46              : 
+      47              : export type SQLiteEffectUpdateWithJoins<
+      48              :   T extends AnySQLiteEffectUpdate,
+      49              :   TDynamic extends boolean,
+      50              :   TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL,
+      51              : > = TDynamic extends true
+      52              :   ? T
+      53              :   : Omit<
+      54              :       SQLiteEffectUpdateBase<
+      55              :         T["_"]["table"],
+      56              :         T["_"]["runResult"],
+      57              :         TFrom,
+      58              :         T["_"]["returning"],
+      59              :         TDynamic,
+      60              :         Exclude<T["_"]["excludedMethods"] | "from", "leftJoin" | "rightJoin" | "innerJoin" | "fullJoin">,
+      61              :         T["_"]["effectHKT"]
+      62              :       >,
+      63              :       Exclude<T["_"]["excludedMethods"] | "from", "leftJoin" | "rightJoin" | "innerJoin" | "fullJoin">
+      64              :     >
+      65              : 
+      66              : export type SQLiteEffectUpdateReturningAll<
+      67              :   T extends AnySQLiteEffectUpdate,
+      68              :   TDynamic extends boolean,
+      69              : > = SQLiteEffectUpdateWithout<
+      70              :   SQLiteEffectUpdateBase<
+      71              :     T["_"]["table"],
+      72              :     T["_"]["runResult"],
+      73              :     T["_"]["from"],
+      74              :     T["_"]["table"]["$inferSelect"],
+      75              :     TDynamic,
+      76              :     T["_"]["excludedMethods"],
+      77              :     T["_"]["effectHKT"]
+      78              :   >,
+      79              :   TDynamic,
+      80              :   "returning"
+      81              : >
+      82              : 
+      83              : export type SQLiteEffectUpdateReturning<
+      84              :   T extends AnySQLiteEffectUpdate,
+      85              :   TDynamic extends boolean,
+      86              :   TSelectedFields extends SelectedFields,
+      87              : > = SQLiteEffectUpdateWithout<
+      88              :   SQLiteEffectUpdateBase<
+      89              :     T["_"]["table"],
+      90              :     T["_"]["runResult"],
+      91              :     T["_"]["from"],
+      92              :     SelectResultFields<TSelectedFields>,
+      93              :     TDynamic,
+      94              :     T["_"]["excludedMethods"],
+      95              :     T["_"]["effectHKT"]
+      96              :   >,
+      97              :   TDynamic,
+      98              :   "returning"
+      99              : >
+     100              : 
+     101              : export type SQLiteEffectUpdateExecute<T extends AnySQLiteEffectUpdate> = T["_"]["returning"] extends undefined
+     102              :   ? T["_"]["runResult"]
+     103              :   : T["_"]["returning"][]
+     104              : 
+     105              : export type SQLiteEffectUpdatePrepare<
+     106              :   T extends AnySQLiteEffectUpdate,
+     107              :   TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"],
+     108              : > = SQLiteEffectPreparedQuery<
+     109              :   PreparedQueryConfig & {
+     110              :     run: T["_"]["runResult"]
+     111              :     all: T["_"]["returning"] extends undefined
+     112              :       ? DrizzleTypeError<".all() cannot be used without .returning()">
+     113              :       : T["_"]["returning"][]
+     114              :     get: T["_"]["returning"] extends undefined
+     115              :       ? DrizzleTypeError<".get() cannot be used without .returning()">
+     116              :       : T["_"]["returning"]
+     117              :     values: T["_"]["returning"] extends undefined
+     118              :       ? DrizzleTypeError<".values() cannot be used without .returning()">
+     119              :       : any[][]
+     120              :     execute: SQLiteEffectUpdateExecute<T>
+     121              :   },
+     122              :   TEffectHKT
+     123              : >
+     124              : 
+     125              : export type SQLiteEffectUpdateDynamic<T extends AnySQLiteEffectUpdate> = SQLiteEffectUpdate<
+     126              :   T["_"]["table"],
+     127              :   T["_"]["runResult"],
+     128              :   T["_"]["from"],
+     129              :   T["_"]["returning"],
+     130              :   T["_"]["effectHKT"]
+     131              : >
+     132              : 
+     133              : export type SQLiteEffectUpdate<
+     134              :   TTable extends SQLiteTable = SQLiteTable,
+     135              :   TRunResult = unknown,
+     136              :   TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined,
+     137              :   TReturning extends Record<string, unknown> | undefined = Record<string, unknown> | undefined,
+     138              :   TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+     139              : > = SQLiteEffectUpdateBase<TTable, TRunResult, TFrom, TReturning, true, never, TEffectHKT>
+     140              : 
+     141              : export type AnySQLiteEffectUpdate = SQLiteEffectUpdateBase<any, any, any, any, any, any, any>
+     142              : 
+     143              : export type SQLiteEffectUpdateJoinFn<T extends AnySQLiteEffectUpdate> = <
+     144              :   TJoinedTable extends SQLiteTable | Subquery | SQLiteViewBase | SQL,
+     145              : >(
+     146              :   table: TJoinedTable,
+     147              :   on:
+     148              :     | ((
+     149              :         updateTable: T["_"]["table"]["_"]["columns"],
+     150              :         from: T["_"]["from"] extends SQLiteTable
+     151              :           ? T["_"]["from"]["_"]["columns"]
+     152              :           : T["_"]["from"] extends Subquery | SQLiteViewBase
+     153              :             ? T["_"]["from"]["_"]["selectedFields"]
+     154              :             : never,
+     155              :       ) => SQL | undefined)
+     156              :     | SQL
+     157              :     | undefined,
+     158              : ) => T
+     159              : 
+     160              : export class SQLiteEffectUpdateBuilder<
+     161              :   TTable extends SQLiteTable,
+     162              :   TRunResult,
+     163              :   TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+     164              : > {
+     165            1 :   static readonly [entityKind]: string = "SQLiteEffectUpdateBuilder"
+     166              : 
+     167              :   declare readonly _: {
+     168              :     readonly table: TTable
+     169              :   }
+     170              : 
+     171           13 :   constructor(
+     172           60 :     protected table: TTable,
+     173           72 :     protected session: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
+     174           72 :     protected dialect: SQLiteDialect,
+     175           78 :     private withList?: Subquery[],
+     176           10 :   ) {}
+     177              : 
+     178            5 :   set(
+     179           16 :     values: SQLiteUpdateSetSource<TTable>,
+     180              :   ): SQLiteEffectUpdateWithout<
+     181              :     SQLiteEffectUpdateBase<TTable, TRunResult, undefined, undefined, false, never, TEffectHKT>,
+     182              :     false,
+     183              :     "leftJoin" | "rightJoin" | "innerJoin" | "fullJoin"
+     184           10 :   > {
+     185           68 :     return new SQLiteEffectUpdateBase(
+     186           24 :       this.table,
+     187           68 :       mapUpdateSet(this.table, values),
+     188           28 :       this.session,
+     189           28 :       this.dialect,
+     190           26 :       this.withList,
+     191            8 :     ) as any
+     192              :   }
+     193            2 : }
+     194              : 
+     195              : export interface SQLiteEffectUpdateBase<
+     196              :   TTable extends SQLiteTable = SQLiteTable,
+     197              :   TRunResult = unknown,
+     198              :   TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined,
+     199              :   TReturning = undefined,
+     200              :   TDynamic extends boolean = false,
+     201              :   _TExcludedMethods extends string = never,
+     202              :   TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+     203              : > extends SQLWrapper,
+     204              :     RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">,
+     205              :     Effect.Effect<
+     206              :       TReturning extends undefined ? TRunResult : TReturning[],
+     207              :       TEffectHKT["error"],
+     208              :       TEffectHKT["context"]
+     209              :     > {
+     210              :   readonly _: {
+     211              :     readonly dialect: "sqlite"
+     212              :     readonly table: TTable
+     213              :     readonly resultType: "async"
+     214              :     readonly runResult: TRunResult
+     215              :     readonly from: TFrom
+     216              :     readonly returning: TReturning
+     217              :     readonly dynamic: TDynamic
+     218              :     readonly excludedMethods: _TExcludedMethods
+     219              :     readonly result: TReturning extends undefined ? TRunResult : TReturning[]
+     220              :     readonly effectHKT: TEffectHKT
+     221              :   }
+     222              : }
+     223              : 
+     224           70 : export class SQLiteEffectUpdateBase<
+     225              :     TTable extends SQLiteTable = SQLiteTable,
+     226              :     TRunResult = unknown,
+     227              :     TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined,
+     228              :     TReturning = undefined,
+     229              :     TDynamic extends boolean = false,
+     230              :     _TExcludedMethods extends string = never,
+     231              :     TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+     232              :   >
+     233              :   implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper
+     234            6 : {
+     235           74 :   static readonly [entityKind]: string = "SQLiteEffectUpdate"
+     236              : 
+     237              :   /** @internal */
+     238           17 :   config: SQLiteUpdateConfig
+     239              : 
+     240           13 :   constructor(
+     241           14 :     table: TTable,
+     242           10 :     set: UpdateSet,
+     243          140 :     private effectSession: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
+     244          156 :     private effectDialect: SQLiteDialect,
+     245           20 :     withList?: Subquery[],
+     246           10 :   ) {
+     247          108 :     this.config = { set, table, withList, joins: [] }
+     248              :   }
+     249              : 
+     250            0 :   from<TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL>(
+     251            0 :     source: TFrom,
+     252            0 :   ): SQLiteEffectUpdateWithJoins<this, TDynamic, TFrom> {
+     253            0 :     this.config.from = source
+     254           18 :     return this as any
+     255              :   }
+     256              : 
+     257           12 :   private createJoin<TJoinType extends SQLiteSelectJoinConfig["joinType"]>(
+     258           20 :     joinType: TJoinType,
+     259           10 :   ): SQLiteEffectUpdateJoinFn<this> {
+     260            0 :     return ((
+     261            0 :       table: SQLiteTable | Subquery | SQLiteViewBase | SQL,
+     262            0 :       on: ((updateTable: TTable, from: TFrom) => SQL | undefined) | SQL | undefined,
+     263            0 :     ) => {
+     264            0 :       const tableName = getTableLikeName(table)
+     265            0 : 
+     266            0 :       if (typeof tableName === "string" && this.config.joins.some((join) => join.alias === tableName)) {
+     267            0 :         throw new Error(`Alias "${tableName}" is already used in this query`)
+     268            0 :       }
+     269            0 : 
+     270            0 :       if (typeof on === "function") {
+     271            0 :         const from = this.config.from
+     272            0 :           ? is(table, SQLiteTable)
+     273            0 :             ? getTableColumnsRuntime(table)
+     274            0 :             : is(table, Subquery)
+     275            0 :               ? table._.selectedFields
+     276            0 :               : is(table, SQLiteViewBase)
+     277            0 :                 ? getViewSelectedFieldsRuntime(table).selectedFields
+     278            0 :                 : undefined
+     279            0 :           : undefined
+     280            0 :         on = on(
+     281            0 :           new Proxy(
+     282            0 :             this.config.table._.columns,
+     283            0 :             new SelectionProxyHandler({ sqlAliasedBehavior: "sql", sqlBehavior: "sql" }),
+     284            0 :           ) as any,
+     285            0 :           from &&
+     286            0 :             (new Proxy(from, new SelectionProxyHandler({ sqlAliasedBehavior: "sql", sqlBehavior: "sql" })) as any),
+     287            0 :         )
+     288            0 :       }
+     289            0 : 
+     290            0 :       this.config.joins.push({ on, table, joinType, alias: tableName })
+     291            0 : 
+     292           25 :       return this as any
+     293              :     }) as any
+     294              :   }
+     295              : 
+     296           74 :   leftJoin = this.createJoin("left")
+     297              : 
+     298           78 :   rightJoin = this.createJoin("right")
+     299              : 
+     300           78 :   innerJoin = this.createJoin("inner")
+     301              : 
+     302           73 :   fullJoin = this.createJoin("full")
+     303              : 
+     304           31 :   where(where: SQL | undefined): SQLiteEffectUpdateWithout<this, TDynamic, "where"> {
+     305           60 :     this.config.where = where
+     306           29 :     return this as any
+     307              :   }
+     308              : 
+     309              :   orderBy(
+     310              :     builder: (updateTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>,
+     311              :   ): SQLiteEffectUpdateWithout<this, TDynamic, "orderBy">
+     312              :   orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): SQLiteEffectUpdateWithout<this, TDynamic, "orderBy">
+     313            0 :   orderBy(
+     314            0 :     ...columns:
+     315            0 :       | [(updateTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>]
+     316            0 :       | (SQLiteColumn | SQL | SQL.Aliased)[]
+     317            0 :   ): SQLiteEffectUpdateWithout<this, TDynamic, "orderBy"> {
+     318            0 :     if (typeof columns[0] === "function") {
+     319            0 :       const orderBy = columns[0](
+     320            0 :         new Proxy(
+     321            0 :           getTableColumnsRuntime(this.config.table),
+     322            0 :           new SelectionProxyHandler({ sqlAliasedBehavior: "alias", sqlBehavior: "sql" }),
+     323            0 :         ) as any,
+     324            0 :       )
+     325            0 : 
+     326            0 :       this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy]
+     327            0 :       return this as any
+     328            0 :     }
+     329            0 : 
+     330            0 :     this.config.orderBy = columns as (SQLiteColumn | SQL | SQL.Aliased)[]
+     331           18 :     return this as any
+     332              :   }
+     333              : 
+     334            0 :   limit(limit: number | Placeholder): SQLiteEffectUpdateWithout<this, TDynamic, "limit"> {
+     335            0 :     this.config.limit = limit
+     336           18 :     return this as any
+     337              :   }
+     338              : 
+     339              :   returning(): SQLiteEffectUpdateReturningAll<this, TDynamic>
+     340              :   returning<TSelectedFields extends SelectedFields>(
+     341              :     fields: TSelectedFields,
+     342              :   ): SQLiteEffectUpdateReturning<this, TDynamic, TSelectedFields>
+     343           11 :   returning(
+     344          104 :     fields: SelectedFields = getTableColumnsRuntime(this.config.table),
+     345           10 :   ): SQLiteEffectUpdateWithout<AnySQLiteEffectUpdate, TDynamic, "returning"> {
+     346          112 :     this.config.returning = orderSelectedFields<SQLiteColumn>(fields)
+     347           29 :     return this as any
+     348              :   }
+     349              : 
+     350              :   /** @internal */
+     351           22 :   getSQL(): SQL {
+     352          117 :     return this.effectDialect.buildUpdateQuery(this.config)
+     353              :   }
+     354              : 
+     355            0 :   toSQL(): Query {
+     356           58 :     return this.effectDialect.sqlToQuery(this.getSQL())
+     357              :   }
+     358              : 
+     359              :   /** @internal */
+     360           66 :   _prepare(isOneTimeQuery = true): SQLiteEffectUpdatePrepare<this, TEffectHKT> {
+     361          147 :     return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"](
+     362           92 :       this.effectDialect.sqlToQuery(this.getSQL()),
+     363           46 :       this.config.returning,
+     364           75 :       this.config.returning ? "all" : "run",
+     365           22 :       undefined,
+     366           14 :       {
+     367           42 :         type: "update",
+     368           94 :         tables: extractUsedTable(this.config.table),
+     369            2 :       },
+     370            9 :     ) as SQLiteEffectUpdatePrepare<this, TEffectHKT>
+     371              :   }
+     372              : 
+     373            0 :   prepare(): SQLiteEffectUpdatePrepare<this, TEffectHKT> {
+     374           35 :     return this._prepare(false)
+     375              :   }
+     376              : 
+     377           67 :   run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => {
+     378          100 :     return this._prepare().run(placeholderValues)
+     379              :   }
+     380              : 
+     381            0 :   all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => {
+     382           55 :     return this._prepare().all(placeholderValues)
+     383              :   }
+     384              : 
+     385           67 :   get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => {
+     386          100 :     return this._prepare().get(placeholderValues)
+     387              :   }
+     388              : 
+     389            0 :   values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => {
+     390           58 :     return this._prepare().values(placeholderValues)
+     391              :   }
+     392              : 
+     393            0 :   execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => {
+     394           58 :     return this._prepare().execute(placeholderValues)
+     395              :   }
+     396              : 
+     397            0 :   $dynamic(): SQLiteEffectUpdateDynamic<this> {
+     398           17 :     return this as any
+     399              :   }
+     400            2 : }
+     401              : 
+     402           85 : applyEffectWrapper(SQLiteEffectUpdateBase)
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/up-migrations/effect-sqlite.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/up-migrations/effect-sqlite.ts.gcov.html new file mode 100644 index 00000000..84a363c0 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/up-migrations/effect-sqlite.ts.gcov.html @@ -0,0 +1,178 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/up-migrations/effect-sqlite.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/up-migrations - effect-sqlite.tsCoverageTotalHit
Test:opencode-lcov.infoLines:16.0 %7512
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2           40 : import * as Effect from "effect/Effect"
+       3              : import type { SqlError } from "effect/unstable/sql/SqlError"
+       4           68 : import { EffectDrizzleError } from "drizzle-orm/effect-core/errors"
+       5              : import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+       6              : import type { MigrationMeta } from "drizzle-orm/migrator"
+       7           42 : import { sql } from "drizzle-orm/sql/sql"
+       8              : import type { SQLiteEffectSession } from "../sqlite-core/effect/session"
+       9           99 : import {
+      10              :   buildSQLiteMigrationBackfillStatements,
+      11              :   prepareSQLiteMigrationBackfill,
+      12              :   type SQLiteMigrationTableRow,
+      13              : } from "./sqlite"
+      14           69 : import { GET_VERSION_FOR, MIGRATIONS_TABLE_VERSIONS, type UpgradeResult } from "./utils"
+      15              : 
+      16            0 : const migrationUpgradeError = (cause: unknown) =>
+      17            0 :   new EffectDrizzleError({
+      18            0 :     message:
+      19            0 :       typeof cause === "object" && cause !== null && "message" in cause && typeof cause.message === "string"
+      20            0 :         ? cause.message
+      21            0 :         : String(cause),
+      22              :     cause,
+      23            2 :   })
+      24              : 
+      25           30 : export const upgradeIfNeeded: <TEffectHKT extends QueryEffectHKTBase>(
+      26              :   migrationsTable: string,
+      27              :   session: SQLiteEffectSession<TEffectHKT>,
+      28              :   localMigrations: MigrationMeta[],
+      29              : ) => Effect.Effect<UpgradeResult, EffectDrizzleError | TEffectHKT["error"] | SqlError, TEffectHKT["context"]> =
+      30            0 :   Effect.fn("upgradeIfNeeded")(function* <TEffectHKT extends QueryEffectHKTBase>(
+      31            0 :     migrationsTable: string,
+      32            0 :     session: SQLiteEffectSession<TEffectHKT>,
+      33            0 :     localMigrations: MigrationMeta[],
+      34            0 :   ) {
+      35            0 :     const tableExists = yield* session.all(
+      36            0 :       sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`,
+      37            0 :     )
+      38            0 : 
+      39            0 :     if (tableExists.length === 0) {
+      40            0 :       return { newDb: true }
+      41            0 :     }
+      42            0 : 
+      43            0 :     const rows = yield* session.all<{ column_name: string }>(
+      44            0 :       sql`SELECT name as column_name FROM pragma_table_info(${migrationsTable})`,
+      45            0 :     )
+      46            0 : 
+      47            0 :     const version = GET_VERSION_FOR.sqlite(rows.map((r) => r.column_name))
+      48            0 : 
+      49            0 :     for (let v = version; v < MIGRATIONS_TABLE_VERSIONS.sqlite; v++) {
+      50            0 :       const upgradeFn = upgradeFunctions[v]
+      51            0 :       if (!upgradeFn) {
+      52            0 :         return yield* new EffectDrizzleError({
+      53            0 :           message: `No upgrade path from migration table version ${v} to ${v + 1}`,
+      54            0 :           cause: { version: v },
+      55            0 :         })
+      56            0 :       }
+      57            0 :       yield* upgradeFn(migrationsTable, session, localMigrations)
+      58            0 :     }
+      59            0 : 
+      60              :     return { newDb: false }
+      61            3 :   })
+      62              : 
+      63           24 : const upgradeFunctions: Record<
+      64              :   number,
+      65              :   <TEffectHKT extends QueryEffectHKTBase>(
+      66              :     migrationsTable: string,
+      67              :     session: SQLiteEffectSession<TEffectHKT>,
+      68              :     localMigrations: MigrationMeta[],
+      69              :   ) => Effect.Effect<void, EffectDrizzleError | TEffectHKT["error"] | SqlError, TEffectHKT["context"]>
+      70            3 : > = {
+      71           17 :   0: upgradeFromV0,
+      72            1 : }
+      73              : 
+      74            0 : function upgradeFromV0<TEffectHKT extends QueryEffectHKTBase>(
+      75            0 :   migrationsTable: string,
+      76            0 :   session: SQLiteEffectSession<TEffectHKT>,
+      77            0 :   localMigrations: MigrationMeta[],
+      78            0 : ): Effect.Effect<void, EffectDrizzleError | TEffectHKT["error"] | SqlError, TEffectHKT["context"]> {
+      79            0 :   return Effect.gen(function* () {
+      80            0 :     const table = sql`${sql.identifier(migrationsTable)}`
+      81            0 : 
+      82            0 :     const dbRows = yield* session.all<SQLiteMigrationTableRow>(
+      83            0 :       sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`,
+      84            0 :     )
+      85            0 :     const statements = yield* Effect.try({
+      86            0 :       try: () =>
+      87            0 :         buildSQLiteMigrationBackfillStatements(
+      88            0 :           migrationsTable,
+      89            0 :           prepareSQLiteMigrationBackfill(dbRows, localMigrations),
+      90            0 :         ),
+      91            0 :       catch: migrationUpgradeError,
+      92            0 :     })
+      93            0 : 
+      94            0 :     yield* session.transaction((tx) =>
+      95            0 :       Effect.gen(function* () {
+      96            0 :         for (const statement of statements) {
+      97            0 :           yield* tx.run(statement)
+      98            0 :         }
+      99            0 :       }),
+     100            0 :     )
+     101              :   })
+     102              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/up-migrations/index-sort-f.html b/packages/core/effect-drizzle-sqlite/src/up-migrations/index-sort-f.html new file mode 100644 index 00000000..0bc136fa --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/up-migrations/index-sort-f.html @@ -0,0 +1,116 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/up-migrations + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/up-migrationsCoverageTotalHit
Test:opencode-lcov.infoLines:16.0 %28245
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
effect-sqlite.ts +
16.0%16.0%
+
16.0 %7512
sqlite.ts +
8.6%8.6%
+
8.6 %17515
utils.ts +
56.2%56.2%
+
56.2 %3218
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/up-migrations/index-sort-l.html b/packages/core/effect-drizzle-sqlite/src/up-migrations/index-sort-l.html new file mode 100644 index 00000000..20262235 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/up-migrations/index-sort-l.html @@ -0,0 +1,116 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/up-migrations + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/up-migrationsCoverageTotalHit
Test:opencode-lcov.infoLines:16.0 %28245
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
sqlite.ts +
8.6%8.6%
+
8.6 %17515
effect-sqlite.ts +
16.0%16.0%
+
16.0 %7512
utils.ts +
56.2%56.2%
+
56.2 %3218
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/up-migrations/index.html b/packages/core/effect-drizzle-sqlite/src/up-migrations/index.html new file mode 100644 index 00000000..1a882756 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/up-migrations/index.html @@ -0,0 +1,116 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/up-migrations + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/up-migrationsCoverageTotalHit
Test:opencode-lcov.infoLines:16.0 %28245
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
effect-sqlite.ts +
16.0%16.0%
+
16.0 %7512
sqlite.ts +
8.6%8.6%
+
8.6 %17515
utils.ts +
56.2%56.2%
+
56.2 %3218
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/up-migrations/sqlite.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/up-migrations/sqlite.ts.gcov.html new file mode 100644 index 00000000..b8e3d410 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/up-migrations/sqlite.ts.gcov.html @@ -0,0 +1,329 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/up-migrations/sqlite.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/up-migrations - sqlite.tsCoverageTotalHit
Test:opencode-lcov.infoLines:8.6 %17515
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2              : import type { TablesRelationalConfig } from "drizzle-orm/_relations"
+       3              : import type { MigrationMeta } from "drizzle-orm/migrator"
+       4              : import type { AnyRelations } from "drizzle-orm/relations"
+       5           42 : import { type SQL, sql } from "drizzle-orm/sql/sql"
+       6              : import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core"
+       7              : import type { SQLiteSession } from "drizzle-orm/sqlite-core/session"
+       8           68 : import { GET_VERSION_FOR, MIGRATIONS_TABLE_VERSIONS, type UpgradeResult } from "./utils"
+       9              : 
+      10              : /** @internal */
+      11              : export type SQLiteMigrationTableRow = { id: number | null; hash: string; created_at: number }
+      12              : 
+      13              : type AsyncSQLiteDatabaseWithSession = BaseSQLiteDatabase<"async", unknown, Record<string, unknown>> & {
+      14              :   session: {
+      15              :     all<T>(query: SQL): Promise<T[]>
+      16              :   }
+      17              :   transaction<T>(transaction: (tx: { run(query: SQL): Promise<unknown> }) => Promise<T>): Promise<T>
+      18              : }
+      19              : 
+      20              : type SQLiteMigrationBackfillEntry = {
+      21              :   name: string
+      22              :   selector:
+      23              :     | { column: "id"; value: number }
+      24              :     | { column: "created_at"; value: number }
+      25              :     | { column: "hash"; value: string }
+      26              : }
+      27              : 
+      28            0 : function unmatchedMigrationError(unmatched: SQLiteMigrationTableRow[]) {
+      29            0 :   return new Error(
+      30            0 :     `While upgrading your database migrations table we found ${unmatched.length} (${unmatched
+      31            0 :       .map((it) => `[id: ${it.id}, created_at: ${it.created_at}]`)
+      32            0 :       .join(
+      33            0 :         ", ",
+      34            0 :       )}) migrations in the database that do not match any local migration. This means that some migrations were applied to the database but are missing from the local environment`,
+      35            1 :   )
+      36              : }
+      37              : 
+      38              : /** @internal */
+      39            0 : export function prepareSQLiteMigrationBackfill(
+      40            0 :   dbRows: SQLiteMigrationTableRow[],
+      41            0 :   localMigrations: MigrationMeta[],
+      42            0 : ): SQLiteMigrationBackfillEntry[] {
+      43            0 :   const sortedLocalMigrations = [...localMigrations].sort((a, b) =>
+      44            0 :     a.folderMillis !== b.folderMillis ? a.folderMillis - b.folderMillis : (a.name ?? "").localeCompare(b.name ?? ""),
+      45            0 :   )
+      46            0 :   const byMillis = new Map<number, MigrationMeta[]>()
+      47            0 :   const byHash = new Map<string, MigrationMeta>()
+      48            0 :   for (const migration of sortedLocalMigrations) {
+      49            0 :     if (!byMillis.has(migration.folderMillis)) {
+      50            0 :       byMillis.set(migration.folderMillis, [])
+      51            0 :     }
+      52            0 :     byMillis.get(migration.folderMillis)!.push(migration)
+      53            0 :     byHash.set(migration.hash, migration)
+      54            0 :   }
+      55            0 : 
+      56            0 :   const toApply: SQLiteMigrationBackfillEntry[] = []
+      57            0 :   const unmatched: SQLiteMigrationTableRow[] = []
+      58            0 : 
+      59            0 :   for (const dbRow of dbRows) {
+      60            0 :     const stringified = String(dbRow.created_at)
+      61            0 :     const millis = Number(stringified.substring(0, stringified.length - 3) + "000")
+      62            0 :     const candidates = byMillis.get(millis)
+      63            0 : 
+      64            0 :     const matchedByMillis = candidates?.length === 1 ? candidates[0] : undefined
+      65            0 :     const matchedByCandidateHash =
+      66            0 :       candidates && candidates.length > 1
+      67            0 :         ? candidates.find((candidate) => candidate.hash && dbRow.hash && candidate.hash === dbRow.hash)
+      68            0 :         : undefined
+      69            0 :     const matchedByHash = matchedByMillis || matchedByCandidateHash ? undefined : byHash.get(dbRow.hash)
+      70            0 :     const matched = matchedByMillis ?? matchedByCandidateHash ?? matchedByHash
+      71            0 : 
+      72            0 :     if (matched) {
+      73            0 :       toApply.push({
+      74            0 :         name: matched.name,
+      75            0 :         selector:
+      76            0 :           dbRow.id !== null
+      77            0 :             ? { column: "id", value: dbRow.id }
+      78            0 :             : matchedByMillis
+      79            0 :               ? { column: "created_at", value: dbRow.created_at }
+      80            0 :               : { column: "hash", value: dbRow.hash },
+      81            0 :       })
+      82            0 :       continue
+      83            0 :     }
+      84            0 : 
+      85            0 :     unmatched.push(dbRow)
+      86            0 :   }
+      87            0 : 
+      88            0 :   if (unmatched.length > 0) {
+      89            0 :     throw unmatchedMigrationError(unmatched)
+      90            0 :   }
+      91            0 : 
+      92            1 :   return toApply
+      93              : }
+      94              : 
+      95              : /** @internal */
+      96            0 : export function buildSQLiteMigrationBackfillStatements(
+      97            0 :   migrationsTable: string,
+      98            0 :   backfillEntries: SQLiteMigrationBackfillEntry[],
+      99            0 : ) {
+     100            0 :   const table = sql`${sql.identifier(migrationsTable)}`
+     101            0 :   const statements: SQL[] = [
+     102            0 :     sql`ALTER TABLE ${table} ADD COLUMN ${sql.identifier("name")} text`,
+     103            0 :     sql`ALTER TABLE ${table} ADD COLUMN ${sql.identifier("applied_at")} TEXT`,
+     104            0 :   ]
+     105            0 : 
+     106            0 :   for (const backfillEntry of backfillEntries) {
+     107            0 :     const updateQuery = sql`UPDATE ${table} SET ${sql.identifier("name")} = ${backfillEntry.name}, ${sql.identifier(
+     108            0 :       "applied_at",
+     109            0 :     )} = NULL WHERE`
+     110            0 : 
+     111            0 :     updateQuery.append(sql` ${sql.identifier(backfillEntry.selector.column)} = ${backfillEntry.selector.value}`)
+     112            0 : 
+     113            0 :     statements.push(updateQuery)
+     114            0 :   }
+     115            0 : 
+     116            1 :   return statements
+     117              : }
+     118              : 
+     119              : /**
+     120              :  * Detects the current version of the migrations table schema and upgrades it if needed.
+     121              :  *
+     122              :  * Version 0: Original schema (id, hash, created_at)
+     123              :  * Version 1: Extended schema (id, hash, created_at, name, applied_at)
+     124              :  */
+     125            0 : export function upgradeSyncIfNeeded(
+     126            0 :   migrationsTable: string,
+     127            0 :   session: SQLiteSession<"sync", unknown, Record<string, unknown>, AnyRelations, TablesRelationalConfig>,
+     128            0 :   localMigrations: MigrationMeta[],
+     129            0 : ): UpgradeResult {
+     130            0 :   const tableExists = session.all(sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`)
+     131            0 : 
+     132            0 :   if (tableExists.length === 0) {
+     133            0 :     return { newDb: true }
+     134            0 :   }
+     135            0 : 
+     136            0 :   // Table exists, check table shape
+     137            0 :   const rows = session.all<{ column_name: string }>(
+     138            0 :     sql`SELECT name as column_name FROM pragma_table_info(${migrationsTable})`,
+     139            0 :   )
+     140            0 : 
+     141            0 :   const version = GET_VERSION_FOR.sqlite(rows.map((r) => r.column_name))
+     142            0 : 
+     143            0 :   for (let v = version; v < MIGRATIONS_TABLE_VERSIONS.sqlite; v++) {
+     144            0 :     const upgradeFn = upgradeSyncFunctions[v]
+     145            0 :     if (!upgradeFn) {
+     146            0 :       throw new Error(`No upgrade path from migration table version ${v} to ${v + 1}`)
+     147            0 :     }
+     148            0 :     upgradeFn(migrationsTable, session, localMigrations)
+     149            0 :   }
+     150            0 : 
+     151            1 :   return { newDb: false }
+     152              : }
+     153              : 
+     154           28 : const upgradeSyncFunctions: Record<
+     155              :   number,
+     156              :   (
+     157              :     migrationsTable: string,
+     158              :     session: SQLiteSession<"sync", unknown, Record<string, unknown>, AnyRelations, TablesRelationalConfig>,
+     159              :     localMigrations: MigrationMeta[],
+     160              :   ) => void
+     161            3 : > = {
+     162              :   /**
+     163              :    * Upgrade from version 0 to version 1:
+     164              :    * 1. Read all existing DB migrations
+     165              :    * 2. Sort localMigrations ASC by millis and if the same - sort by name
+     166              :    * 3. Match each DB row to a local migration
+     167              :    * If multiple migrations share the same second, use hash matching as a tiebreaker
+     168              :    * Not implemented for now -> If hash matching fails, fall back to serial id ordering
+     169              :    * 5. Create extra column and backfill names for matched migrations
+     170              :    */
+     171            0 :   0: (migrationsTable, session, localMigrations) => {
+     172            0 :     const table = sql`${sql.identifier(migrationsTable)}`
+     173            0 :     const dbRows = session.all<SQLiteMigrationTableRow>(sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`)
+     174            0 :     const statements = buildSQLiteMigrationBackfillStatements(
+     175            0 :       migrationsTable,
+     176            0 :       prepareSQLiteMigrationBackfill(dbRows, localMigrations),
+     177            0 :     )
+     178            0 : 
+     179            0 :     session.transaction((tx) => {
+     180            0 :       for (const statement of statements) {
+     181            0 :         tx.run(statement)
+     182            0 :       }
+     183            1 :     })
+     184              :   },
+     185            2 : }
+     186              : 
+     187              : /**
+     188              :  * Detects the current version of the migrations table schema and upgrades it if needed.
+     189              :  *
+     190              :  * Version 0: Original schema (id, hash, created_at)
+     191              :  * Version 1: Extended schema (id, hash, created_at, name, applied_at)
+     192              :  */
+     193            0 : export async function upgradeAsyncIfNeeded(
+     194            0 :   migrationsTable: string,
+     195            0 :   db: AsyncSQLiteDatabaseWithSession,
+     196            0 :   localMigrations: MigrationMeta[],
+     197            0 : ): Promise<UpgradeResult> {
+     198            0 :   // Check if the table exists at all
+     199            0 :   const tableExists = await db.session.all(
+     200            0 :     sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`,
+     201            0 :   )
+     202            0 : 
+     203            0 :   if (tableExists.length === 0) {
+     204            0 :     return { newDb: true }
+     205            0 :   }
+     206            0 : 
+     207            0 :   const rows = await db.session.all<{ column_name: string }>(
+     208            0 :     sql`SELECT name as column_name FROM pragma_table_info(${migrationsTable})`,
+     209            0 :   )
+     210            0 : 
+     211            0 :   const version = GET_VERSION_FOR.sqlite(rows.map((r) => r.column_name))
+     212            0 : 
+     213            0 :   for (let v = version; v < MIGRATIONS_TABLE_VERSIONS.sqlite; v++) {
+     214            0 :     const upgradeFn = upgradeAsyncFunctions[v]
+     215            0 :     if (!upgradeFn) {
+     216            0 :       throw new Error(`No upgrade path from migration table version ${v} to ${v + 1}`)
+     217            0 :     }
+     218            0 :     await upgradeFn(migrationsTable, db, localMigrations)
+     219            0 :   }
+     220            0 : 
+     221            1 :   return { newDb: false }
+     222              : }
+     223              : 
+     224           29 : const upgradeAsyncFunctions: Record<
+     225              :   number,
+     226              :   (migrationsTable: string, db: AsyncSQLiteDatabaseWithSession, localMigrations: MigrationMeta[]) => Promise<void>
+     227            3 : > = {
+     228              :   /**
+     229              :    * Upgrade from version 0 to version 1:
+     230              :    * 1. Read all existing DB migrations
+     231              :    * 2. Sort localMigrations ASC by millis and if the same - sort by name
+     232              :    * 3. Match each DB row to a local migration
+     233              :    * If multiple migrations share the same second, use hash matching as a tiebreaker
+     234              :    * Not implemented for now -> If hash matching fails, fall back to serial id ordering
+     235              :    * 5. Create extra column and backfill names for matched migrations
+     236              :    */
+     237            0 :   0: async (migrationsTable, db, localMigrations) => {
+     238            0 :     const table = sql`${sql.identifier(migrationsTable)}`
+     239            0 :     const dbRows = await db.session.all<SQLiteMigrationTableRow>(
+     240            0 :       sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`,
+     241            0 :     )
+     242            0 :     const statements = buildSQLiteMigrationBackfillStatements(
+     243            0 :       migrationsTable,
+     244            0 :       prepareSQLiteMigrationBackfill(dbRows, localMigrations),
+     245            0 :     )
+     246            0 : 
+     247            0 :     await db.transaction(async (tx) => {
+     248            0 :       for (const statement of statements) {
+     249            0 :         await tx.run(statement)
+     250            0 :       }
+     251            1 :     })
+     252              :   },
+     253            1 : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/effect-drizzle-sqlite/src/up-migrations/utils.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/up-migrations/utils.ts.gcov.html new file mode 100644 index 00000000..8b8c90f3 --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/up-migrations/utils.ts.gcov.html @@ -0,0 +1,121 @@ + + + + + + + LCOV - opencode-lcov.info - ../effect-drizzle-sqlite/src/up-migrations/utils.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../effect-drizzle-sqlite/src/up-migrations - utils.tsCoverageTotalHit
Test:opencode-lcov.infoLines:56.2 %3218
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /* oxlint-disable */
+       2              : export interface UpgradeResult {
+       3              :   newDb: boolean
+       4              : }
+       5              : 
+       6           43 : export const MIGRATIONS_TABLE_VERSIONS = {
+       7           12 :   sqlite: 1,
+       8            8 :   pg: 1,
+       9           12 :   effect: 1,
+      10           11 :   mysql: 1,
+      11           11 :   mssql: 1,
+      12           15 :   cockroach: 1,
+      13           15 :   singlestore: 1,
+      14            2 : } as const
+      15              : 
+      16           33 : export const GET_VERSION_FOR = {
+      17            0 :   mysql: (columns: string[]): number => {
+      18            0 :     if (columns.includes("name")) return 1
+      19            3 :     return 0
+      20              :   },
+      21            0 :   pg: (columns: string[]): number => {
+      22            0 :     if (columns.includes("name")) return 1
+      23            3 :     return 0
+      24              :   },
+      25            0 :   effect: (columns: string[]): number => {
+      26            0 :     if (columns.includes("name")) return 1
+      27            3 :     return 0
+      28              :   },
+      29            0 :   mssql: (columns: string[]): number => {
+      30            0 :     if (columns.includes("name")) return 1
+      31            3 :     return 0
+      32              :   },
+      33            0 :   cockroach: (columns: string[]): number => {
+      34            0 :     if (columns.includes("name")) return 1
+      35            3 :     return 0
+      36              :   },
+      37            0 :   singlestore: (columns: string[]): number => {
+      38            0 :     if (columns.includes("name")) return 1
+      39            3 :     return 0
+      40              :   },
+      41            0 :   sqlite: (columns: string[]): number => {
+      42            0 :     if (columns.includes("name")) return 1
+      43            1 :     return 0
+      44              :   },
+      45            1 : } as const
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/cassette.ts.gcov.html b/packages/core/http-recorder/src/cassette.ts.gcov.html new file mode 100644 index 00000000..b06aa68c --- /dev/null +++ b/packages/core/http-recorder/src/cassette.ts.gcov.html @@ -0,0 +1,255 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src/cassette.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/src - cassette.tsCoverageTotalHit
Test:opencode-lcov.infoLines:40.3 %14458
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           41 : import { Context, Effect, FileSystem, Layer, Schema, Semaphore } from "effect"
+       2           30 : import * as fs from "node:fs"
+       3           34 : import * as path from "node:path"
+       4           69 : import { secretFindings, SecretFindingSchema, type SecretFinding } from "./redaction.js"
+       5           61 : import { CassetteSchema, encodeCassette, type Cassette, type CassetteMetadata, type Interaction } from "./schema.js"
+       6              : 
+       7           93 : const DEFAULT_RECORDINGS_DIR = path.resolve(process.cwd(), "test", "fixtures", "recordings")
+       8              : 
+       9           96 : export class CassetteNotFoundError extends Schema.TaggedErrorClass<CassetteNotFoundError>()("CassetteNotFoundError", {
+      10           28 :   cassetteName: Schema.String,
+      11            0 : }) {
+      12            0 :   override get message() {
+      13            1 :     return `Cassette "${this.cassetteName}" not found`
+      14              :   }
+      15            1 : }
+      16              : 
+      17           92 : export class UnsafeCassetteError extends Schema.TaggedErrorClass<UnsafeCassetteError>()("UnsafeCassetteError", {
+      18           30 :   cassetteName: Schema.String,
+      19           44 :   findings: Schema.Array(SecretFindingSchema),
+      20            0 : }) {
+      21            0 :   override get message() {
+      22            0 :     return `Refusing to write cassette "${this.cassetteName}" because it contains possible secrets: ${this.findings
+      23            0 :       .map((finding) => `${finding.path} (${finding.reason})`)
+      24            1 :       .join(", ")}`
+      25              :   }
+      26            1 : }
+      27              : 
+      28              : export interface Interface {
+      29              :   readonly read: (name: string) => Effect.Effect<ReadonlyArray<Interaction>, CassetteNotFoundError>
+      30              :   readonly append: (
+      31              :     name: string,
+      32              :     interaction: Interaction,
+      33              :     metadata?: CassetteMetadata,
+      34              :   ) => Effect.Effect<void, UnsafeCassetteError>
+      35              :   readonly exists: (name: string) => Effect.Effect<boolean>
+      36              :   readonly list: () => Effect.Effect<ReadonlyArray<string>>
+      37              : }
+      38              : 
+      39           88 : export class Service extends Context.Service<Service, Interface>()("@opencode-ai/http-recorder/Cassette") {}
+      40              : 
+      41           43 : const cassettePath = (directory: string, name: string) => {
+      42          107 :   if (!name || path.isAbsolute(name) || path.win32.isAbsolute(name) || name.split(/[\\/]/).includes(".."))
+      43            2 :     throw new Error(`Invalid cassette name "${name}"`)
+      44           39 :   const root = path.resolve(directory)
+      45           52 :   const target = path.resolve(root, `${name}.json`)
+      46           47 :   const relative = path.relative(root, target)
+      47           75 :   if (!relative || relative.startsWith("..") || path.isAbsolute(relative))
+      48            2 :     throw new Error(`Invalid cassette name "${name}"`)
+      49           15 :   return target
+      50              : }
+      51              : 
+      52            0 : export const hasCassetteSync = (name: string, options: { readonly directory?: string } = {}) =>
+      53            2 :   fs.existsSync(cassettePath(options.directory ?? DEFAULT_RECORDINGS_DIR, name))
+      54              : 
+      55            0 : const buildCassette = (
+      56            0 :   name: string,
+      57            0 :   interactions: ReadonlyArray<Interaction>,
+      58            0 :   metadata: CassetteMetadata | undefined,
+      59            0 : ): Cassette => ({
+      60            0 :   version: 1,
+      61            0 :   metadata: { name, recordedAt: new Date().toISOString(), ...metadata },
+      62              :   interactions,
+      63            2 : })
+      64              : 
+      65           23 : const formatCassette = (cassette: Cassette) => `${JSON.stringify(encodeCassette(cassette), null, 2)}\n`
+      66              : 
+      67           86 : const parseCassette = Schema.decodeUnknownSync(Schema.fromJsonString(CassetteSchema))
+      68              : 
+      69            0 : const failIfUnsafe = (name: string, findings: ReadonlyArray<SecretFinding>) =>
+      70            2 :   findings.length === 0 ? Effect.void : Effect.fail(new UnsafeCassetteError({ cassetteName: name, findings }))
+      71              : 
+      72           25 : export const fileSystem = (
+      73           17 :   options: { readonly directory?: string } = {},
+      74              : ): Layer.Layer<Service, never, FileSystem.FileSystem> =>
+      75           13 :   Layer.effect(
+      76            9 :     Service,
+      77           15 :     Effect.gen(function* () {
+      78           42 :       const fs = yield* FileSystem.FileSystem
+      79           64 :       const directory = options.directory ?? DEFAULT_RECORDINGS_DIR
+      80           27 :       const recorded = new Map<string, { interactions: Interaction[]; findings: SecretFinding[] }>()
+      81           46 :       const appendLock = yield* Semaphore.make(1)
+      82              : 
+      83           56 :       const pathFor = (name: string) => cassettePath(directory, name)
+      84              : 
+      85            0 :       const walk = (current: string): Effect.Effect<ReadonlyArray<string>> =>
+      86            0 :         Effect.gen(function* () {
+      87            0 :           const entries = yield* fs.readDirectory(current).pipe(Effect.catch(() => Effect.succeed([] as string[])))
+      88            0 :           const nested = yield* Effect.forEach(entries, (entry) => {
+      89            0 :             const full = path.join(current, entry)
+      90            0 :             return fs.stat(full).pipe(
+      91            0 :               Effect.flatMap((stat) => (stat.type === "Directory" ? walk(full) : Effect.succeed([full]))),
+      92            0 :               Effect.catch(() => Effect.succeed([] as string[])),
+      93            0 :             )
+      94            0 :           })
+      95              :           return nested.flat()
+      96            3 :         })
+      97              : 
+      98           23 :       return Service.of({
+      99           15 :         read: (name) =>
+     100           38 :           fs.readFileString(pathFor(name)).pipe(
+     101           52 :             Effect.map((raw) => parseCassette(raw).interactions),
+     102           13 :             Effect.catch(() => Effect.fail(new CassetteNotFoundError({ cassetteName: name }))),
+     103            5 :           ),
+     104            0 :         append: (name, interaction, metadata) =>
+     105            0 :           appendLock.withPermit(
+     106            0 :             Effect.gen(function* () {
+     107            0 :               const entry = recorded.get(name) ?? { interactions: [], findings: [] }
+     108            0 :               const interactions = [...entry.interactions, interaction]
+     109            0 :               const interactionFindings = [...entry.findings, ...secretFindings(interaction)]
+     110            0 :               const cassette = buildCassette(name, interactions, metadata)
+     111            0 :               const findings = [...interactionFindings, ...secretFindings(cassette.metadata ?? {})]
+     112            0 :               yield* failIfUnsafe(name, findings)
+     113            0 :               const target = pathFor(name)
+     114            0 :               yield* fs.makeDirectory(path.dirname(target), { recursive: true }).pipe(Effect.orDie)
+     115            0 :               const temporary = `${target}.${crypto.randomUUID()}.tmp`
+     116            0 :               yield* fs.writeFileString(temporary, formatCassette(cassette)).pipe(
+     117            0 :                 Effect.flatMap(() => fs.rename(temporary, target)),
+     118            0 :                 Effect.ensuring(fs.remove(temporary, { force: true }).pipe(Effect.catch(() => Effect.void))),
+     119            0 :                 Effect.orDie,
+     120            0 :               )
+     121            0 :               recorded.set(name, { interactions, findings: interactionFindings })
+     122              :             }),
+     123            5 :           ),
+     124           17 :         exists: (name) =>
+     125           30 :           fs.access(pathFor(name)).pipe(
+     126           17 :             Effect.as(true),
+     127           13 :             Effect.catch(() => Effect.succeed(false)),
+     128            5 :           ),
+     129            0 :         list: () =>
+     130            0 :           walk(directory).pipe(
+     131            0 :             Effect.map((files) =>
+     132            0 :               files
+     133            0 :                 .filter((file) => file.endsWith(".json"))
+     134            0 :                 .map((file) =>
+     135            0 :                   path
+     136            0 :                     .relative(directory, file)
+     137            0 :                     .replace(/\\/g, "/")
+     138            0 :                     .replace(/\.json$/, ""),
+     139            0 :                 )
+     140            0 :                 .toSorted((a, b) => a.localeCompare(b)),
+     141              :             ),
+     142            2 :           ),
+     143            2 :       })
+     144            1 :     }),
+     145            2 :   )
+     146              : 
+     147            0 : export const memory = (initial: Record<string, ReadonlyArray<Interaction>> = {}): Layer.Layer<Service> =>
+     148            0 :   Layer.sync(Service, () => {
+     149            0 :     const stored = new Map<string, Interaction[]>(
+     150            0 :       Object.entries(initial).map(([name, interactions]) => [name, [...interactions]]),
+     151            0 :     )
+     152            0 :     const accumulatedFindings = new Map<string, SecretFinding[]>()
+     153            0 :     const appendLock = Semaphore.makeUnsafe(1)
+     154            0 : 
+     155            0 :     return Service.of({
+     156            0 :       read: (name) =>
+     157            0 :         stored.has(name)
+     158            0 :           ? Effect.succeed(stored.get(name) ?? [])
+     159            0 :           : Effect.fail(new CassetteNotFoundError({ cassetteName: name })),
+     160            0 :       append: (name, interaction, metadata) =>
+     161            0 :         appendLock.withPermit(
+     162            0 :           Effect.suspend(() => {
+     163            0 :             const interactions = [...(stored.get(name) ?? []), interaction]
+     164            0 :             const findings = [...(accumulatedFindings.get(name) ?? []), ...secretFindings(interaction)]
+     165            0 :             const allFindings = metadata ? [...findings, ...secretFindings({ name, ...metadata })] : findings
+     166            0 :             return failIfUnsafe(name, allFindings).pipe(
+     167            0 :               Effect.tap(() =>
+     168            0 :                 Effect.sync(() => {
+     169            0 :                   stored.set(name, interactions)
+     170            0 :                   accumulatedFindings.set(name, findings)
+     171            0 :                 }),
+     172            0 :               ),
+     173            0 :             )
+     174            0 :           }),
+     175            0 :         ),
+     176            0 :       exists: (name) => Effect.sync(() => stored.has(name)),
+     177            0 :       list: () => Effect.sync(() => Array.from(stored.keys()).toSorted()),
+     178              :     })
+     179            1 :   })
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/effect.ts.gcov.html b/packages/core/http-recorder/src/effect.ts.gcov.html new file mode 100644 index 00000000..e82f7331 --- /dev/null +++ b/packages/core/http-recorder/src/effect.ts.gcov.html @@ -0,0 +1,101 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src/effect.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/src - effect.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %1616
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           55 : import { NodeFileSystem } from "@effect/platform-node"
+       2           38 : import * as Layer from "effect/Layer"
+       3           55 : import { FetchHttpClient } from "effect/unstable/http"
+       4              : import type * as HttpClient from "effect/unstable/http/HttpClient"
+       5           49 : import * as CassetteService from "./cassette.js"
+       6           54 : import { recordingLayer } from "./internal-effect.js"
+       7           37 : import { make } from "./redactor.js"
+       8              : import type { RecorderOptions } from "./types.js"
+       9              : 
+      10              : /**
+      11              :  * Provides a fetch-backed `HttpClient` with cassette recording and replay.
+      12              :  *
+      13              :  * Locally, a missing cassette is recorded from the real service. Existing
+      14              :  * cassettes are replayed, and `CI=true` makes a missing cassette fail.
+      15              :  */
+      16           42 : export const http = (name: string, options: RecorderOptions = {}): Layer.Layer<HttpClient.HttpClient> =>
+      17           24 :   recordingLayer(name, {
+      18           29 :     metadata: options.metadata,
+      19           33 :     redactor: make(options.redact),
+      20           21 :     match: options.match,
+      21            7 :   }).pipe(
+      22           77 :     Layer.provide(CassetteService.fileSystem({ directory: options.directory })),
+      23           38 :     Layer.provide(FetchHttpClient.layer),
+      24           35 :     Layer.provide(NodeFileSystem.layer),
+      25            1 :   )
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/index-sort-f.html b/packages/core/http-recorder/src/index-sort-f.html new file mode 100644 index 00000000..a466f6f0 --- /dev/null +++ b/packages/core/http-recorder/src/index-sort-f.html @@ -0,0 +1,197 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/srcCoverageTotalHit
Test:opencode-lcov.infoLines:44.0 %1090480
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
cassette.ts +
40.3%40.3%
+
40.3 %14458
effect.ts +
100.0%
+
100.0 %1616
index.ts +
100.0%
+
100.0 %33
internal-effect.ts +
54.2%54.2%
+
54.2 %14478
internal.ts +
100.0%
+
100.0 %88
matching.ts +
51.2%51.2%
+
51.2 %8644
recorder.ts +
95.2%95.2%
+
95.2 %4240
redaction.ts +
76.0%76.0%
+
76.0 %9673
redactor.ts +
80.2%80.2%
+
80.2 %9173
schema.ts +
98.2%98.2%
+
98.2 %5554
socket.ts +
8.1%8.1%
+
8.1 %27022
websocket.ts +
8.1%8.1%
+
8.1 %13511
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/index-sort-l.html b/packages/core/http-recorder/src/index-sort-l.html new file mode 100644 index 00000000..55d3882f --- /dev/null +++ b/packages/core/http-recorder/src/index-sort-l.html @@ -0,0 +1,197 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/srcCoverageTotalHit
Test:opencode-lcov.infoLines:44.0 %1090480
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
websocket.ts +
8.1%8.1%
+
8.1 %13511
socket.ts +
8.1%8.1%
+
8.1 %27022
cassette.ts +
40.3%40.3%
+
40.3 %14458
matching.ts +
51.2%51.2%
+
51.2 %8644
internal-effect.ts +
54.2%54.2%
+
54.2 %14478
redaction.ts +
76.0%76.0%
+
76.0 %9673
redactor.ts +
80.2%80.2%
+
80.2 %9173
recorder.ts +
95.2%95.2%
+
95.2 %4240
schema.ts +
98.2%98.2%
+
98.2 %5554
index.ts +
100.0%
+
100.0 %33
internal.ts +
100.0%
+
100.0 %88
effect.ts +
100.0%
+
100.0 %1616
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/index.html b/packages/core/http-recorder/src/index.html new file mode 100644 index 00000000..7caa2dc2 --- /dev/null +++ b/packages/core/http-recorder/src/index.html @@ -0,0 +1,197 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/srcCoverageTotalHit
Test:opencode-lcov.infoLines:44.0 %1090480
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
cassette.ts +
40.3%40.3%
+
40.3 %14458
effect.ts +
100.0%
+
100.0 %1616
index.ts +
100.0%
+
100.0 %33
internal-effect.ts +
54.2%54.2%
+
54.2 %14478
internal.ts +
100.0%
+
100.0 %88
matching.ts +
51.2%51.2%
+
51.2 %8644
recorder.ts +
95.2%95.2%
+
95.2 %4240
redaction.ts +
76.0%76.0%
+
76.0 %9673
redactor.ts +
80.2%80.2%
+
80.2 %9173
schema.ts +
98.2%98.2%
+
98.2 %5554
socket.ts +
8.1%8.1%
+
8.1 %27022
websocket.ts +
8.1%8.1%
+
8.1 %13511
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/index.ts.gcov.html b/packages/core/http-recorder/src/index.ts.gcov.html new file mode 100644 index 00000000..da189523 --- /dev/null +++ b/packages/core/http-recorder/src/index.ts.gcov.html @@ -0,0 +1,94 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src/index.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/src - index.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %33
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           35 : import { http } from "./effect.js"
+       2           37 : import { socket } from "./socket.js"
+       3              : 
+       4              : /** HTTP and WebSocket cassette recording. */
+       5           44 : export const HttpRecorder = { http, socket } as const
+       6              : 
+       7              : export namespace HttpRecorder {
+       8              :   /** Additional JSON metadata stored with a cassette. */
+       9              :   export type CassetteMetadata = import("./types.js").CassetteMetadata
+      10              :   /** Recorder configuration. */
+      11              :   export type RecorderOptions = import("./types.js").RecorderOptions
+      12              :   /** Additive redaction and header-preservation policy. */
+      13              :   export type RedactOptions = import("./types.js").RedactOptions
+      14              :   /** Returns whether an incoming HTTP request matches a recorded request. */
+      15              :   export type RequestMatcher = import("./types.js").RequestMatcher
+      16              :   /** The normalized HTTP request representation used for matching. */
+      17              :   export type RequestSnapshot = import("./types.js").RequestSnapshot
+      18              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/internal-effect.ts.gcov.html b/packages/core/http-recorder/src/internal-effect.ts.gcov.html new file mode 100644 index 00000000..79046862 --- /dev/null +++ b/packages/core/http-recorder/src/internal-effect.ts.gcov.html @@ -0,0 +1,265 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src/internal-effect.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/src - internal-effect.tsCoverageTotalHit
Test:opencode-lcov.infoLines:54.2 %14478
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           55 : import { NodeFileSystem } from "@effect/platform-node"
+       2           62 : import { Deferred, Effect, Layer, Option, Ref } from "effect"
+       3          160 : import {
+       4              :   FetchHttpClient,
+       5              :   Headers,
+       6              :   HttpBody,
+       7              :   HttpClient,
+       8              :   HttpClientError,
+       9              :   HttpClientRequest,
+      10              :   HttpClientResponse,
+      11              :   UrlParams,
+      12              : } from "effect/unstable/http"
+      13           49 : import * as CassetteService from "./cassette.js"
+      14           65 : import { defaultMatcher, selectSequential } from "./matching.js"
+      15           65 : import { makeReplayState, resolveAutoMode } from "./recorder.js"
+      16           37 : import { make, type Redactor } from "./redactor.js"
+      17           43 : import { redactUrl } from "./redaction.js"
+      18           47 : import { httpInteractions } from "./schema.js"
+      19              : import type { CassetteMetadata, HttpInteraction, RequestMatcher, ResponseSnapshot } from "./types.js"
+      20              : 
+      21           26 : export { defaultMatcher }
+      22              : 
+      23              : export type RecordReplayMode = "auto" | "record" | "replay" | "passthrough"
+      24              : 
+      25              : export interface RecordReplayOptions {
+      26              :   readonly mode?: RecordReplayMode
+      27              :   readonly directory?: string
+      28              :   readonly metadata?: CassetteMetadata
+      29              :   readonly redactor?: Redactor
+      30              :   readonly match?: RequestMatcher
+      31              : }
+      32              : 
+      33           37 : const TEXT_CONTENT_TYPES = new Set([
+      34           24 :   "application/graphql",
+      35           27 :   "application/javascript",
+      36           21 :   "application/json",
+      37           20 :   "application/sql",
+      38           38 :   "application/x-www-form-urlencoded",
+      39           20 :   "application/xml",
+      40           21 :   "application/yaml",
+      41           16 :   "image/svg+xml",
+      42            3 : ])
+      43              : 
+      44            0 : const isTextContentType = (contentType: string | undefined) => {
+      45            0 :   const mediaType = contentType?.split(";", 1)[0]?.trim().toLowerCase()
+      46            0 :   if (!mediaType) return false
+      47            0 :   return (
+      48            0 :     mediaType.startsWith("text/") ||
+      49            0 :     mediaType.endsWith("+json") ||
+      50            0 :     mediaType.endsWith("+xml") ||
+      51            2 :     TEXT_CONTENT_TYPES.has(mediaType)
+      52              :   )
+      53              : }
+      54              : 
+      55            0 : const captureResponseBody = (response: HttpClientResponse.HttpClientResponse, contentType: string | undefined) =>
+      56            0 :   response.arrayBuffer.pipe(
+      57            0 :     Effect.map((bytes) =>
+      58            0 :       isTextContentType(contentType)
+      59            0 :         ? { body: new TextDecoder().decode(bytes) }
+      60            0 :         : { body: Buffer.from(bytes).toString("base64"), bodyEncoding: "base64" as const },
+      61              :     ),
+      62            2 :   )
+      63              : 
+      64           39 : const decodeResponseBody = (snapshot: ResponseSnapshot) =>
+      65           53 :   snapshot.bodyEncoding === "base64" ? Buffer.from(snapshot.body, "base64") : snapshot.body
+      66              : 
+      67           50 : const responseFromSnapshot = (request: HttpClientRequest.HttpClientRequest, snapshot: ResponseSnapshot) =>
+      68           27 :   HttpClientResponse.fromWeb(
+      69            9 :     request,
+      70           13 :     new Response(
+      71          108 :       request.method === "HEAD" || snapshot.status === 204 || snapshot.status === 205 || snapshot.status === 304
+      72            2 :         ? null
+      73           29 :         : decodeResponseBody(snapshot),
+      74            8 :       snapshot,
+      75            1 :     ),
+      76            2 :   )
+      77              : 
+      78            0 : export const redactedErrorRequest = (request: HttpClientRequest.HttpClientRequest) =>
+      79            0 :   HttpClientRequest.makeWith(
+      80            0 :     request.method,
+      81            0 :     redactUrl(request.url),
+      82            0 :     UrlParams.empty,
+      83            0 :     Option.none(),
+      84            0 :     Headers.empty,
+      85              :     HttpBody.empty,
+      86            2 :   )
+      87              : 
+      88            0 : const transportError = (request: HttpClientRequest.HttpClientRequest, description: string) =>
+      89            0 :   new HttpClientError.HttpClientError({
+      90              :     reason: new HttpClientError.TransportError({ request: redactedErrorRequest(request), description }),
+      91            2 :   })
+      92              : 
+      93           29 : export const recordingLayer = (
+      94            6 :   name: string,
+      95           17 :   options: Omit<RecordReplayOptions, "directory"> = {},
+      96              : ): Layer.Layer<HttpClient.HttpClient, never, HttpClient.HttpClient | CassetteService.Service> =>
+      97           13 :   Layer.effect(
+      98           23 :     HttpClient.HttpClient,
+      99           15 :     Effect.gen(function* () {
+     100           48 :       const upstream = yield* HttpClient.HttpClient
+     101           57 :       const cassetteService = yield* CassetteService.Service
+     102           46 :       const redactor = options.redactor ?? make()
+     103           48 :       const match = options.match ?? defaultMatcher
+     104           43 :       const requested = options.mode ?? "auto"
+     105           82 :       const mode = requested === "auto" ? yield* resolveAutoMode(cassetteService, name) : requested
+     106              : 
+     107           36 :       const snapshotRequest = (request: HttpClientRequest.HttpClientRequest) =>
+     108           17 :         Effect.gen(function* () {
+     109           75 :           const web = yield* HttpClientRequest.toWeb(request).pipe(Effect.orDie)
+     110           31 :           return redactor.request({
+     111           25 :             method: web.method,
+     112           19 :             url: web.url,
+     113           57 :             headers: Object.fromEntries(web.headers.entries()),
+     114           47 :             body: yield* Effect.promise(() => web.text()),
+     115            3 :           })
+     116            3 :         })
+     117              : 
+     118           32 :       if (mode === "passthrough") return upstream
+     119              : 
+     120           22 :       if (mode === "record") {
+     121            0 :         const initial = yield* Deferred.make<void>()
+     122            0 :         yield* Deferred.succeed(initial, undefined)
+     123            0 :         const tail = yield* Ref.make(initial)
+     124            0 :         return HttpClient.make((request) =>
+     125            0 :           Effect.gen(function* () {
+     126            0 :             const completed = yield* Deferred.make<void>()
+     127            0 :             const previous = yield* Ref.modify(tail, (current) => [current, completed])
+     128            0 :             return yield* Effect.gen(function* () {
+     129            0 :               const incoming = yield* snapshotRequest(request)
+     130            0 :               const response = yield* upstream.execute(request)
+     131            0 :               const captured = yield* captureResponseBody(response, response.headers["content-type"])
+     132            0 :               const responseSnapshot: ResponseSnapshot = {
+     133            0 :                 status: response.status,
+     134            0 :                 headers: response.headers as Record<string, string>,
+     135            0 :                 ...captured,
+     136            0 :               }
+     137            0 :               const interaction: HttpInteraction = {
+     138            0 :                 transport: "http",
+     139            0 :                 request: incoming,
+     140            0 :                 response: redactor.response(responseSnapshot),
+     141            0 :               }
+     142            0 :               yield* Deferred.await(previous)
+     143            0 :               yield* cassetteService
+     144            0 :                 .append(name, interaction, options.metadata)
+     145            0 :                 .pipe(
+     146            0 :                   Effect.catchTag("UnsafeCassetteError", (error) =>
+     147            0 :                     Effect.fail(transportError(request, error.message)),
+     148            0 :                   ),
+     149            0 :                 )
+     150            0 :               return responseFromSnapshot(request, responseSnapshot)
+     151              :             }).pipe(Effect.ensuring(Deferred.succeed(completed, undefined)))
+     152              :           }),
+     153            0 :         )
+     154            2 :       }
+     155              : 
+     156           81 :       const replay = yield* makeReplayState(cassetteService, name, httpInteractions)
+     157           35 :       return HttpClient.make((request) =>
+     158           17 :         Effect.gen(function* () {
+     159           53 :           const incoming = yield* snapshotRequest(request)
+     160           30 :           const claimed = yield* replay
+     161           50 :             .claim((interaction, index, interactions) => {
+     162           76 :               const result = selectSequential(interactions, incoming, match, index)
+     163           48 :               if (result.interaction) return Effect.void
+     164            0 :               return Effect.fail(
+     165            0 :                 transportError(request, `Fixture "${name}" does not match the current request: ${result.detail}.`),
+     166            0 :               )
+     167            2 :             })
+     168            5 :             .pipe(
+     169            0 :               Effect.mapError((error) =>
+     170            0 :                 error._tag === "CassetteNotFoundError"
+     171            0 :                   ? transportError(
+     172            0 :                       request,
+     173            0 :                       `Fixture "${name}" not found. Run locally to record it (CI=true forces replay).`,
+     174            0 :                     )
+     175              :                   : error,
+     176            1 :               ),
+     177            6 :             )
+     178           67 :           return responseFromSnapshot(request, claimed.interaction.response)
+     179              :         }),
+     180            1 :       )
+     181            1 :     }),
+     182            2 :   )
+     183              : 
+     184            0 : export const cassetteLayer = (name: string, options: RecordReplayOptions = {}): Layer.Layer<HttpClient.HttpClient> =>
+     185            0 :   recordingLayer(name, options).pipe(
+     186            0 :     Layer.provide(CassetteService.fileSystem({ directory: options.directory })),
+     187            0 :     Layer.provide(FetchHttpClient.layer),
+     188              :     Layer.provide(NodeFileSystem.layer),
+     189            1 :   )
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/internal.ts.gcov.html b/packages/core/http-recorder/src/internal.ts.gcov.html new file mode 100644 index 00000000..667db8c7 --- /dev/null +++ b/packages/core/http-recorder/src/internal.ts.gcov.html @@ -0,0 +1,91 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src/internal.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/src - internal.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %88
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           92 : export { CassetteNotFoundError, hasCassetteSync, UnsafeCassetteError } from "./cassette.js"
+       2           69 : export { cassetteLayer, recordingLayer, type RecordReplayMode, type RecordReplayOptions } from "./internal-effect.js"
+       3           74 : export { redactHeaders, redactUrl, secretFindings, type SecretFinding } from "./redaction.js"
+       4           42 : export { socketLayer } from "./socket.js"
+       5           55 : export {
+       6              :   makeWebSocketExecutor,
+       7              :   type WebSocketConnection,
+       8              :   type WebSocketExecutor,
+       9              :   type WebSocketRecordReplayOptions,
+      10              :   type WebSocketRequest,
+      11              : } from "./websocket.js"
+      12           42 : export * as Cassette from "./cassette.js"
+      13           42 : export * as Redactor from "./redactor.js"
+      14              : 
+      15           53 : export * as HttpRecorderInternal from "./internal.js"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/matching.ts.gcov.html b/packages/core/http-recorder/src/matching.ts.gcov.html new file mode 100644 index 00000000..6fa657b1 --- /dev/null +++ b/packages/core/http-recorder/src/matching.ts.gcov.html @@ -0,0 +1,182 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src/matching.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/src - matching.tsCoverageTotalHit
Test:opencode-lcov.infoLines:51.2 %8644
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           40 : import { Option, Schema } from "effect"
+       2           58 : import { REDACTED, secretFindings } from "./redaction.js"
+       3              : import type { HttpInteraction, RequestMatcher, RequestSnapshot } from "./types.js"
+       4              : 
+       5           56 : const JsonValue = Schema.fromJsonString(Schema.Unknown)
+       6           64 : export const decodeJson = Schema.decodeUnknownOption(JsonValue)
+       7              : 
+       8           26 : const isRecord = (value: unknown): value is Record<string, unknown> =>
+       9           69 :   value !== null && typeof value === "object" && !Array.isArray(value)
+      10              : 
+      11           44 : export const canonicalizeJson = (value: unknown): unknown => {
+      12           64 :   if (Array.isArray(value)) return value.map(canonicalizeJson)
+      13           25 :   if (isRecord(value)) {
+      14           26 :     return Object.fromEntries(
+      15           19 :       Object.keys(value)
+      16           11 :         .toSorted()
+      17           47 :         .map((key) => [key, canonicalizeJson(value[key])]),
+      18            1 :     )
+      19            2 :   }
+      20           14 :   return value
+      21              : }
+      22              : 
+      23              : export type { RequestMatcher } from "./types.js"
+      24              : 
+      25           45 : export const canonicalSnapshot = (snapshot: RequestSnapshot): string =>
+      26           18 :   JSON.stringify({
+      27           26 :     method: snapshot.method,
+      28           20 :     url: snapshot.url,
+      29           46 :     headers: canonicalizeJson(snapshot.headers),
+      30           51 :     body: Option.match(decodeJson(snapshot.body), {
+      31           12 :       onNone: () => snapshot.body,
+      32           26 :       onSome: canonicalizeJson,
+      33            3 :     }),
+      34            2 :   })
+      35              : 
+      36           52 : export const defaultMatcher: RequestMatcher = (incoming, recorded) =>
+      37           60 :   canonicalSnapshot(incoming) === canonicalSnapshot(recorded)
+      38              : 
+      39            0 : export const safeText = (value: unknown) => {
+      40            0 :   if (value === undefined) return "undefined"
+      41            0 :   if (secretFindings(value).length > 0) return JSON.stringify(REDACTED)
+      42            0 :   const text = JSON.stringify(value)
+      43            0 :   if (!text) return typeof value
+      44            2 :   return text.length > 300 ? `${text.slice(0, 300)}...` : text
+      45              : }
+      46              : 
+      47           17 : const jsonBody = (body: string) => Option.getOrUndefined(decodeJson(body))
+      48              : 
+      49            0 : const valueDiffs = (expected: unknown, received: unknown, base = "$", limit = 8): ReadonlyArray<string> => {
+      50            0 :   if (Object.is(expected, received)) return []
+      51            0 :   if (isRecord(expected) && isRecord(received)) {
+      52            0 :     return [...new Set([...Object.keys(expected), ...Object.keys(received)])]
+      53            0 :       .toSorted()
+      54            0 :       .flatMap((key) => valueDiffs(expected[key], received[key], `${base}.${key}`, limit))
+      55            0 :       .slice(0, limit)
+      56            0 :   }
+      57            0 :   if (Array.isArray(expected) && Array.isArray(received)) {
+      58            0 :     return Array.from({ length: Math.max(expected.length, received.length) }, (_, index) => index)
+      59            0 :       .flatMap((index) => valueDiffs(expected[index], received[index], `${base}[${index}]`, limit))
+      60            0 :       .slice(0, limit)
+      61            0 :   }
+      62            2 :   return [`${base} expected ${safeText(expected)}, received ${safeText(received)}`]
+      63              : }
+      64              : 
+      65            0 : const headerDiffs = (expected: Record<string, string>, received: Record<string, string>) =>
+      66            0 :   [...new Set([...Object.keys(expected), ...Object.keys(received)])].toSorted().flatMap((key) => {
+      67            0 :     if (expected[key] === received[key]) return []
+      68            0 :     if (expected[key] === undefined) return [`  ${key} unexpected ${safeText(received[key])}`]
+      69            0 :     if (received[key] === undefined) return [`  ${key} missing expected ${safeText(expected[key])}`]
+      70              :     return [`  ${key} expected ${safeText(expected[key])}, received ${safeText(received[key])}`]
+      71            2 :   })
+      72              : 
+      73            0 : export const requestDiff = (expected: RequestSnapshot, received: RequestSnapshot): ReadonlyArray<string> => {
+      74            0 :   const lines: string[] = []
+      75            0 :   if (expected.method !== received.method) {
+      76            0 :     lines.push("method:", `  expected ${expected.method}, received ${received.method}`)
+      77            0 :   }
+      78            0 :   if (expected.url !== received.url) {
+      79            0 :     lines.push("url:", `  expected ${expected.url}`, `  received ${received.url}`)
+      80            0 :   }
+      81            0 :   const headers = headerDiffs(expected.headers, received.headers)
+      82            0 :   if (headers.length > 0) lines.push("headers:", ...headers.slice(0, 8))
+      83            0 :   const expectedBody = jsonBody(expected.body)
+      84            0 :   const receivedBody = jsonBody(received.body)
+      85            0 :   const body =
+      86            0 :     expectedBody !== undefined && receivedBody !== undefined
+      87            0 :       ? valueDiffs(expectedBody, receivedBody).map((line) => `  ${line}`)
+      88            0 :       : expected.body === received.body
+      89            0 :         ? []
+      90            0 :         : [`  expected ${safeText(expected.body)}, received ${safeText(received.body)}`]
+      91            0 :   if (body.length > 0) lines.push("body:", ...body)
+      92            2 :   return lines
+      93              : }
+      94              : 
+      95           31 : export const selectSequential = (
+      96           14 :   interactions: ReadonlyArray<HttpInteraction>,
+      97           10 :   incoming: RequestSnapshot,
+      98            7 :   match: RequestMatcher,
+      99           10 :   index: number,
+     100            3 : ): { readonly interaction: HttpInteraction | undefined; readonly detail: string } => {
+     101           42 :   const interaction = interactions[index]
+     102           22 :   if (!interaction) return { interaction, detail: `interaction ${index + 1} of ${interactions.length} not recorded` }
+     103           45 :   if (!match(incoming, interaction.request))
+     104            2 :     return { interaction: undefined, detail: requestDiff(interaction.request, incoming).join("\n") }
+     105           35 :   return { interaction, detail: "" }
+     106              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/recorder.ts.gcov.html b/packages/core/http-recorder/src/recorder.ts.gcov.html new file mode 100644 index 00000000..2dc70c34 --- /dev/null +++ b/packages/core/http-recorder/src/recorder.ts.gcov.html @@ -0,0 +1,138 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src/recorder.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/src - recorder.tsCoverageTotalHit
Test:opencode-lcov.infoLines:95.2 %4240
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           49 : import { Effect, Scope, SynchronizedRef } from "effect"
+       2              : import type * as CassetteService from "./cassette.js"
+       3              : import type { CassetteNotFoundError } from "./cassette.js"
+       4              : import type { Interaction } from "./schema.js"
+       5              : 
+       6           20 : const isCI = () => {
+       7           31 :   const value = process.env.CI
+       8           82 :   return value !== undefined && value !== "" && value !== "false" && value !== "0"
+       9              : }
+      10              : 
+      11           30 : export const resolveAutoMode = (
+      12           10 :   cassette: CassetteService.Interface,
+      13            9 :   name: string,
+      14              : ): Effect.Effect<"record" | "replay" | "passthrough"> =>
+      15           15 :   Effect.gen(function* () {
+      16           16 :     if (isCI()) return "replay"
+      17           47 :     return (yield* cassette.exists(name)) ? "replay" : "record"
+      18            2 :   })
+      19              : 
+      20              : export interface ReplayState<T> {
+      21              :   readonly claim: <E>(
+      22              :     validate: (interaction: T | undefined, index: number, interactions: ReadonlyArray<T>) => Effect.Effect<void, E>,
+      23              :   ) => Effect.Effect<{ readonly interaction: T; readonly index: number }, CassetteNotFoundError | E>
+      24              : }
+      25              : 
+      26           30 : export const makeReplayState = <T>(
+      27           10 :   cassette: CassetteService.Interface,
+      28            6 :   name: string,
+      29           12 :   project: (interactions: ReadonlyArray<Interaction>) => ReadonlyArray<T>,
+      30              : ): Effect.Effect<ReplayState<T>, never, Scope.Scope> =>
+      31           15 :   Effect.gen(function* () {
+      32           83 :     const load = yield* Effect.cached(cassette.read(name).pipe(Effect.map(project)))
+      33           50 :     const position = yield* SynchronizedRef.make(0)
+      34              : 
+      35           32 :     yield* Effect.addFinalizer(() =>
+      36           17 :       Effect.gen(function* () {
+      37           54 :         const used = yield* SynchronizedRef.get(position)
+      38           24 :         if (used === 0) return yield* Effect.void
+      39           56 :         const interactions = yield* load.pipe(Effect.orDie)
+      40           36 :         if (used < interactions.length)
+      41            0 :           return yield* Effect.die(
+      42            0 :             new Error(`Unused recorded interactions in ${name}: used ${used} of ${interactions.length}`),
+      43            4 :           )
+      44           26 :         return yield* Effect.void
+      45              :       }),
+      46            4 :     )
+      47              : 
+      48           12 :     return {
+      49           20 :       claim: (validate) =>
+      50           38 :         Effect.flatMap(load, (interactions) =>
+      51           49 :           SynchronizedRef.modifyEffect(position, (index) =>
+      52           19 :             Effect.gen(function* () {
+      53           46 :               const interaction = interactions[index]
+      54           56 :               yield* validate(interaction, index, interactions)
+      55           37 :               if (interaction === undefined)
+      56            6 :                 return yield* Effect.die("Replay validation accepted a missing interaction")
+      57           43 :               return [{ interaction, index }, index + 1] as const
+      58              :             }),
+      59              :           ),
+      60            2 :         ),
+      61            1 :     }
+      62            1 :   })
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/redaction.ts.gcov.html b/packages/core/http-recorder/src/redaction.ts.gcov.html new file mode 100644 index 00000000..f2e9e4e1 --- /dev/null +++ b/packages/core/http-recorder/src/redaction.ts.gcov.html @@ -0,0 +1,193 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src/redaction.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/src - redaction.tsCoverageTotalHit
Test:opencode-lcov.infoLines:76.0 %9673
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Schema } from "effect"
+       2              : 
+       3           37 : export const REDACTED = "[REDACTED]"
+       4              : 
+       5           33 : const DEFAULT_REDACT_HEADERS = [
+       6           18 :   "authorization",
+       7           11 :   "cookie",
+       8           24 :   "proxy-authorization",
+       9           15 :   "set-cookie",
+      10           14 :   "x-api-key",
+      11           25 :   "x-amz-security-token",
+      12           17 :   "x-goog-api-key",
+      13            2 : ]
+      14              : 
+      15           31 : const DEFAULT_REDACT_QUERY = [
+      16           17 :   "access_token",
+      17           12 :   "api-key",
+      18           12 :   "api_key",
+      19           11 :   "apikey",
+      20            9 :   "code",
+      21            8 :   "key",
+      22           14 :   "signature",
+      23            8 :   "sig",
+      24           10 :   "token",
+      25           21 :   "x-amz-credential",
+      26           25 :   "x-amz-security-token",
+      27           18 :   "x-amz-signature",
+      28            2 : ]
+      29              : 
+      30           26 : const SECRET_PATTERNS: ReadonlyArray<{ readonly label: string; readonly pattern: RegExp }> = [
+      31           78 :   { label: "bearer token", pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}\b/i },
+      32           72 :   { label: "API key", pattern: /\bsk-[A-Za-z0-9][A-Za-z0-9_-]{20,}\b/ },
+      33           75 :   { label: "Anthropic API key", pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/ },
+      34           69 :   { label: "Google API key", pattern: /\bAIza[0-9A-Za-z_-]{20,}\b/ },
+      35           72 :   { label: "AWS access key", pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
+      36           72 :   { label: "GitHub token", pattern: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/ },
+      37           72 :   { label: "private key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
+      38            2 : ]
+      39              : 
+      40           85 : const ENV_SECRET_NAMES = /(?:API|AUTH|BEARER|CREDENTIAL|KEY|PASSWORD|SECRET|TOKEN)/i
+      41           65 : const SAFE_ENV_VALUES = new Set(["fixture", "test", "test-key"])
+      42              : 
+      43            0 : const envSecrets = () =>
+      44            0 :   Object.entries(process.env).flatMap(([name, value]) => {
+      45            0 :     if (!value) return []
+      46            0 :     if (!ENV_SECRET_NAMES.test(name)) return []
+      47            0 :     if (value.length < 12) return []
+      48            0 :     if (SAFE_ENV_VALUES.has(value.toLowerCase())) return []
+      49              :     return [{ name, value }]
+      50            2 :   })
+      51              : 
+      52           16 : const pathFor = (base: string, key: string) => (base ? `${base}.${key}` : key)
+      53              : 
+      54            0 : const stringEntries = (value: unknown, base = ""): ReadonlyArray<{ readonly path: string; readonly value: string }> => {
+      55            0 :   if (typeof value === "string") return [{ path: base, value }]
+      56            0 :   if (Array.isArray(value)) return value.flatMap((item, index) => stringEntries(item, `${base}[${index}]`))
+      57            0 :   if (value && typeof value === "object") {
+      58            0 :     return Object.entries(value).flatMap(([key, child]) => stringEntries(child, pathFor(base, key)))
+      59            0 :   }
+      60            2 :   return []
+      61              : }
+      62              : 
+      63           41 : const redactionSet = (values: ReadonlyArray<string> | undefined, defaults: ReadonlyArray<string>) =>
+      64           74 :   new Set([...defaults, ...(values ?? [])].map((value) => value.toLowerCase()))
+      65              : 
+      66              : export type UrlRedactor = (url: string) => string
+      67              : 
+      68           24 : export const redactUrl = (
+      69            5 :   raw: string,
+      70           30 :   query: ReadonlyArray<string> = DEFAULT_REDACT_QUERY,
+      71           16 :   urlRedactor?: UrlRedactor,
+      72            3 : ) => {
+      73           28 :   if (!URL.canParse(raw)) return urlRedactor?.(raw) ?? raw
+      74           27 :   const url = new URL(raw)
+      75           22 :   if (url.username) url.username = REDACTED
+      76           22 :   if (url.password) url.password = REDACTED
+      77           61 :   const redacted = redactionSet(query, DEFAULT_REDACT_QUERY)
+      78           42 :   for (const key of url.searchParams.keys()) {
+      79            0 :     if (redacted.has(key.toLowerCase())) url.searchParams.set(key, REDACTED)
+      80            2 :   }
+      81           56 :   return urlRedactor?.(url.toString()) ?? url.toString()
+      82              : }
+      83              : 
+      84           28 : export const redactHeaders = (
+      85            9 :   headers: Record<string, string>,
+      86            7 :   allow: ReadonlyArray<string>,
+      87           36 :   redact: ReadonlyArray<string> = DEFAULT_REDACT_HEADERS,
+      88            3 : ) => {
+      89           65 :   const allowed = new Set(allow.map((name) => name.toLowerCase()))
+      90           64 :   const redacted = redactionSet(redact, DEFAULT_REDACT_HEADERS)
+      91           26 :   return Object.fromEntries(
+      92           24 :     Object.entries(headers)
+      93           50 :       .map(([name, value]) => [name.toLowerCase(), value] as const)
+      94           36 :       .filter(([name]) => allowed.has(name))
+      95           58 :       .map(([name, value]) => [name, redacted.has(name) ? REDACTED : value] as const)
+      96            9 :       .toSorted(([a], [b]) => a.localeCompare(b)),
+      97            3 :   )
+      98              : }
+      99              : 
+     100           51 : export const SecretFindingSchema = Schema.Struct({
+     101           22 :   path: Schema.String,
+     102           22 :   reason: Schema.String,
+     103            3 : })
+     104              : export type SecretFinding = Schema.Schema.Type<typeof SecretFindingSchema>
+     105              : 
+     106            0 : export const secretFindings = (value: unknown): ReadonlyArray<SecretFinding> => {
+     107            0 :   const environment = envSecrets()
+     108            0 :   return stringEntries(value).flatMap((entry) => [
+     109            0 :     ...SECRET_PATTERNS.filter((item) => item.pattern.test(entry.value)).map((item) => ({
+     110            0 :       path: entry.path,
+     111            0 :       reason: item.label,
+     112            0 :     })),
+     113            0 :     ...environment
+     114            0 :       .filter((item) => entry.value.includes(item.value))
+     115            0 :       .map((item) => ({ path: entry.path, reason: `environment secret ${item.name}` })),
+     116            1 :   ])
+     117              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/redactor.ts.gcov.html b/packages/core/http-recorder/src/redactor.ts.gcov.html new file mode 100644 index 00000000..5dda80fc --- /dev/null +++ b/packages/core/http-recorder/src/redactor.ts.gcov.html @@ -0,0 +1,211 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src/redactor.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/src - redactor.tsCoverageTotalHit
Test:opencode-lcov.infoLines:80.2 %9173
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Option } from "effect"
+       2           43 : import { decodeJson } from "./matching.js"
+       3           68 : import { REDACTED, redactHeaders, redactUrl } from "./redaction.js"
+       4              : import type { RedactOptions, RequestSnapshot, ResponseSnapshot } from "./types.js"
+       5              : 
+       6              : export type { RedactOptions } from "./types.js"
+       7              : 
+       8           81 : export const DEFAULT_REQUEST_HEADERS: ReadonlyArray<string> = ["content-type", "accept", "openai-beta"]
+       9           57 : export const DEFAULT_RESPONSE_HEADERS: ReadonlyArray<string> = ["content-type"]
+      10              : 
+      11           17 : const identity = <T>(value: T) => value
+      12              : 
+      13              : export interface Redactor {
+      14              :   readonly request: (snapshot: RequestSnapshot) => RequestSnapshot
+      15              :   readonly response: (snapshot: ResponseSnapshot) => ResponseSnapshot
+      16              : }
+      17              : 
+      18           42 : export const compose = (...redactors: ReadonlyArray<Partial<Redactor>>): Redactor => {
+      19           80 :   const requests = redactors.map((r) => r.request).filter((fn): fn is Redactor["request"] => fn !== undefined)
+      20           82 :   const responses = redactors.map((r) => r.response).filter((fn): fn is Redactor["response"] => fn !== undefined)
+      21           12 :   return {
+      22           95 :     request: requests.length === 0 ? identity : (snapshot) => requests.reduce((acc, fn) => fn(acc), snapshot),
+      23           36 :     response: responses.length === 0 ? identity : (snapshot) => responses.reduce((acc, fn) => fn(acc), snapshot),
+      24            3 :   }
+      25              : }
+      26              : 
+      27              : export interface HeaderOptions {
+      28              :   readonly allow?: ReadonlyArray<string>
+      29              :   readonly redact?: ReadonlyArray<string>
+      30              : }
+      31              : 
+      32           50 : export const requestHeaders = (options: HeaderOptions = {}): Partial<Redactor> => ({
+      33           31 :   request: (snapshot) => ({
+      34           13 :     ...snapshot,
+      35          100 :     headers: redactHeaders(snapshot.headers, options.allow ?? DEFAULT_REQUEST_HEADERS, options.redact),
+      36            2 :   }),
+      37            2 : })
+      38              : 
+      39           51 : export const responseHeaders = (options: HeaderOptions = {}): Partial<Redactor> => ({
+      40            0 :   response: (snapshot) => ({
+      41            0 :     ...snapshot,
+      42            0 :     headers: redactHeaders(snapshot.headers, options.allow ?? DEFAULT_RESPONSE_HEADERS, options.redact),
+      43            1 :   }),
+      44            2 : })
+      45              : 
+      46              : export interface UrlOptions {
+      47              :   readonly query?: ReadonlyArray<string>
+      48              :   readonly transform?: (url: string) => string
+      49              : }
+      50              : 
+      51           39 : export const url = (options: UrlOptions = {}): Partial<Redactor> => ({
+      52          103 :   request: (snapshot) => ({ ...snapshot, url: redactUrl(snapshot.url, options.query, options.transform) }),
+      53            2 : })
+      54              : 
+      55            0 : export const body = (transform: (parsed: unknown) => unknown): Partial<Redactor> => ({
+      56            0 :   request: (snapshot) => ({
+      57            0 :     ...snapshot,
+      58            0 :     body: Option.match(decodeJson(snapshot.body), {
+      59            0 :       onNone: () => snapshot.body,
+      60            0 :       onSome: (parsed) => JSON.stringify(transform(parsed)),
+      61            0 :     }),
+      62              :   }),
+      63            2 : })
+      64              : 
+      65              : export interface DefaultRedactorOverrides {
+      66              :   readonly requestHeaders?: HeaderOptions
+      67              :   readonly responseHeaders?: HeaderOptions
+      68              :   readonly url?: UrlOptions
+      69              :   readonly body?: (parsed: unknown) => unknown
+      70              : }
+      71              : 
+      72           37 : const DEFAULT_REDACT_JSON_FIELDS = [
+      73           17 :   "access_token",
+      74           12 :   "api_key",
+      75           11 :   "apikey",
+      76           18 :   "client_secret",
+      77           13 :   "password",
+      78           18 :   "refresh_token",
+      79           11 :   "secret",
+      80            8 :   "token",
+      81            2 : ]
+      82              : 
+      83           79 : const normalizeField = (field: string) => field.replace(/[^a-z0-9]/gi, "").toLowerCase()
+      84              : 
+      85           45 : const redactJsonFields = (value: unknown, fields: ReadonlySet<string>): unknown => {
+      86           86 :   if (Array.isArray(value)) return value.map((item) => redactJsonFields(item, fields))
+      87           57 :   if (!value || typeof value !== "object") return value
+      88           26 :   return Object.fromEntries(
+      89           48 :     Object.entries(value).map(([key, child]) => [
+      90            8 :       key,
+      91           67 :       fields.has(normalizeField(key)) ? REDACTED : redactJsonFields(child, fields),
+      92            1 :     ]),
+      93            3 :   )
+      94              : }
+      95              : 
+      96           50 : const redactBody = (value: string, fields: ReadonlySet<string>, transform: ((body: string) => string) | undefined) => {
+      97           54 :   const redacted = Option.match(decodeJson(value), {
+      98           12 :     onNone: () => value,
+      99           68 :     onSome: (parsed) => JSON.stringify(redactJsonFields(parsed, fields)),
+     100            5 :   })
+     101           42 :   return transform?.(redacted) ?? redacted
+     102              : }
+     103              : 
+     104           39 : export const make = (options: RedactOptions = {}): Redactor => {
+     105          107 :   const fields = new Set([...DEFAULT_REDACT_JSON_FIELDS, ...(options.jsonFields ?? [])].map(normalizeField))
+     106           15 :   return compose(
+     107           20 :     requestHeaders({
+     108          104 :       allow: [...DEFAULT_REQUEST_HEADERS, ...(options.allowRequestHeaders ?? []), ...(options.headers ?? [])],
+     109           25 :       redact: options.headers,
+     110            4 :     }),
+     111           21 :     responseHeaders({
+     112          106 :       allow: [...DEFAULT_RESPONSE_HEADERS, ...(options.allowResponseHeaders ?? []), ...(options.headers ?? [])],
+     113           25 :       redact: options.headers,
+     114            4 :     }),
+     115           65 :     url({ query: options.queryParameters, transform: options.url }),
+     116            5 :     {
+     117           33 :       request: (snapshot) => ({
+     118           15 :         ...snapshot,
+     119           57 :         body: redactBody(snapshot.body, fields, options.body),
+     120            6 :       }),
+     121            0 :       response: (snapshot) => ({
+     122            0 :         ...snapshot,
+     123            0 :         body: redactBody(snapshot.body, fields, options.body),
+     124            2 :       }),
+     125            1 :     },
+     126            3 :   )
+     127              : }
+     128              : 
+     129            0 : export const defaults = (overrides: DefaultRedactorOverrides = {}): Redactor =>
+     130            0 :   compose(
+     131            0 :     requestHeaders(overrides.requestHeaders),
+     132            0 :     responseHeaders(overrides.responseHeaders),
+     133            0 :     url(overrides.url),
+     134              :     ...(overrides.body ? [body(overrides.body)] : []),
+     135            1 :   )
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/schema.ts.gcov.html b/packages/core/http-recorder/src/schema.ts.gcov.html new file mode 100644 index 00000000..5e4aeb6a --- /dev/null +++ b/packages/core/http-recorder/src/schema.ts.gcov.html @@ -0,0 +1,163 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src/schema.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/src - schema.tsCoverageTotalHit
Test:opencode-lcov.infoLines:98.2 %5554
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Schema } from "effect"
+       2              : import type {
+       3              :   CassetteMetadata,
+       4              :   HttpInteraction,
+       5              :   RequestSnapshot,
+       6              :   ResponseSnapshot,
+       7              :   WebSocketEvent,
+       8              :   WebSocketInteraction,
+       9              : } from "./types.js"
+      10              : 
+      11              : export type {
+      12              :   CassetteMetadata,
+      13              :   HttpInteraction,
+      14              :   RequestSnapshot,
+      15              :   ResponseSnapshot,
+      16              :   WebSocketEvent,
+      17              :   WebSocketInteraction,
+      18              : } from "./types.js"
+      19              : 
+      20           53 : export const RequestSnapshotSchema = Schema.Struct({
+      21           24 :   method: Schema.String,
+      22           21 :   url: Schema.String,
+      23           55 :   headers: Schema.Record(Schema.String, Schema.String),
+      24           20 :   body: Schema.String,
+      25            3 : })
+      26              : 
+      27           54 : export const ResponseSnapshotSchema = Schema.Struct({
+      28           24 :   status: Schema.Number,
+      29           55 :   headers: Schema.Record(Schema.String, Schema.String),
+      30           22 :   body: Schema.String,
+      31           67 :   bodyEncoding: Schema.optional(Schema.Literals(["text", "base64"])),
+      32            3 : })
+      33              : 
+      34           83 : export const CassetteMetadataSchema = Schema.Record(Schema.String, Schema.Unknown)
+      35              : 
+      36           53 : export const HttpInteractionSchema = Schema.Struct({
+      37           32 :   transport: Schema.tag("http"),
+      38           33 :   request: RequestSnapshotSchema,
+      39           33 :   response: ResponseSnapshotSchema,
+      40            3 : })
+      41              : 
+      42           51 : export const WebSocketEventSchema = Schema.Union([
+      43           19 :   Schema.Struct({
+      44           53 :     direction: Schema.Literals(["client", "server"]),
+      45           29 :     kind: Schema.tag("text"),
+      46           21 :     body: Schema.String,
+      47            5 :   }),
+      48           19 :   Schema.Struct({
+      49           53 :     direction: Schema.Literals(["client", "server"]),
+      50           31 :     kind: Schema.tag("binary"),
+      51           24 :     body: Schema.String,
+      52           40 :     bodyEncoding: Schema.Literal("base64"),
+      53            3 :   }),
+      54            3 : ])
+      55              : 
+      56           58 : export const WebSocketInteractionSchema = Schema.Struct({
+      57           37 :   transport: Schema.tag("websocket"),
+      58           25 :   open: Schema.Struct({
+      59           23 :     url: Schema.String,
+      60           54 :     headers: Schema.Record(Schema.String, Schema.String),
+      61            5 :   }),
+      62           43 :   events: Schema.Array(WebSocketEventSchema),
+      63            3 : })
+      64              : 
+      65          103 : export const InteractionSchema = Schema.Union([HttpInteractionSchema, WebSocketInteractionSchema]).pipe(
+      66           33 :   Schema.toTaggedUnion("transport"),
+      67            3 : )
+      68              : export type Interaction = Schema.Schema.Type<typeof InteractionSchema>
+      69              : 
+      70           63 : export const isHttpInteraction = InteractionSchema.guards.http
+      71              : 
+      72           73 : export const isWebSocketInteraction = InteractionSchema.guards.websocket
+      73              : 
+      74           87 : export const httpInteractions = (interactions: ReadonlyArray<Interaction>) => interactions.filter(isHttpInteraction)
+      75              : 
+      76            0 : export const webSocketInteractions = (interactions: ReadonlyArray<Interaction>) =>
+      77            2 :   interactions.filter(isWebSocketInteraction)
+      78              : 
+      79           46 : export const CassetteSchema = Schema.Struct({
+      80           29 :   version: Schema.Literal(1),
+      81           52 :   metadata: Schema.optional(CassetteMetadataSchema),
+      82           46 :   interactions: Schema.Array(InteractionSchema),
+      83            3 : })
+      84              : export type Cassette = Schema.Schema.Type<typeof CassetteSchema>
+      85              : 
+      86           71 : export const decodeCassette = Schema.decodeUnknownSync(CassetteSchema)
+      87           63 : export const encodeCassette = Schema.encodeSync(CassetteSchema)
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/socket.ts.gcov.html b/packages/core/http-recorder/src/socket.ts.gcov.html new file mode 100644 index 00000000..9182b210 --- /dev/null +++ b/packages/core/http-recorder/src/socket.ts.gcov.html @@ -0,0 +1,402 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src/socket.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/src - socket.tsCoverageTotalHit
Test:opencode-lcov.infoLines:8.1 %27022
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           55 : import { NodeFileSystem } from "@effect/platform-node"
+       2           81 : import { Deferred, Effect, Exit, FiberSet, Layer, Ref, Scope, Semaphore } from "effect"
+       3           48 : import { Socket } from "effect/unstable/socket"
+       4           49 : import * as CassetteService from "./cassette.js"
+       5           71 : import { canonicalizeJson, decodeJson, safeText } from "./matching.js"
+       6           65 : import { makeReplayState, resolveAutoMode } from "./recorder.js"
+       7           37 : import { make, type Redactor } from "./redactor.js"
+       8           52 : import { webSocketInteractions } from "./schema.js"
+       9              : import type {
+      10              :   RecorderOptions,
+      11              :   WebSocketEvent,
+      12              :   WebSocketInteraction,
+      13              :   WebSocketRecorderOptions,
+      14              :   WebSocketRequest,
+      15              : } from "./types.js"
+      16              : 
+      17              : interface ActiveReplay {
+      18              :   readonly interaction: WebSocketInteraction
+      19              :   readonly progress: Ref.Ref<{ readonly position: number; readonly changed: Deferred.Deferred<void> }>
+      20              :   readonly writeLock: Semaphore.Semaphore
+      21              :   readonly closed: Ref.Ref<boolean>
+      22              : }
+      23              : 
+      24              : interface ActiveRecording {
+      25              :   readonly events: Array<WebSocketEvent>
+      26              :   readonly eventLock: Semaphore.Semaphore
+      27              :   readonly accepting: Ref.Ref<boolean>
+      28              :   opened: boolean
+      29              :   valid: boolean
+      30              : }
+      31              : 
+      32              : type Frame = string | Uint8Array
+      33              : 
+      34            0 : const encodeEvent = (direction: "client" | "server", message: Frame): WebSocketEvent =>
+      35            0 :   typeof message === "string"
+      36            0 :     ? { direction, kind: "text", body: message }
+      37            2 :     : { direction, kind: "binary", body: Buffer.from(message).toString("base64"), bodyEncoding: "base64" }
+      38              : 
+      39            0 : const decodeEvent = (event: WebSocketEvent): Frame =>
+      40            2 :   event.kind === "text" ? event.body : new Uint8Array(Buffer.from(event.body, "base64"))
+      41              : 
+      42            0 : const redactEvent = (event: WebSocketEvent, redactor: Redactor): WebSocketEvent => {
+      43            0 :   if (event.kind === "binary") return event
+      44            0 :   const body =
+      45            0 :     event.direction === "client"
+      46            0 :       ? redactor.request({ method: "WEBSOCKET", url: "", headers: {}, body: event.body }).body
+      47            0 :       : redactor.response({ status: 101, headers: {}, body: event.body }).body
+      48            2 :   return { ...event, body }
+      49              : }
+      50              : 
+      51            0 : const comparable = (event: WebSocketEvent, asJson: boolean) => {
+      52            0 :   if (!asJson || event.kind === "binary") return JSON.stringify(canonicalizeJson(event))
+      53            0 :   const decoded = decodeJson(event.body)
+      54            0 :   return JSON.stringify(
+      55            0 :     canonicalizeJson({
+      56            0 :       ...event,
+      57            0 :       body: decoded._tag === "None" ? event.body : canonicalizeJson(decoded.value),
+      58            0 :     }),
+      59            2 :   )
+      60              : }
+      61              : 
+      62            0 : const assertEvent = (actual: WebSocketEvent, expected: WebSocketEvent | undefined, index: number, asJson: boolean) =>
+      63            0 :   Effect.sync(() => {
+      64            0 :     if (expected && comparable(actual, asJson) === comparable(expected, asJson)) return
+      65              :     throw new Error(`WebSocket event ${index + 1}: expected ${safeText(expected)}, received ${safeText(actual)}`)
+      66            2 :   })
+      67              : 
+      68            0 : const runHandler = <A, E, R>(handler: (value: A) => Effect.Effect<unknown, E, R> | void, value: A) =>
+      69            0 :   Effect.suspend(() => {
+      70            0 :     const result = handler(value)
+      71              :     return Effect.isEffect(result) ? Effect.asVoid(result) : Effect.void
+      72            2 :   })
+      73              : 
+      74            0 : const runReplay = <A, E, R>(
+      75            0 :   state: ActiveReplay,
+      76            0 :   handler: (value: A) => Effect.Effect<unknown, E, R> | void,
+      77            0 :   decode: (event: WebSocketEvent) => A,
+      78            0 :   onOpen: Effect.Effect<void> | undefined,
+      79            0 : ) =>
+      80            0 :   Effect.scoped(
+      81            0 :     Effect.gen(function* () {
+      82            0 :       const handlers = yield* FiberSet.make<unknown, E>()
+      83            0 :       const run = yield* FiberSet.runtime(handlers)<R>()
+      84            0 :       if (onOpen) yield* onOpen
+      85            0 : 
+      86            0 :       const drive = Effect.gen(function* () {
+      87            0 :         while (true) {
+      88            0 :           const current = yield* Ref.get(state.progress)
+      89            0 :           const event = state.interaction.events[current.position]
+      90            0 :           if (!event) return
+      91            0 :           if (yield* Ref.get(state.closed))
+      92            0 :             return yield* Effect.die(
+      93            0 :               new Error(
+      94            0 :                 `WebSocket closed with unconsumed events: used ${current.position} of ${state.interaction.events.length}`,
+      95            0 :               ),
+      96            0 :             )
+      97            0 :           if (event.direction === "server") {
+      98            0 :             yield* Ref.set(state.progress, {
+      99            0 :               position: current.position + 1,
+     100            0 :               changed: yield* Deferred.make<void>(),
+     101            0 :             })
+     102            0 :             run(runHandler(handler, decode(event)))
+     103            0 :             continue
+     104            0 :           }
+     105            0 :           yield* Deferred.await(current.changed)
+     106            0 :         }
+     107            0 :       })
+     108            0 : 
+     109            0 :       yield* drive.pipe(Effect.raceFirst(FiberSet.join(handlers)))
+     110            0 :       yield* FiberSet.awaitEmpty(handlers).pipe(Effect.raceFirst(FiberSet.join(handlers)))
+     111              :     }),
+     112            2 :   )
+     113              : 
+     114            0 : const openSnapshot = (request: WebSocketRequest, redactor: Redactor) => {
+     115            0 :   const snapshot = redactor.request({ method: "GET", url: request.url, headers: request.headers ?? {}, body: "" })
+     116            2 :   return { url: snapshot.url, headers: snapshot.headers }
+     117              : }
+     118              : 
+     119            0 : const makeRecordingSocket = (
+     120            0 :   upstream: Socket.Socket,
+     121            0 :   cassette: CassetteService.Interface,
+     122            0 :   name: string,
+     123            0 :   request: WebSocketRequest,
+     124            0 :   options: WebSocketRecorderOptions,
+     125            0 :   redactor: Redactor,
+     126            0 : ) =>
+     127            0 :   Effect.gen(function* () {
+     128            0 :     const active = yield* Ref.make<ActiveRecording | undefined>(undefined)
+     129            0 :     const writeLock = yield* Semaphore.make(1)
+     130            0 : 
+     131            0 :     return Socket.make({
+     132            0 :       runRaw: (handler, runOptions) =>
+     133            0 :         Effect.gen(function* () {
+     134            0 :           const state: ActiveRecording = {
+     135            0 :             events: [],
+     136            0 :             eventLock: yield* Semaphore.make(1),
+     137            0 :             accepting: yield* Ref.make(true),
+     138            0 :             opened: false,
+     139            0 :             valid: true,
+     140            0 :           }
+     141            0 :           const occupied = yield* Ref.modify(active, (current) => [current !== undefined, current ?? state])
+     142            0 :           if (occupied) return yield* Effect.die("Concurrent runs of a recorded WebSocket are not supported")
+     143            0 :           yield* upstream
+     144            0 :             .runRaw(
+     145            0 :               (message) => {
+     146            0 :                 if (!Ref.getUnsafe(state.accepting)) throw new Error("WebSocket received a frame after closing")
+     147            0 :                 state.events.push(redactEvent(encodeEvent("server", message), redactor))
+     148            0 :                 return handler(message)
+     149            0 :               },
+     150            0 :               {
+     151            0 :                 ...runOptions,
+     152            0 :                 onOpen: Effect.gen(function* () {
+     153            0 :                   state.opened = true
+     154            0 :                   if (runOptions?.onOpen) yield* runOptions.onOpen
+     155            0 :                 }),
+     156            0 :               },
+     157            0 :             )
+     158            0 :             .pipe(
+     159            0 :               Effect.onExit((exit) =>
+     160            0 :                 writeLock.withPermit(
+     161            0 :                   state.eventLock.withPermit(
+     162            0 :                     Effect.gen(function* () {
+     163            0 :                       yield* Ref.set(state.accepting, false)
+     164            0 :                       yield* Ref.set(active, undefined)
+     165            0 :                       if (!Exit.isSuccess(exit) || !state.opened || !state.valid) return
+     166            0 :                       yield* cassette
+     167            0 :                         .append(
+     168            0 :                           name,
+     169            0 :                           {
+     170            0 :                             transport: "websocket",
+     171            0 :                             open: openSnapshot(request, redactor),
+     172            0 :                             events: [...state.events],
+     173            0 :                           },
+     174            0 :                           options.metadata,
+     175            0 :                         )
+     176            0 :                         .pipe(Effect.orDie)
+     177            0 :                     }),
+     178            0 :                   ),
+     179            0 :                 ),
+     180            0 :               ),
+     181            0 :             )
+     182            0 :         }),
+     183            0 :       writer: upstream.writer.pipe(
+     184            0 :         Effect.map(
+     185            0 :           (write) => (message) =>
+     186            0 :             writeLock.withPermit(
+     187            0 :               Effect.gen(function* () {
+     188            0 :                 if (Socket.isCloseEvent(message)) return yield* write(message)
+     189            0 :                 const state = yield* Ref.get(active)
+     190            0 :                 if (!state || !(yield* Ref.get(state.accepting)))
+     191            0 :                   return yield* Effect.die("WebSocket writer used without an active socket run")
+     192            0 :                 const event = redactEvent(encodeEvent("client", message), redactor)
+     193            0 :                 yield* state.eventLock.withPermit(Effect.sync(() => state.events.push(event)))
+     194            0 :                 return yield* write(message).pipe(Effect.onError(() => Effect.sync(() => (state.valid = false))))
+     195            0 :               }),
+     196            0 :             ),
+     197            0 :         ),
+     198            0 :       ),
+     199              :     })
+     200            2 :   })
+     201              : 
+     202            0 : const makeReplaySocket = (
+     203            0 :   cassette: CassetteService.Interface,
+     204            0 :   name: string,
+     205            0 :   request: WebSocketRequest,
+     206            0 :   options: WebSocketRecorderOptions,
+     207            0 :   redactor: Redactor,
+     208            0 : ): Effect.Effect<Socket.Socket, never, Scope.Scope> =>
+     209            0 :   Effect.gen(function* () {
+     210            0 :     const replay = yield* makeReplayState(cassette, name, webSocketInteractions)
+     211            0 :     const active = yield* Ref.make<ActiveReplay | undefined>(undefined)
+     212            0 : 
+     213            0 :     return Socket.make({
+     214            0 :       runRaw: (handler, runOptions) =>
+     215            0 :         Effect.gen(function* () {
+     216            0 :           const claimed = yield* replay
+     217            0 :             .claim((interaction, index) =>
+     218            0 :               Effect.sync(() => {
+     219            0 :                 const incoming = openSnapshot(request, redactor)
+     220            0 :                 if (
+     221            0 :                   interaction &&
+     222            0 :                   JSON.stringify(canonicalizeJson(incoming)) === JSON.stringify(canonicalizeJson(interaction.open))
+     223            0 :                 )
+     224            0 :                   return
+     225            0 :                 throw new Error(
+     226            0 :                   `WebSocket open ${index + 1}: expected ${safeText(interaction?.open)}, received ${safeText(incoming)}`,
+     227            0 :                 )
+     228            0 :               }),
+     229            0 :             )
+     230            0 :             .pipe(Effect.orDie)
+     231            0 :           const progress = yield* Ref.make({ position: 0, changed: yield* Deferred.make<void>() })
+     232            0 :           const writeLock = yield* Semaphore.make(1)
+     233            0 :           const state = {
+     234            0 :             interaction: claimed.interaction,
+     235            0 :             progress,
+     236            0 :             writeLock,
+     237            0 :             closed: yield* Ref.make(false),
+     238            0 :           }
+     239            0 :           const occupied = yield* Ref.modify(active, (current) => [current !== undefined, current ?? state])
+     240            0 :           if (occupied) return yield* Effect.die("Concurrent runs of a replayed WebSocket are not supported")
+     241            0 :           yield* runReplay(state, handler, decodeEvent, runOptions?.onOpen).pipe(
+     242            0 :             Effect.ensuring(Ref.set(active, undefined)),
+     243            0 :           )
+     244            0 :         }),
+     245            0 :       writer: Effect.succeed((message) => {
+     246            0 :         return Ref.get(active).pipe(
+     247            0 :           Effect.flatMap((state) =>
+     248            0 :             state
+     249            0 :               ? state.writeLock.withPermit(
+     250            0 :                   Effect.gen(function* () {
+     251            0 :                     const current = yield* Ref.get(state.progress)
+     252            0 :                     if (Socket.isCloseEvent(message)) {
+     253            0 :                       yield* Ref.set(state.closed, true)
+     254            0 :                       yield* Deferred.succeed(current.changed, undefined)
+     255            0 :                       if (current.position === state.interaction.events.length) return
+     256            0 :                       return yield* Effect.die(
+     257            0 :                         new Error(
+     258            0 :                           `WebSocket closed with unconsumed events: used ${current.position} of ${state.interaction.events.length}`,
+     259            0 :                         ),
+     260            0 :                       )
+     261            0 :                     }
+     262            0 :                     const actual = redactEvent(encodeEvent("client", message), redactor)
+     263            0 :                     yield* assertEvent(
+     264            0 :                       actual,
+     265            0 :                       state.interaction.events[current.position],
+     266            0 :                       current.position,
+     267            0 :                       options.compareClientMessagesAsJson === true,
+     268            0 :                     )
+     269            0 :                     yield* Ref.set(state.progress, {
+     270            0 :                       position: current.position + 1,
+     271            0 :                       changed: yield* Deferred.make<void>(),
+     272            0 :                     })
+     273            0 :                     yield* Deferred.succeed(current.changed, undefined)
+     274            0 :                   }),
+     275            0 :                 )
+     276            0 :               : Effect.die("WebSocket writer used without an active socket run"),
+     277            0 :           ),
+     278            0 :         )
+     279            0 :       }),
+     280              :     })
+     281            2 :   })
+     282              : 
+     283            0 : const recordingLayer = (
+     284            0 :   name: string,
+     285            0 :   request: WebSocketRequest,
+     286            0 :   options: WebSocketRecorderOptions,
+     287            0 :   forcedMode?: "record" | "replay",
+     288            0 : ): Layer.Layer<Socket.Socket, never, Socket.Socket | CassetteService.Service> =>
+     289            0 :   Layer.effect(
+     290            0 :     Socket.Socket,
+     291            0 :     Effect.gen(function* () {
+     292            0 :       const upstream = yield* Socket.Socket
+     293            0 :       const cassette = yield* CassetteService.Service
+     294            0 :       const redactor = make(options.redact)
+     295            0 :       if ((forcedMode ?? (yield* resolveAutoMode(cassette, name))) === "record")
+     296            0 :         return yield* makeRecordingSocket(upstream, cassette, name, request, options, redactor)
+     297            0 :       return yield* makeReplaySocket(cassette, name, request, options, redactor)
+     298              :     }),
+     299            2 :   )
+     300              : 
+     301              : /**
+     302              :  * Wraps a provided `Socket.Socket` with cassette recording and replay.
+     303              :  *
+     304              :  * Supply the ordinary URL-bound Effect socket layer beneath this decorator.
+     305              :  * The cassette name identifies the connection; recorder configuration does not
+     306              :  * duplicate the transport URL.
+     307              :  */
+     308            0 : export const socket = (name: string, options: RecorderOptions = {}): Layer.Layer<Socket.Socket, never, Socket.Socket> =>
+     309            2 :   provideCassette(recordingLayer(name, { url: "" }, { ...options, compareClientMessagesAsJson: true }), options)
+     310              : 
+     311              : /** @internal */
+     312            0 : export const socketLayer = (
+     313            0 :   name: string,
+     314            0 :   request: WebSocketRequest,
+     315            0 :   options: WebSocketRecorderOptions & { readonly mode: "record" | "replay" },
+     316            0 : ): Layer.Layer<Socket.Socket, never, Socket.Socket> =>
+     317            2 :   provideCassette(recordingLayer(name, request, options, options.mode), options)
+     318              : 
+     319            0 : const provideCassette = (
+     320            0 :   layer: Layer.Layer<Socket.Socket, never, Socket.Socket | CassetteService.Service>,
+     321            0 :   options: WebSocketRecorderOptions,
+     322            0 : ) =>
+     323            0 :   layer.pipe(
+     324            0 :     Layer.provide(CassetteService.fileSystem({ directory: options.directory })),
+     325              :     Layer.provide(NodeFileSystem.layer),
+     326            1 :   )
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/http-recorder/src/websocket.ts.gcov.html b/packages/core/http-recorder/src/websocket.ts.gcov.html new file mode 100644 index 00000000..0be6e9e9 --- /dev/null +++ b/packages/core/http-recorder/src/websocket.ts.gcov.html @@ -0,0 +1,249 @@ + + + + + + + LCOV - opencode-lcov.info - ../http-recorder/src/websocket.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../http-recorder/src - websocket.tsCoverageTotalHit
Test:opencode-lcov.infoLines:8.1 %13511
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           81 : import { Effect, Option, Ref, Scope, Semaphore, Stream, SynchronizedRef } from "effect"
+       2              : import type { Headers } from "effect/unstable/http"
+       3              : import * as CassetteService from "./cassette.js"
+       4           71 : import { canonicalizeJson, decodeJson, safeText } from "./matching.js"
+       5           65 : import { makeReplayState, resolveAutoMode } from "./recorder.js"
+       6              : import type { RecordReplayMode } from "./internal-effect.js"
+       7           37 : import { make, type Redactor } from "./redactor.js"
+       8           52 : import { webSocketInteractions, type CassetteMetadata, type WebSocketEvent } from "./schema.js"
+       9              : 
+      10              : export interface WebSocketRequest {
+      11              :   readonly url: string
+      12              :   readonly headers: Headers.Headers
+      13              : }
+      14              : 
+      15              : export interface WebSocketConnection<E> {
+      16              :   readonly sendText: (message: string) => Effect.Effect<void, E>
+      17              :   readonly messages: Stream.Stream<string | Uint8Array, E>
+      18              :   readonly close: Effect.Effect<void>
+      19              : }
+      20              : 
+      21              : export interface WebSocketExecutor<E> {
+      22              :   readonly open: (request: WebSocketRequest) => Effect.Effect<WebSocketConnection<E>, E>
+      23              : }
+      24              : 
+      25              : export interface WebSocketRecordReplayOptions<E> {
+      26              :   readonly name: string
+      27              :   readonly mode?: RecordReplayMode
+      28              :   readonly metadata?: CassetteMetadata
+      29              :   readonly cassette: CassetteService.Interface
+      30              :   readonly live: WebSocketExecutor<E>
+      31              :   readonly redactor?: Redactor
+      32              :   readonly compareClientMessagesAsJson?: boolean
+      33              : }
+      34              : 
+      35            0 : const headersRecord = (headers: Headers.Headers): Record<string, string> =>
+      36            0 :   Object.fromEntries(
+      37            0 :     Object.entries(headers as Record<string, unknown>).filter(
+      38            0 :       (entry): entry is [string, string] => typeof entry[1] === "string",
+      39              :     ),
+      40            2 :   )
+      41              : 
+      42            0 : const textEvent = (direction: "client" | "server", body: string): WebSocketEvent => ({
+      43            0 :   direction,
+      44            0 :   kind: "text",
+      45              :   body,
+      46            2 : })
+      47              : 
+      48            0 : const decodeEvent = (event: WebSocketEvent) =>
+      49            2 :   event.kind === "text" ? event.body : new Uint8Array(Buffer.from(event.body, "base64"))
+      50              : 
+      51           19 : const jsonOrText = (value: string) => Option.match(decodeJson(value), { onNone: () => value, onSome: canonicalizeJson })
+      52              : 
+      53            0 : const assertClientEvent = (actual: string, expected: WebSocketEvent | undefined, index: number, asJson: boolean) =>
+      54            0 :   Effect.sync(() => {
+      55            0 :     const matches =
+      56            0 :       expected?.direction === "client" &&
+      57            0 :       expected.kind === "text" &&
+      58            0 :       JSON.stringify(asJson ? jsonOrText(actual) : actual) ===
+      59            0 :         JSON.stringify(asJson ? jsonOrText(expected.body) : expected.body)
+      60            0 :     if (matches) return
+      61              :     throw new Error(`WebSocket client frame ${index + 1}: expected ${safeText(expected)}, received ${safeText(actual)}`)
+      62            2 :   })
+      63              : 
+      64            0 : export const makeWebSocketExecutor = <E>(
+      65            0 :   options: WebSocketRecordReplayOptions<E>,
+      66            0 : ): Effect.Effect<WebSocketExecutor<E>, never, Scope.Scope> =>
+      67            0 :   Effect.gen(function* () {
+      68            0 :     const mode = options.mode ?? (yield* resolveAutoMode(options.cassette, options.name))
+      69            0 :     const redactor = options.redactor ?? make()
+      70            0 :     const openSnapshot = (request: WebSocketRequest) => {
+      71            0 :       const snapshot = redactor.request({
+      72            0 :         method: "GET",
+      73            0 :         url: request.url,
+      74            0 :         headers: headersRecord(request.headers),
+      75            0 :         body: "",
+      76            0 :       })
+      77            0 :       return { url: snapshot.url, headers: snapshot.headers }
+      78            0 :     }
+      79            0 :     const redactEvent = (event: WebSocketEvent) => {
+      80            0 :       if (event.kind === "binary") return event
+      81            0 :       const body =
+      82            0 :         event.direction === "client"
+      83            0 :           ? redactor.request({ method: "WEBSOCKET", url: "", headers: {}, body: event.body }).body
+      84            0 :           : redactor.response({ status: 101, headers: {}, body: event.body }).body
+      85            0 :       return { ...event, body }
+      86            0 :     }
+      87            0 : 
+      88            0 :     if (mode === "passthrough") return options.live
+      89            0 : 
+      90            0 :     if (mode === "record") {
+      91            0 :       return {
+      92            0 :         open: (request) =>
+      93            0 :           Effect.gen(function* () {
+      94            0 :             const events: WebSocketEvent[] = []
+      95            0 :             const connection = yield* options.live.open(request)
+      96            0 :             const closed = yield* Ref.make(false)
+      97            0 :             const closeLock = yield* Semaphore.make(1)
+      98            0 :             return {
+      99            0 :               sendText: (message) =>
+     100            0 :                 Effect.sync(() => events.push(redactEvent(textEvent("client", message)))).pipe(
+     101            0 :                   Effect.andThen(connection.sendText(message)),
+     102            0 :                 ),
+     103            0 :               messages: connection.messages.pipe(
+     104            0 :                 Stream.tap((message) =>
+     105            0 :                   Effect.sync(() =>
+     106            0 :                     events.push(
+     107            0 :                       typeof message === "string"
+     108            0 :                         ? redactEvent(textEvent("server", message))
+     109            0 :                         : {
+     110            0 :                             direction: "server",
+     111            0 :                             kind: "binary",
+     112            0 :                             body: Buffer.from(message).toString("base64"),
+     113            0 :                             bodyEncoding: "base64",
+     114            0 :                           },
+     115            0 :                     ),
+     116            0 :                   ),
+     117            0 :                 ),
+     118            0 :               ),
+     119            0 :               close: closeLock.withPermit(
+     120            0 :                 Effect.gen(function* () {
+     121            0 :                   if (yield* Ref.get(closed)) return
+     122            0 :                   yield* connection.close
+     123            0 :                   yield* options.cassette
+     124            0 :                     .append(
+     125            0 :                       options.name,
+     126            0 :                       { transport: "websocket", open: openSnapshot(request), events },
+     127            0 :                       options.metadata,
+     128            0 :                     )
+     129            0 :                     .pipe(Effect.orDie)
+     130            0 :                   yield* Ref.set(closed, true)
+     131            0 :                 }),
+     132            0 :               ),
+     133            0 :             }
+     134            0 :           }),
+     135            0 :       }
+     136            0 :     }
+     137            0 : 
+     138            0 :     const replay = yield* makeReplayState(options.cassette, options.name, webSocketInteractions)
+     139            0 :     return {
+     140            0 :       open: (request) =>
+     141            0 :         Effect.gen(function* () {
+     142            0 :           const claimed = yield* replay
+     143            0 :             .claim((interaction, index) =>
+     144            0 :               Effect.sync(() => {
+     145            0 :                 const incoming = canonicalizeJson(openSnapshot(request))
+     146            0 :                 if (interaction && JSON.stringify(incoming) === JSON.stringify(canonicalizeJson(interaction.open)))
+     147            0 :                   return
+     148            0 :                 throw new Error(`WebSocket open ${index + 1} does not match ${safeText(incoming)}`)
+     149            0 :               }),
+     150            0 :             )
+     151            0 :             .pipe(Effect.orDie)
+     152            0 :           const client = claimed.interaction.events.filter((event) => event.direction === "client")
+     153            0 :           const server = claimed.interaction.events.filter((event) => event.direction === "server")
+     154            0 :           const position = yield* SynchronizedRef.make(0)
+     155            0 :           return {
+     156            0 :             sendText: (message) =>
+     157            0 :               SynchronizedRef.updateEffect(position, (index) =>
+     158            0 :                 assertClientEvent(message, client[index], index, options.compareClientMessagesAsJson === true).pipe(
+     159            0 :                   Effect.as(index + 1),
+     160            0 :                 ),
+     161            0 :               ),
+     162            0 :             messages: Stream.fromIterable(server).pipe(Stream.map(decodeEvent)),
+     163            0 :             close: Effect.gen(function* () {
+     164            0 :               const used = yield* SynchronizedRef.get(position)
+     165            0 :               if (used !== client.length)
+     166            0 :                 return yield* Effect.die(
+     167            0 :                   new Error(`WebSocket client frame count: expected ${client.length}, received ${used}`),
+     168            0 :                 )
+     169            0 :             }),
+     170            0 :           }
+     171            0 :         }),
+     172              :     }
+     173            1 :   })
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/cache-policy.ts.gcov.html b/packages/core/llm/src/cache-policy.ts.gcov.html new file mode 100644 index 00000000..22d350e6 --- /dev/null +++ b/packages/core/llm/src/cache-policy.ts.gcov.html @@ -0,0 +1,187 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/cache-policy.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src - cache-policy.tsCoverageTotalHit
Test:opencode-lcov.infoLines:29.2 %6519
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : // Apply an `LLMRequest.cache` policy by injecting `CacheHint`s onto the parts
+       2              : // the policy designates. Runs once at compile time, before the per-protocol
+       3              : // body builder, so the existing inline-hint lowering path handles the rest.
+       4              : //
+       5              : // The default `"auto"` shape places one breakpoint at the last tool definition,
+       6              : // one at the last system part, and one at the latest user message. This
+       7              : // matches what production agent harnesses (LangChain's caching middleware,
+       8              : // kern-ai's 10x cost-reduction playbook) converge on for tool-use loops: the
+       9              : // latest user message stays put while a single turn explodes into many
+      10              : // assistant/tool round-trips, so caching at that boundary lets every
+      11              : // intra-turn API call hit the prefix.
+      12              : //
+      13              : // Manual `cache: CacheHint` placements on individual parts are preserved —
+      14              : // this function only fills gaps the caller left empty.
+      15           45 : import { CacheHint, type CachePolicy, type CachePolicyObject } from "./schema/options"
+      16           72 : import { LLMRequest, Message, ToolDefinition, type ContentPart } from "./schema/messages"
+      17              : 
+      18           15 : const AUTO: CachePolicyObject = {
+      19           14 :   tools: true,
+      20           15 :   system: true,
+      21           32 :   messages: "latest-user-message",
+      22            2 : }
+      23              : 
+      24           16 : const NONE: CachePolicyObject = {}
+      25              : 
+      26              : // Resolution rules:
+      27              : //   - undefined   → "auto" — caching is on by default. The math favors it:
+      28              : //                   Anthropic 5m-cache write is 1.25x base, read is 0.1x,
+      29              : //                   so a single reuse within 5 minutes already wins.
+      30              : //   - "auto"      → tools + system + latest user msg.
+      31              : //   - "none"      → no auto placement; manual `CacheHint`s still flow.
+      32              : //   - object form → exactly what the caller asked for.
+      33            0 : const resolve = (policy: CachePolicy | undefined): CachePolicyObject => {
+      34            0 :   if (policy === undefined || policy === "auto") return AUTO
+      35            0 :   if (policy === "none") return NONE
+      36            2 :   return policy
+      37              : }
+      38              : 
+      39              : // Protocols whose wire format ignores inline cache markers (OpenAI's implicit
+      40              : // prefix caching, Gemini's implicit + out-of-band CachedContent). Skip the
+      41              : // whole policy pass for these — emitting hints would be harmless but pointless.
+      42           82 : const RESPECTS_INLINE_HINTS = new Set(["anthropic-messages", "bedrock-converse"])
+      43              : 
+      44            0 : const makeHint = (ttlSeconds: number | undefined): CacheHint =>
+      45            2 :   ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" })
+      46              : 
+      47            0 : const markLastTool = (tools: ReadonlyArray<ToolDefinition>, hint: CacheHint): ReadonlyArray<ToolDefinition> => {
+      48            0 :   if (tools.length === 0) return tools
+      49            0 :   const last = tools.length - 1
+      50            0 :   if (tools[last]!.cache) return tools
+      51            2 :   return tools.map((tool, i) => (i === last ? new ToolDefinition({ ...tool, cache: hint }) : tool))
+      52              : }
+      53              : 
+      54            0 : const markLastSystem = (system: LLMRequest["system"], hint: CacheHint): LLMRequest["system"] => {
+      55            0 :   if (system.length === 0) return system
+      56            0 :   const last = system.length - 1
+      57            0 :   if (system[last]!.cache) return system
+      58            2 :   return system.map((part, i) => (i === last ? { ...part, cache: hint } : part))
+      59              : }
+      60              : 
+      61            0 : const lastIndexOfRole = (messages: ReadonlyArray<Message>, role: Message["role"]): number =>
+      62            2 :   messages.findLastIndex((m) => m.role === role)
+      63              : 
+      64              : // Mark the last text part of `messages[index]`. If no text part exists, mark
+      65              : // the last content part regardless of type — that's the breakpoint position
+      66              : // in tool-result-only messages too.
+      67            0 : const markMessageAt = (messages: ReadonlyArray<Message>, index: number, hint: CacheHint): ReadonlyArray<Message> => {
+      68            0 :   if (index < 0 || index >= messages.length) return messages
+      69            0 :   const target = messages[index]!
+      70            0 :   if (target.content.length === 0) return messages
+      71            0 :   const lastTextIndex = target.content.findLastIndex((part) => part.type === "text")
+      72            0 :   const markAt = lastTextIndex >= 0 ? lastTextIndex : target.content.length - 1
+      73            0 :   const existing = target.content[markAt]!
+      74            0 :   if ("cache" in existing && existing.cache) return messages
+      75            0 :   const nextContent = target.content.map((part, i) => (i === markAt ? ({ ...part, cache: hint } as ContentPart) : part))
+      76            0 :   const next = new Message({ ...target, content: nextContent })
+      77            0 :   // Single pass over `messages`, substituting the one updated entry. Long
+      78            0 :   // conversations call this on every request, so avoid `.map()` here — its
+      79            0 :   // closure dispatch and identity copies show up in profiling.
+      80            0 :   const result = messages.slice()
+      81            0 :   result[index] = next
+      82            2 :   return result
+      83              : }
+      84              : 
+      85            0 : const markMessages = (
+      86            0 :   messages: ReadonlyArray<Message>,
+      87            0 :   strategy: NonNullable<CachePolicyObject["messages"]>,
+      88            0 :   hint: CacheHint,
+      89            0 : ): ReadonlyArray<Message> => {
+      90            0 :   if (messages.length === 0) return messages
+      91            0 :   if (strategy === "latest-user-message") return markMessageAt(messages, lastIndexOfRole(messages, "user"), hint)
+      92            0 :   if (strategy === "latest-assistant") return markMessageAt(messages, lastIndexOfRole(messages, "assistant"), hint)
+      93            0 :   const start = Math.max(0, messages.length - strategy.tail)
+      94            0 :   let next = messages
+      95            0 :   for (let i = start; i < messages.length; i++) next = markMessageAt(next, i, hint)
+      96            2 :   return next
+      97              : }
+      98              : 
+      99           46 : export const applyCachePolicy = (request: LLMRequest): LLMRequest => {
+     100           72 :   if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request
+     101            0 :   const policy = resolve(request.cache)
+     102            0 :   if (!policy.tools && !policy.system && !policy.messages) return request
+     103              : 
+     104            0 :   const hint = makeHint(policy.ttlSeconds)
+     105            0 :   const tools = policy.tools ? markLastTool(request.tools, hint) : request.tools
+     106            0 :   const system = policy.system ? markLastSystem(request.system, hint) : request.system
+     107            0 :   const messages = policy.messages ? markMessages(request.messages, policy.messages, hint) : request.messages
+     108              : 
+     109            0 :   if (tools === request.tools && system === request.system && messages === request.messages) return request
+     110            1 :   return LLMRequest.update(request, { tools, system, messages })
+     111              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/index-sort-f.html b/packages/core/llm/src/index-sort-f.html new file mode 100644 index 00000000..cac4f7ad --- /dev/null +++ b/packages/core/llm/src/index-sort-f.html @@ -0,0 +1,152 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/srcCoverageTotalHit
Test:opencode-lcov.infoLines:34.6 %341118
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
cache-policy.ts +
29.2%29.2%
+
29.2 %6519
index.ts +
100.0%
+
100.0 %88
llm.ts +
36.5%36.5%
+
36.5 %10438
provider-error.ts +
94.9%94.9%
+
94.9 %3937
provider.ts +
50.0%50.0%
+
50.0 %42
tool-runtime.ts +
11.5%11.5%
+
11.5 %526
tool.ts +
11.6%11.6%
+
11.6 %698
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/index-sort-l.html b/packages/core/llm/src/index-sort-l.html new file mode 100644 index 00000000..7700d514 --- /dev/null +++ b/packages/core/llm/src/index-sort-l.html @@ -0,0 +1,152 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/srcCoverageTotalHit
Test:opencode-lcov.infoLines:34.6 %341118
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
tool-runtime.ts +
11.5%11.5%
+
11.5 %526
tool.ts +
11.6%11.6%
+
11.6 %698
cache-policy.ts +
29.2%29.2%
+
29.2 %6519
llm.ts +
36.5%36.5%
+
36.5 %10438
provider.ts +
50.0%50.0%
+
50.0 %42
provider-error.ts +
94.9%94.9%
+
94.9 %3937
index.ts +
100.0%
+
100.0 %88
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/index.html b/packages/core/llm/src/index.html new file mode 100644 index 00000000..4ae7b01b --- /dev/null +++ b/packages/core/llm/src/index.html @@ -0,0 +1,152 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/srcCoverageTotalHit
Test:opencode-lcov.infoLines:34.6 %341118
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
cache-policy.ts +
29.2%29.2%
+
29.2 %6519
index.ts +
100.0%
+
100.0 %88
llm.ts +
36.5%36.5%
+
36.5 %10438
provider-error.ts +
94.9%94.9%
+
94.9 %3937
provider.ts +
50.0%50.0%
+
50.0 %42
tool-runtime.ts +
11.5%11.5%
+
11.5 %526
tool.ts +
11.6%11.6%
+
11.6 %698
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/index.ts.gcov.html b/packages/core/llm/src/index.ts.gcov.html new file mode 100644 index 00000000..aa554510 --- /dev/null +++ b/packages/core/llm/src/index.ts.gcov.html @@ -0,0 +1,109 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/index.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src - index.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %88
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           43 : export { LLMClient } from "./route/client"
+       2           36 : export { Auth } from "./route/auth"
+       3           38 : export { Provider } from "./provider"
+       4           79 : export { isContextOverflow, isContextOverflowFailure } from "./provider-error"
+       5              : export type {
+       6              :   RouteModelInput,
+       7              :   RouteRoutedModelInput,
+       8              :   Interface as LLMClientShape,
+       9              :   Service as LLMClientService,
+      10              : } from "./route/client"
+      11           25 : export * from "./schema"
+      12           58 : export { Tool, ToolFailure, toDefinitions } from "./tool"
+      13           45 : export { ToolRuntime } from "./tool-runtime"
+      14              : export type { DispatchResult as ToolDispatchResult, ToolSettlement } from "./tool-runtime"
+      15              : export type {
+      16              :   AnyExecutableTool,
+      17              :   AnyTool,
+      18              :   ExecutableTool,
+      19              :   ExecutableTools,
+      20              :   Tool as ToolShape,
+      21              :   ToolExecute,
+      22              :   ToolExecuteContext,
+      23              :   ToolModelOutputInput,
+      24              :   Tools,
+      25              :   ToolSchema,
+      26              :   ToolToModelOutput,
+      27              : } from "./tool"
+      28           28 : export * as LLM from "./llm"
+      29              : export type {
+      30              :   Definition as ProviderDefinition,
+      31              :   ModelFactory as ProviderModelFactory,
+      32              :   ModelOptions as ProviderModelOptions,
+      33              : } from "./provider"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/llm.ts.gcov.html b/packages/core/llm/src/llm.ts.gcov.html new file mode 100644 index 00000000..d522f591 --- /dev/null +++ b/packages/core/llm/src/llm.ts.gcov.html @@ -0,0 +1,262 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/llm.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src - llm.tsCoverageTotalHit
Test:opencode-lcov.infoLines:36.5 %10438
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           80 : import { Effect, JsonSchema, Schema } from "effect"
+       2           86 : import { LLMClient } from "./route/client"
+       3          354 : import {
+       4              :   GenerationOptions,
+       5              :   HttpOptions,
+       6              :   InvalidProviderOutputReason,
+       7              :   LLMError,
+       8              :   LLMEvent,
+       9              :   LLMRequest,
+      10              :   LLMResponse,
+      11              :   Message,
+      12              :   type ModelInput as SchemaModelInput,
+      13              :   SystemPart,
+      14              :   ToolChoice,
+      15              :   ToolDefinition,
+      16              :   type ContentPart,
+      17              :   ToolResultPart,
+      18              : } from "./schema"
+      19          114 : import { make as makeTool, toDefinitions, type ToolSchema } from "./tool"
+      20              : 
+      21              : export type ModelInput = SchemaModelInput
+      22              : 
+      23              : export type MessageInput = Message.Input
+      24              : 
+      25              : export type ToolChoiceInput = ToolChoice.Input
+      26              : export type ToolChoiceMode = ToolChoice.Mode
+      27              : 
+      28              : export type ToolResultInput = Parameters<typeof ToolResultPart.make>[0]
+      29              : 
+      30              : /** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */
+      31              : export type RequestInput = Omit<
+      32              :   ConstructorParameters<typeof LLMRequest>[0],
+      33              :   "system" | "messages" | "tools" | "toolChoice" | "generation" | "http" | "providerOptions"
+      34              : > & {
+      35              :   readonly system?: string | SystemPart | ReadonlyArray<SystemPart>
+      36              :   readonly prompt?: string | ContentPart | ReadonlyArray<ContentPart>
+      37              :   readonly messages?: ReadonlyArray<Message | MessageInput>
+      38              :   readonly tools?: ReadonlyArray<ToolDefinition.Input>
+      39              :   readonly toolChoice?: ToolChoiceInput
+      40              :   readonly generation?: GenerationOptions.Input
+      41              :   readonly providerOptions?: ConstructorParameters<typeof LLMRequest>[0]["providerOptions"]
+      42              :   readonly http?: HttpOptions.Input
+      43              : }
+      44              : 
+      45           86 : export const generate = LLMClient.generate
+      46              : 
+      47           78 : export const stream = LLMClient.stream
+      48              : 
+      49            0 : export const requestInput = (input: LLMRequest): RequestInput => ({
+      50           24 :   ...LLMRequest.input(input),
+      51            5 : })
+      52              : 
+      53           71 : export const request = (input: RequestInput) => {
+      54           22 :   const {
+      55           52 :     system: requestSystem,
+      56           22 :     prompt,
+      57           26 :     messages,
+      58           20 :     tools,
+      59           68 :     toolChoice: requestToolChoice,
+      60           68 :     generation: requestGeneration,
+      61           88 :     providerOptions: requestProviderOptions,
+      62           50 :     http: requestHttp,
+      63           20 :     ...rest
+      64           16 :   } = input
+      65           60 :   return new LLMRequest({
+      66           18 :     ...rest,
+      67           92 :     system: SystemPart.content(requestSystem),
+      68          213 :     messages: [...(messages?.map(Message.make) ?? []), ...(prompt === undefined ? [] : [Message.user(prompt)])],
+      69           98 :     tools: tools?.map(ToolDefinition.make) ?? [],
+      70          163 :     toolChoice: requestToolChoice ? ToolChoice.make(requestToolChoice) : undefined,
+      71          205 :     generation: requestGeneration === undefined ? undefined : GenerationOptions.make(requestGeneration),
+      72           88 :     providerOptions: requestProviderOptions,
+      73          120 :     http: requestHttp === undefined ? undefined : HttpOptions.make(requestHttp),
+      74           10 :   })
+      75              : }
+      76              : 
+      77            0 : export const updateRequest = (input: LLMRequest, patch: Partial<RequestInput>) =>
+      78           49 :   request({ ...requestInput(input), ...patch })
+      79              : 
+      80          104 : const GENERATE_OBJECT_TOOL_NAME = "generate_object"
+      81              : 
+      82          188 : const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool."
+      83              : 
+      84              : type GenerateObjectBase = Omit<RequestInput, "tools" | "toolChoice" | "responseFormat">
+      85              : 
+      86              : export class GenerateObjectResponse<T> {
+      87            0 :   constructor(
+      88            0 :     readonly object: T,
+      89            0 :     readonly response: LLMResponse,
+      90            0 :   ) {}
+      91            0 : 
+      92            0 :   get events() {
+      93            0 :     return this.response.events
+      94            0 :   }
+      95            0 : 
+      96            0 :   get usage() {
+      97           32 :     return this.response.usage
+      98              :   }
+      99            2 : }
+     100              : 
+     101              : export interface GenerateObjectOptions<S extends ToolSchema<any>> extends GenerateObjectBase {
+     102              :   readonly schema: S
+     103              : }
+     104              : 
+     105              : export interface GenerateObjectDynamicOptions extends GenerateObjectBase {
+     106              :   /** Raw JSON Schema object describing the expected output shape. */
+     107              :   readonly jsonSchema: JsonSchema.JsonSchema
+     108              : }
+     109              : 
+     110            0 : const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
+     111            0 :   options: GenerateObjectBase,
+     112            0 :   tool: ReturnType<typeof makeTool>,
+     113            0 : ) {
+     114            0 :   const baseRequest = request(options)
+     115            0 :   const generateRequest = LLMRequest.update(baseRequest, {
+     116            0 :     tools: toDefinitions({ [GENERATE_OBJECT_TOOL_NAME]: tool }),
+     117            0 :     toolChoice: ToolChoice.named(GENERATE_OBJECT_TOOL_NAME),
+     118            0 :   })
+     119            0 :   const response = yield* LLMClient.generate(generateRequest)
+     120            0 :   const call = response.toolCalls.find(
+     121            0 :     (event) => LLMEvent.is.toolCall(event) && event.name === GENERATE_OBJECT_TOOL_NAME,
+     122            0 :   )
+     123            0 :   if (!call || !LLMEvent.is.toolCall(call))
+     124            0 :     return yield* new LLMError({
+     125            0 :       module: "LLM",
+     126            0 :       method: "generateObject",
+     127            0 :       reason: new InvalidProviderOutputReason({
+     128            0 :         message: `generateObject: model did not call the forced \`${GENERATE_OBJECT_TOOL_NAME}\` tool`,
+     129            0 :       }),
+     130            0 :     })
+     131            0 :   const object = yield* tool._decode(call.input).pipe(
+     132            0 :     Effect.mapError(
+     133            0 :       (error) =>
+     134            0 :         new LLMError({
+     135            0 :           module: "LLM",
+     136            0 :           method: "generateObject",
+     137            0 :           reason: new InvalidProviderOutputReason({
+     138            0 :             message: `generateObject: tool input failed schema decode: ${error.message}`,
+     139            0 :           }),
+     140            0 :         }),
+     141            0 :     ),
+     142            0 :   )
+     143           53 :   return new GenerateObjectResponse(object, response)
+     144            6 : })
+     145              : 
+     146              : /**
+     147              :  * Run a model and decode its output against `schema`. Works on every protocol
+     148              :  * because it forces a synthetic tool call internally — provider-native JSON
+     149              :  * modes are intentionally avoided so behaviour is uniform.
+     150              :  *
+     151              :  * Two input modes:
+     152              :  *
+     153              :  * 1. `schema: EffectSchema<T>` — `.object` is decoded and typed as `T`.
+     154              :  *    Decode failures surface as `LLMError`.
+     155              :  * 2. `jsonSchema: JsonSchema.JsonSchema` — `.object` is `unknown`. Use when
+     156              :  *    the schema is only available at runtime (MCP, plugin manifests). Caller validates.
+     157              :  */
+     158              : export function generateObject<S extends ToolSchema<any>>(
+     159              :   options: GenerateObjectOptions<S>,
+     160              : ): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, LLMError>
+     161              : export function generateObject(
+     162              :   options: GenerateObjectDynamicOptions,
+     163              : ): Effect.Effect<GenerateObjectResponse<unknown>, LLMError>
+     164            0 : export function generateObject(options: GenerateObjectOptions<ToolSchema<any>> | GenerateObjectDynamicOptions) {
+     165            0 :   if ("schema" in options) {
+     166            0 :     const { schema, ...rest } = options
+     167            0 :     return runGenerateObject(
+     168            0 :       rest,
+     169            0 :       makeTool({
+     170            0 :         description: GENERATE_OBJECT_TOOL_DESCRIPTION,
+     171            0 :         parameters: schema,
+     172            0 :         success: Schema.Unknown as ToolSchema<unknown>,
+     173            0 :         execute: () => Effect.void,
+     174            0 :       }),
+     175            0 :     )
+     176            0 :   }
+     177            0 :   const { jsonSchema, ...rest } = options
+     178            0 :   return runGenerateObject(
+     179            0 :     rest,
+     180            0 :     makeTool({
+     181            0 :       description: GENERATE_OBJECT_TOOL_DESCRIPTION,
+     182            0 :       jsonSchema,
+     183            0 :       execute: () => Effect.void,
+     184            0 :     }),
+     185            4 :   )
+     186              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/anthropic-messages.ts.gcov.html b/packages/core/llm/src/protocols/anthropic-messages.ts.gcov.html new file mode 100644 index 00000000..19482b78 --- /dev/null +++ b/packages/core/llm/src/protocols/anthropic-messages.ts.gcov.html @@ -0,0 +1,931 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols/anthropic-messages.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocols - anthropic-messages.tsCoverageTotalHit
Test:opencode-lcov.infoLines:33.0 %660218
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           40 : import { Effect, Schema } from "effect"
+       2           40 : import { Route } from "../route/client"
+       3           37 : import { Auth } from "../route/auth"
+       4           45 : import { Endpoint } from "../route/endpoint"
+       5           43 : import { Framing } from "../route/framing"
+       6           45 : import { Protocol } from "../route/protocol"
+       7           45 : import {
+       8              :   LLMEvent,
+       9              :   Usage,
+      10              :   type CacheHint,
+      11              :   type FinishReason,
+      12              :   type JsonSchema,
+      13              :   type LLMRequest,
+      14              :   type MediaPart,
+      15              :   type ProviderMetadata,
+      16              :   type ToolCallPart,
+      17              :   type ToolDefinition,
+      18              :   type ToolContent,
+      19              :   type ToolResultPart,
+      20              : } from "../schema"
+      21           83 : import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
+      22           54 : import { isContextOverflow } from "../provider-error"
+      23           39 : import * as Cache from "./utils/cache"
+      24           46 : import { Lifecycle } from "./utils/lifecycle"
+      25           59 : import { ToolSchemaProjection } from "./utils/tool-schema"
+      26           49 : import { ToolStream } from "./utils/tool-stream"
+      27              : 
+      28           37 : const ADAPTER = "anthropic-messages"
+      29           63 : export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
+      30           32 : export const PATH = "/messages"
+      31              : 
+      32              : // =============================================================================
+      33              : // Request Body Schema
+      34              : // =============================================================================
+      35           46 : const AnthropicCacheControl = Schema.Struct({
+      36           32 :   type: Schema.tag("ephemeral"),
+      37           52 :   ttl: Schema.optional(Schema.Literals(["5m", "1h"])),
+      38            3 : })
+      39              : 
+      40           43 : const AnthropicTextBlock = Schema.Struct({
+      41           27 :   type: Schema.tag("text"),
+      42           22 :   text: Schema.String,
+      43           54 :   cache_control: Schema.optional(AnthropicCacheControl),
+      44            3 : })
+      45              : type AnthropicTextBlock = Schema.Schema.Type<typeof AnthropicTextBlock>
+      46              : 
+      47           44 : const AnthropicImageBlock = Schema.Struct({
+      48           28 :   type: Schema.tag("image"),
+      49           27 :   source: Schema.Struct({
+      50           31 :     type: Schema.tag("base64"),
+      51           30 :     media_type: Schema.String,
+      52           21 :     data: Schema.String,
+      53            5 :   }),
+      54           54 :   cache_control: Schema.optional(AnthropicCacheControl),
+      55            3 : })
+      56              : type AnthropicImageBlock = Schema.Schema.Type<typeof AnthropicImageBlock>
+      57              : 
+      58           47 : const AnthropicThinkingBlock = Schema.Struct({
+      59           31 :   type: Schema.tag("thinking"),
+      60           26 :   thinking: Schema.String,
+      61           44 :   signature: Schema.optional(Schema.String),
+      62           54 :   cache_control: Schema.optional(AnthropicCacheControl),
+      63            3 : })
+      64              : 
+      65           46 : const AnthropicToolUseBlock = Schema.Struct({
+      66           31 :   type: Schema.tag("tool_use"),
+      67           20 :   id: Schema.String,
+      68           22 :   name: Schema.String,
+      69           24 :   input: Schema.Unknown,
+      70           54 :   cache_control: Schema.optional(AnthropicCacheControl),
+      71            3 : })
+      72              : type AnthropicToolUseBlock = Schema.Schema.Type<typeof AnthropicToolUseBlock>
+      73              : 
+      74           52 : const AnthropicServerToolUseBlock = Schema.Struct({
+      75           38 :   type: Schema.tag("server_tool_use"),
+      76           20 :   id: Schema.String,
+      77           22 :   name: Schema.String,
+      78           24 :   input: Schema.Unknown,
+      79           54 :   cache_control: Schema.optional(AnthropicCacheControl),
+      80            3 : })
+      81              : type AnthropicServerToolUseBlock = Schema.Schema.Type<typeof AnthropicServerToolUseBlock>
+      82              : 
+      83              : // Server tool result blocks: web_search_tool_result, code_execution_tool_result,
+      84              : // and web_fetch_tool_result. The provider executes the tool and inlines the
+      85              : // structured result into the assistant turn — there is no client tool_result
+      86              : // round-trip. We round-trip the structured `content` payload as opaque JSON so
+      87              : // the next request can echo it back when continuing the conversation.
+      88           56 : const AnthropicServerToolResultType = Schema.Literals([
+      89           27 :   "web_search_tool_result",
+      90           31 :   "code_execution_tool_result",
+      91           24 :   "web_fetch_tool_result",
+      92            3 : ])
+      93              : type AnthropicServerToolResultType = Schema.Schema.Type<typeof AnthropicServerToolResultType>
+      94              : 
+      95           55 : const AnthropicServerToolResultBlock = Schema.Struct({
+      96           38 :   type: AnthropicServerToolResultType,
+      97           29 :   tool_use_id: Schema.String,
+      98           26 :   content: Schema.Unknown,
+      99           54 :   cache_control: Schema.optional(AnthropicCacheControl),
+     100            3 : })
+     101              : type AnthropicServerToolResultBlock = Schema.Schema.Type<typeof AnthropicServerToolResultBlock>
+     102              : 
+     103              : // Anthropic accepts either a plain string or an ordered array of text/image
+     104              : // blocks inside `tool_result.content`. The array form is required when a tool
+     105              : // returns image bytes (screenshot, image search, etc.) so they can be passed
+     106              : // to the model as proper image inputs instead of being JSON-stringified into
+     107              : // the prompt — which silently inflates context by megabytes and can push the
+     108              : // conversation over the model's token limit.
+     109           91 : const AnthropicToolResultContent = Schema.Union([AnthropicTextBlock, AnthropicImageBlock])
+     110              : 
+     111           49 : const AnthropicToolResultBlock = Schema.Struct({
+     112           34 :   type: Schema.tag("tool_result"),
+     113           29 :   tool_use_id: Schema.String,
+     114           83 :   content: Schema.Union([Schema.String, Schema.Array(AnthropicToolResultContent)]),
+     115           44 :   is_error: Schema.optional(Schema.Boolean),
+     116           54 :   cache_control: Schema.optional(AnthropicCacheControl),
+     117            3 : })
+     118              : 
+     119          109 : const AnthropicUserBlock = Schema.Union([AnthropicTextBlock, AnthropicImageBlock, AnthropicToolResultBlock])
+     120              : type AnthropicUserBlock = Schema.Schema.Type<typeof AnthropicUserBlock>
+     121           47 : const AnthropicAssistantBlock = Schema.Union([
+     122           21 :   AnthropicTextBlock,
+     123           25 :   AnthropicThinkingBlock,
+     124           24 :   AnthropicToolUseBlock,
+     125           30 :   AnthropicServerToolUseBlock,
+     126           31 :   AnthropicServerToolResultBlock,
+     127            3 : ])
+     128              : type AnthropicAssistantBlock = Schema.Schema.Type<typeof AnthropicAssistantBlock>
+     129              : type AnthropicToolResultBlock = Schema.Schema.Type<typeof AnthropicToolResultBlock>
+     130              : 
+     131           40 : const AnthropicMessage = Schema.Union([
+     132           93 :   Schema.Struct({ role: Schema.Literal("user"), content: Schema.Array(AnthropicUserBlock) }),
+     133          103 :   Schema.Struct({ role: Schema.Literal("assistant"), content: Schema.Array(AnthropicAssistantBlock) }),
+     134           93 :   Schema.Struct({ role: Schema.Literal("system"), content: Schema.Array(AnthropicTextBlock) }),
+     135           38 : ]).pipe(Schema.toTaggedUnion("role"))
+     136              : type AnthropicMessage = Schema.Schema.Type<typeof AnthropicMessage>
+     137              : 
+     138           38 : const AnthropicTool = Schema.Struct({
+     139           22 :   name: Schema.String,
+     140           29 :   description: Schema.String,
+     141           27 :   input_schema: JsonObject,
+     142           54 :   cache_control: Schema.optional(AnthropicCacheControl),
+     143            3 : })
+     144              : type AnthropicTool = Schema.Schema.Type<typeof AnthropicTool>
+     145              : 
+     146           43 : const AnthropicToolChoice = Schema.Union([
+     147           60 :   Schema.Struct({ type: Schema.Literals(["auto", "any"]) }),
+     148           65 :   Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }),
+     149            3 : ])
+     150              : 
+     151           42 : const AnthropicThinking = Schema.Struct({
+     152           30 :   type: Schema.tag("enabled"),
+     153           29 :   budget_tokens: Schema.Number,
+     154            3 : })
+     155              : 
+     156           30 : const AnthropicBodyFields = {
+     157           23 :   model: Schema.String,
+     158           44 :   system: optionalArray(AnthropicTextBlock),
+     159           43 :   messages: Schema.Array(AnthropicMessage),
+     160           38 :   tools: optionalArray(AnthropicTool),
+     161           52 :   tool_choice: Schema.optional(AnthropicToolChoice),
+     162           31 :   stream: Schema.Literal(true),
+     163           28 :   max_tokens: Schema.Number,
+     164           46 :   temperature: Schema.optional(Schema.Number),
+     165           40 :   top_p: Schema.optional(Schema.Number),
+     166           40 :   top_k: Schema.optional(Schema.Number),
+     167           47 :   stop_sequences: optionalArray(Schema.String),
+     168           45 :   thinking: Schema.optional(AnthropicThinking),
+     169            2 : }
+     170           65 : const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields)
+     171              : export type AnthropicMessagesBody = Schema.Schema.Type<typeof AnthropicMessagesBody>
+     172              : 
+     173           39 : const AnthropicUsage = Schema.Struct({
+     174           47 :   input_tokens: Schema.optional(Schema.Number),
+     175           48 :   output_tokens: Schema.optional(Schema.Number),
+     176           59 :   cache_creation_input_tokens: optionalNull(Schema.Number),
+     177           53 :   cache_read_input_tokens: optionalNull(Schema.Number),
+     178            3 : })
+     179              : type AnthropicUsage = Schema.Schema.Type<typeof AnthropicUsage>
+     180              : 
+     181           45 : const AnthropicStreamBlock = Schema.Struct({
+     182           22 :   type: Schema.String,
+     183           37 :   id: Schema.optional(Schema.String),
+     184           39 :   name: Schema.optional(Schema.String),
+     185           39 :   text: Schema.optional(Schema.String),
+     186           43 :   thinking: Schema.optional(Schema.String),
+     187           44 :   signature: Schema.optional(Schema.String),
+     188           41 :   input: Schema.optional(Schema.Unknown),
+     189              :   // *_tool_result blocks arrive whole as content_block_start (no streaming
+     190              :   // delta) with the structured payload in `content` and the originating
+     191              :   // server_tool_use id in `tool_use_id`.
+     192           46 :   tool_use_id: Schema.optional(Schema.String),
+     193           41 :   content: Schema.optional(Schema.Unknown),
+     194            3 : })
+     195              : 
+     196           45 : const AnthropicStreamDelta = Schema.Struct({
+     197           39 :   type: Schema.optional(Schema.String),
+     198           39 :   text: Schema.optional(Schema.String),
+     199           43 :   thinking: Schema.optional(Schema.String),
+     200           47 :   partial_json: Schema.optional(Schema.String),
+     201           44 :   signature: Schema.optional(Schema.String),
+     202           43 :   stop_reason: optionalNull(Schema.String),
+     203           43 :   stop_sequence: optionalNull(Schema.String),
+     204            3 : })
+     205              : 
+     206           39 : const AnthropicEvent = Schema.Struct({
+     207           22 :   type: Schema.String,
+     208           40 :   index: Schema.optional(Schema.Number),
+     209           86 :   message: Schema.optional(Schema.Struct({ usage: Schema.optional(AnthropicUsage) })),
+     210           55 :   content_block: Schema.optional(AnthropicStreamBlock),
+     211           47 :   delta: Schema.optional(AnthropicStreamDelta),
+     212           41 :   usage: Schema.optional(AnthropicUsage),
+     213              :   // `type` and `message` are both required per Anthropic's spec, but
+     214              :   // OpenAI-compatible proxies and gateway translations occasionally drop one
+     215              :   // or the other; mark them optional so a partial payload still parses and
+     216              :   // the parser can fall back to whichever field is populated.
+     217           23 :   error: Schema.optional(
+     218           96 :     Schema.Struct({ type: Schema.optional(Schema.String), message: Schema.optional(Schema.String) }),
+     219            2 :   ),
+     220            3 : })
+     221              : type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent>
+     222              : 
+     223              : interface ParserState {
+     224              :   readonly tools: ToolStream.State<number>
+     225              :   readonly usage?: Usage
+     226              :   readonly lifecycle: Lifecycle.State
+     227              : }
+     228              : 
+     229           46 : const invalid = ProviderShared.invalidRequest
+     230              : 
+     231              : // =============================================================================
+     232              : // Request Lowering
+     233              : // =============================================================================
+     234              : // Anthropic accepts at most 4 explicit cache_control breakpoints per request,
+     235              : // across `tools`, `system`, and `messages`. Beyond the cap the API returns a
+     236              : // 400 — so the lowering layer counts emitted markers and silently drops any
+     237              : // that exceed it.
+     238           35 : const ANTHROPIC_BREAKPOINT_CAP = 4
+     239              : 
+     240           43 : const EPHEMERAL_5M = { type: "ephemeral" as const }
+     241           54 : const EPHEMERAL_1H = { type: "ephemeral" as const, ttl: "1h" as const }
+     242              : 
+     243            0 : const cacheControl = (breakpoints: Cache.Breakpoints, cache: CacheHint | undefined) => {
+     244            0 :   if (cache?.type !== "ephemeral" && cache?.type !== "persistent") return undefined
+     245            0 :   if (breakpoints.remaining <= 0) {
+     246            0 :     breakpoints.dropped += 1
+     247            0 :     return undefined
+     248            0 :   }
+     249            0 :   breakpoints.remaining -= 1
+     250            2 :   return Cache.ttlBucket(cache.ttlSeconds) === "1h" ? EPHEMERAL_1H : EPHEMERAL_5M
+     251              : }
+     252              : 
+     253           26 : const anthropicMetadata = (metadata: Record<string, unknown>): ProviderMetadata => ({ anthropic: metadata })
+     254              : 
+     255            0 : const signatureFromMetadata = (metadata: ProviderMetadata | undefined): string | undefined => {
+     256            0 :   const anthropic = metadata?.anthropic
+     257            0 :   if (!ProviderShared.isRecord(anthropic)) return undefined
+     258            2 :   return typeof anthropic.signature === "string" ? anthropic.signature : undefined
+     259              : }
+     260              : 
+     261            0 : const lowerTool = (breakpoints: Cache.Breakpoints, tool: ToolDefinition, inputSchema: JsonSchema): AnthropicTool => ({
+     262            0 :   name: tool.name,
+     263            0 :   description: tool.description,
+     264            0 :   input_schema: inputSchema,
+     265              :   cache_control: cacheControl(breakpoints, tool.cache),
+     266            2 : })
+     267              : 
+     268            0 : const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
+     269            0 :   ProviderShared.matchToolChoice("Anthropic Messages", toolChoice, {
+     270            0 :     auto: () => ({ type: "auto" as const }),
+     271            0 :     none: () => undefined,
+     272            0 :     required: () => ({ type: "any" as const }),
+     273              :     tool: (name) => ({ type: "tool" as const, name }),
+     274            2 :   })
+     275              : 
+     276            0 : const lowerToolCall = (part: ToolCallPart): AnthropicToolUseBlock => ({
+     277            0 :   type: "tool_use",
+     278            0 :   id: part.id,
+     279            0 :   name: part.name,
+     280              :   input: part.input,
+     281            2 : })
+     282              : 
+     283            0 : const lowerServerToolCall = (part: ToolCallPart): AnthropicServerToolUseBlock => ({
+     284            0 :   type: "server_tool_use",
+     285            0 :   id: part.id,
+     286            0 :   name: part.name,
+     287              :   input: part.input,
+     288            2 : })
+     289              : 
+     290              : // Server tool result blocks are typed by name. Anthropic ships three today;
+     291              : // extend this list when new server tools land. The block content is the
+     292              : // structured payload returned by the provider, which we round-trip as-is.
+     293            0 : const serverToolResultType = (name: string): AnthropicServerToolResultType | undefined => {
+     294            0 :   if (name === "web_search") return "web_search_tool_result"
+     295            0 :   if (name === "code_execution") return "code_execution_tool_result"
+     296            0 :   if (name === "web_fetch") return "web_fetch_tool_result"
+     297            2 :   return undefined
+     298              : }
+     299              : 
+     300            0 : const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult")(function* (part: ToolResultPart) {
+     301            0 :   const wireType = serverToolResultType(part.name)
+     302            0 :   if (!wireType)
+     303            0 :     return yield* invalid(`Anthropic Messages does not know how to round-trip server tool result for ${part.name}`)
+     304              :   return { type: wireType, tool_use_id: part.id, content: part.result.value } satisfies AnthropicServerToolResultBlock
+     305            3 : })
+     306              : 
+     307            0 : const lowerImage = Effect.fn("AnthropicMessages.lowerImage")(function* (part: MediaPart) {
+     308            0 :   const media = yield* ProviderShared.validateMedia(
+     309            0 :     "Anthropic Messages",
+     310            0 :     part,
+     311            0 :     new Set<string>(ProviderShared.IMAGE_MIMES),
+     312            0 :   )
+     313            0 :   return {
+     314            0 :     type: "image" as const,
+     315            0 :     source: {
+     316            0 :       type: "base64" as const,
+     317            0 :       media_type: media.mime,
+     318            0 :       data: media.base64,
+     319            0 :     },
+     320              :   } satisfies AnthropicImageBlock
+     321            3 : })
+     322              : 
+     323              : // Tool results may carry structured text/images. Keep media as provider-native
+     324              : // content instead of JSON-stringifying base64 into a prompt string.
+     325            0 : const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* (
+     326            0 :   item: ToolContent,
+     327            0 : ) {
+     328            0 :   if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock
+     329            0 :   const media = yield* ProviderShared.validateToolFile(
+     330            0 :     "Anthropic Messages",
+     331            0 :     item,
+     332            0 :     new Set<string>(ProviderShared.IMAGE_MIMES),
+     333            0 :   )
+     334            0 :   return {
+     335            0 :     type: "image" as const,
+     336            0 :     source: {
+     337            0 :       type: "base64" as const,
+     338            0 :       media_type: media.mime,
+     339            0 :       data: media.base64,
+     340            0 :     },
+     341              :   } satisfies AnthropicImageBlock
+     342            3 : })
+     343              : 
+     344            0 : const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultContent")(function* (part: ToolResultPart) {
+     345            0 :   // Text / json / error results stay as a string for backward compatibility
+     346            0 :   // with existing cassettes and provider expectations.
+     347            0 :   if (part.result.type !== "content") return ProviderShared.toolResultText(part)
+     348            0 :   // Preserve the narrowed array element type when compiled through a consumer package.
+     349            0 :   const content: ReadonlyArray<ToolContent> = part.result.value
+     350              :   return yield* Effect.forEach(content, lowerToolResultContentItem)
+     351            3 : })
+     352              : 
+     353              : // Mid-conversation system messages are a native Claude API feature only for
+     354              : // Opus 4.8. Other Anthropic models intentionally use the same visible wrapped-
+     355              : // user fallback as non-Anthropic routes rather than sending a role they reject.
+     356           36 : const supportsNativeSystemUpdates = (request: LLMRequest) => String(request.model.id) === "claude-opus-4-8"
+     357              : 
+     358            0 : const endsInServerToolUse = (message: LLMRequest["messages"][number]) => {
+     359            0 :   const last = message.content.at(-1)
+     360            2 :   return message.role === "assistant" && last?.type === "tool-call" && last.providerExecuted === true
+     361              : }
+     362              : 
+     363            0 : const canUseNativeSystemUpdate = (messages: LLMRequest["messages"], index: number) => {
+     364            0 :   const previous = messages[index - 1]
+     365            0 :   const next = messages[index + 1]
+     366            0 :   return (
+     367            0 :     previous !== undefined &&
+     368            0 :     previous.role !== "system" &&
+     369            0 :     (previous.role === "user" || previous.role === "tool" || endsInServerToolUse(previous)) &&
+     370            0 :     next?.role !== "system" &&
+     371            2 :     (next === undefined || next.role === "assistant")
+     372              :   )
+     373              : }
+     374              : 
+     375            0 : const splitsLocalToolResults = (messages: LLMRequest["messages"], index: number) => {
+     376            0 :   const pending = new Set<string>()
+     377            0 :   for (const message of messages.slice(0, index)) {
+     378            0 :     for (const part of message.content) {
+     379            0 :       if (message.role === "assistant" && part.type === "tool-call" && part.providerExecuted !== true)
+     380            0 :         pending.add(part.id)
+     381            0 :       if (message.role === "tool" && part.type === "tool-result") pending.delete(part.id)
+     382            0 :     }
+     383            0 :   }
+     384            2 :   return pending.size > 0
+     385              : }
+     386              : 
+     387            0 : const lowerNativeSystemUpdate = Effect.fn("AnthropicMessages.lowerNativeSystemUpdate")(function* (
+     388            0 :   message: LLMRequest["messages"][number],
+     389            0 :   breakpoints: Cache.Breakpoints,
+     390            0 : ) {
+     391            0 :   const content = yield* ProviderShared.systemUpdateText("Anthropic Messages", message)
+     392            0 :   return {
+     393            0 :     role: "system" as const,
+     394            0 :     content: content.map((part) => ({
+     395            0 :       type: "text" as const,
+     396            0 :       text: part.text,
+     397            0 :       cache_control: cacheControl(breakpoints, part.cache),
+     398            0 :     })),
+     399              :   }
+     400            3 : })
+     401              : 
+     402            0 : const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
+     403            0 :   request: LLMRequest,
+     404            0 :   breakpoints: Cache.Breakpoints,
+     405            0 : ) {
+     406            0 :   const messages: AnthropicMessage[] = []
+     407            0 : 
+     408            0 :   for (const [index, message] of request.messages.entries()) {
+     409            0 :     if (message.role === "system") {
+     410            0 :       if (splitsLocalToolResults(request.messages, index))
+     411            0 :         return yield* invalid("Anthropic Messages system updates cannot split a local tool call from its tool result")
+     412            0 :       if (supportsNativeSystemUpdates(request) && canUseNativeSystemUpdate(request.messages, index)) {
+     413            0 :         messages.push(yield* lowerNativeSystemUpdate(message, breakpoints))
+     414            0 :         continue
+     415            0 :       }
+     416            0 :       const part = yield* ProviderShared.wrappedSystemUpdate("Anthropic Messages", message)
+     417            0 :       const block = { type: "text" as const, text: part.text, cache_control: cacheControl(breakpoints, part.cache) }
+     418            0 :       const previous = messages.at(-1)
+     419            0 :       if (previous?.role === "user")
+     420            0 :         messages[messages.length - 1] = { role: "user", content: [...previous.content, block] }
+     421            0 :       else messages.push({ role: "user", content: [block] })
+     422            0 :       continue
+     423            0 :     }
+     424            0 : 
+     425            0 :     if (message.role === "user") {
+     426            0 :       const content: AnthropicUserBlock[] = []
+     427            0 :       for (const part of message.content) {
+     428            0 :         if (part.type === "text") {
+     429            0 :           content.push({ type: "text", text: part.text, cache_control: cacheControl(breakpoints, part.cache) })
+     430            0 :           continue
+     431            0 :         }
+     432            0 :         if (part.type === "media") {
+     433            0 :           content.push(yield* lowerImage(part))
+     434            0 :           continue
+     435            0 :         }
+     436            0 :         return yield* ProviderShared.unsupportedContent("Anthropic Messages", "user", ["text", "media"])
+     437            0 :       }
+     438            0 :       messages.push({ role: "user", content })
+     439            0 :       continue
+     440            0 :     }
+     441            0 : 
+     442            0 :     if (message.role === "assistant") {
+     443            0 :       const content: AnthropicAssistantBlock[] = []
+     444            0 :       for (const part of message.content) {
+     445            0 :         if (part.type === "text") {
+     446            0 :           content.push({ type: "text", text: part.text, cache_control: cacheControl(breakpoints, part.cache) })
+     447            0 :           continue
+     448            0 :         }
+     449            0 :         if (part.type === "reasoning") {
+     450            0 :           content.push({
+     451            0 :             type: "thinking",
+     452            0 :             thinking: part.text,
+     453            0 :             signature: part.encrypted ?? signatureFromMetadata(part.providerMetadata),
+     454            0 :           })
+     455            0 :           continue
+     456            0 :         }
+     457            0 :         if (part.type === "tool-call") {
+     458            0 :           content.push(part.providerExecuted ? lowerServerToolCall(part) : lowerToolCall(part))
+     459            0 :           continue
+     460            0 :         }
+     461            0 :         if (part.type === "tool-result" && part.providerExecuted) {
+     462            0 :           content.push(yield* lowerServerToolResult(part))
+     463            0 :           continue
+     464            0 :         }
+     465            0 :         return yield* invalid(
+     466            0 :           `Anthropic Messages assistant messages only support text, reasoning, and tool-call content for now`,
+     467            0 :         )
+     468            0 :       }
+     469            0 :       messages.push({ role: "assistant", content })
+     470            0 :       continue
+     471            0 :     }
+     472            0 : 
+     473            0 :     const content: AnthropicToolResultBlock[] = []
+     474            0 :     for (const part of message.content) {
+     475            0 :       if (!ProviderShared.supportsContent(part, ["tool-result"]))
+     476            0 :         return yield* ProviderShared.unsupportedContent("Anthropic Messages", "tool", ["tool-result"])
+     477            0 :       content.push({
+     478            0 :         type: "tool_result",
+     479            0 :         tool_use_id: part.id,
+     480            0 :         content: yield* lowerToolResultContent(part),
+     481            0 :         is_error: part.result.type === "error" ? true : undefined,
+     482            0 :         cache_control: cacheControl(breakpoints, part.cache),
+     483            0 :       })
+     484            0 :     }
+     485            0 :     messages.push({ role: "user", content })
+     486            0 :   }
+     487            0 : 
+     488              :   return messages
+     489            3 : })
+     490              : 
+     491           25 : const anthropicOptions = (request: LLMRequest) => request.providerOptions?.anthropic
+     492              : 
+     493            0 : const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (request: LLMRequest) {
+     494            0 :   const thinking = anthropicOptions(request)?.thinking
+     495            0 :   if (!ProviderShared.isRecord(thinking) || thinking.type !== "enabled") return undefined
+     496            0 :   const budget =
+     497            0 :     typeof thinking.budgetTokens === "number"
+     498            0 :       ? thinking.budgetTokens
+     499            0 :       : typeof thinking.budget_tokens === "number"
+     500            0 :         ? thinking.budget_tokens
+     501            0 :         : undefined
+     502            0 :   if (budget === undefined) return yield* invalid("Anthropic thinking provider option requires budgetTokens")
+     503              :   return { type: "enabled" as const, budget_tokens: budget }
+     504            3 : })
+     505              : 
+     506            0 : const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) {
+     507            0 :   const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined
+     508            0 :   const generation = request.generation
+     509            0 :   const toolSchemaCompatibility = request.model.compatibility?.toolSchema
+     510            0 :   const outputLimit = request.model.defaults?.limits?.output ?? request.model.route.defaults.limits?.output ?? 4096
+     511            0 :   // Allocate the 4-breakpoint budget in invalidation order: tools → system →
+     512            0 :   // messages. Tools live highest in the cache hierarchy, so when callers
+     513            0 :   // over-mark we keep their tool hints and shed the message-tail ones first.
+     514            0 :   const breakpoints = Cache.newBreakpoints(ANTHROPIC_BREAKPOINT_CAP)
+     515            0 :   const tools =
+     516            0 :     request.tools.length === 0 || request.toolChoice?.type === "none"
+     517            0 :       ? undefined
+     518            0 :       : request.tools.map((tool) =>
+     519            0 :           lowerTool(
+     520            0 :             breakpoints,
+     521            0 :             tool,
+     522            0 :             ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility),
+     523            0 :           ),
+     524            0 :         )
+     525            0 :   const system =
+     526            0 :     request.system.length === 0
+     527            0 :       ? undefined
+     528            0 :       : request.system.map((part) => ({
+     529            0 :           type: "text" as const,
+     530            0 :           text: part.text,
+     531            0 :           cache_control: cacheControl(breakpoints, part.cache),
+     532            0 :         }))
+     533            0 :   const messages = yield* lowerMessages(request, breakpoints)
+     534            0 :   if (breakpoints.dropped > 0) {
+     535            0 :     yield* Effect.logWarning(
+     536            0 :       `Anthropic Messages: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${ANTHROPIC_BREAKPOINT_CAP} per request.`,
+     537            0 :     )
+     538            0 :   }
+     539            0 :   return {
+     540            0 :     model: request.model.id,
+     541            0 :     system,
+     542            0 :     messages,
+     543            0 :     tools,
+     544            0 :     tool_choice: toolChoice,
+     545            0 :     stream: true as const,
+     546            0 :     max_tokens: generation?.maxTokens ?? outputLimit,
+     547            0 :     temperature: generation?.temperature,
+     548            0 :     top_p: generation?.topP,
+     549            0 :     top_k: generation?.topK,
+     550            0 :     stop_sequences: generation?.stop,
+     551            0 :     thinking: yield* lowerThinking(request),
+     552              :   }
+     553            3 : })
+     554              : 
+     555              : // =============================================================================
+     556              : // Stream Parsing
+     557              : // =============================================================================
+     558            0 : const mapFinishReason = (reason: string | null | undefined): FinishReason => {
+     559            0 :   if (reason === "end_turn" || reason === "stop_sequence" || reason === "pause_turn") return "stop"
+     560            0 :   if (reason === "max_tokens") return "length"
+     561            0 :   if (reason === "tool_use") return "tool-calls"
+     562            0 :   if (reason === "refusal") return "content-filter"
+     563            2 :   return "unknown"
+     564              : }
+     565              : 
+     566              : // Anthropic reports the non-overlapping breakdown natively — its
+     567              : // `input_tokens` is the *non-cached* count per the Messages API docs, with
+     568              : // cache reads and writes as separate fields. We sum them to derive the
+     569              : // inclusive `inputTokens` the rest of the contract expects. Extended
+     570              : // thinking tokens are *not* broken out by Anthropic — they're billed as
+     571              : // part of `output_tokens`, so `reasoningTokens` stays `undefined` and
+     572              : // `outputTokens` carries the combined total.
+     573            0 : const mapUsage = (usage: AnthropicUsage | undefined): Usage | undefined => {
+     574            0 :   if (!usage) return undefined
+     575            0 :   const nonCached = usage.input_tokens
+     576            0 :   const cacheRead = usage.cache_read_input_tokens ?? undefined
+     577            0 :   const cacheWrite = usage.cache_creation_input_tokens ?? undefined
+     578            0 :   const inputTokens = ProviderShared.sumTokens(nonCached, cacheRead, cacheWrite)
+     579            0 :   return new Usage({
+     580            0 :     inputTokens,
+     581            0 :     outputTokens: usage.output_tokens,
+     582            0 :     nonCachedInputTokens: nonCached,
+     583            0 :     cacheReadInputTokens: cacheRead,
+     584            0 :     cacheWriteInputTokens: cacheWrite,
+     585            0 :     totalTokens: ProviderShared.totalTokens(inputTokens, usage.output_tokens, undefined),
+     586            0 :     providerMetadata: { anthropic: usage },
+     587            2 :   })
+     588              : }
+     589              : 
+     590              : // Anthropic emits usage on `message_start` and again on `message_delta` — the
+     591              : // final delta carries the authoritative totals. Right-biased merge: each
+     592              : // field prefers `right` when defined, falls back to `left`. `inputTokens` is
+     593              : // recomputed from the merged breakdown so the inclusive total stays
+     594              : // consistent with `nonCached + cacheRead + cacheWrite`.
+     595            0 : const mergeUsage = (left: Usage | undefined, right: Usage | undefined) => {
+     596            0 :   if (!left) return right
+     597            0 :   if (!right) return left
+     598            0 :   const nonCachedInputTokens = right.nonCachedInputTokens ?? left.nonCachedInputTokens
+     599            0 :   const cacheReadInputTokens = right.cacheReadInputTokens ?? left.cacheReadInputTokens
+     600            0 :   const cacheWriteInputTokens = right.cacheWriteInputTokens ?? left.cacheWriteInputTokens
+     601            0 :   const inputTokens = ProviderShared.sumTokens(nonCachedInputTokens, cacheReadInputTokens, cacheWriteInputTokens)
+     602            0 :   const outputTokens = right.outputTokens ?? left.outputTokens
+     603            0 :   return new Usage({
+     604            0 :     inputTokens,
+     605            0 :     outputTokens,
+     606            0 :     nonCachedInputTokens,
+     607            0 :     cacheReadInputTokens,
+     608            0 :     cacheWriteInputTokens,
+     609            0 :     totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined),
+     610            0 :     providerMetadata: {
+     611            0 :       anthropic: {
+     612            0 :         ...left.providerMetadata?.["anthropic"],
+     613            0 :         ...right.providerMetadata?.["anthropic"],
+     614            0 :       },
+     615            0 :     },
+     616            2 :   })
+     617              : }
+     618              : 
+     619              : // Server tool result blocks come whole in `content_block_start` (no streaming
+     620              : // delta sequence). We convert the payload to a `tool-result` event with
+     621              : // `providerExecuted: true`. The runtime appends it to the assistant message
+     622              : // for round-trip; downstream consumers can inspect `result.value` for the
+     623              : // structured payload.
+     624           35 : const SERVER_TOOL_RESULT_NAMES: Record<AnthropicServerToolResultType, string> = {
+     625           39 :   web_search_tool_result: "web_search",
+     626           47 :   code_execution_tool_result: "code_execution",
+     627           35 :   web_fetch_tool_result: "web_fetch",
+     628            2 : }
+     629              : 
+     630           31 : const isServerToolResultType = (type: string): type is AnthropicServerToolResultType => type in SERVER_TOOL_RESULT_NAMES
+     631              : 
+     632            0 : const serverToolResultEvent = (block: NonNullable<AnthropicEvent["content_block"]>): LLMEvent | undefined => {
+     633            0 :   if (!block.type || !isServerToolResultType(block.type)) return undefined
+     634            0 :   const errorPayload =
+     635            0 :     typeof block.content === "object" && block.content !== null && "type" in block.content
+     636            0 :       ? String((block.content as Record<string, unknown>).type)
+     637            0 :       : ""
+     638            0 :   const isError = errorPayload.endsWith("_tool_result_error")
+     639            0 :   return LLMEvent.toolResult({
+     640            0 :     id: block.tool_use_id ?? "",
+     641            0 :     name: SERVER_TOOL_RESULT_NAMES[block.type],
+     642            0 :     result: isError ? { type: "error", value: block.content } : { type: "json", value: block.content },
+     643            0 :     providerExecuted: true,
+     644            0 :     providerMetadata: anthropicMetadata({ blockType: block.type }),
+     645            2 :   })
+     646              : }
+     647              : 
+     648              : type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
+     649              : 
+     650           21 : const NO_EVENTS: StepResult["1"] = []
+     651              : 
+     652            0 : const onMessageStart = (state: ParserState, event: AnthropicEvent): StepResult => {
+     653            0 :   const usage = mapUsage(event.message?.usage)
+     654            2 :   return [usage ? { ...state, usage: mergeUsage(state.usage, usage) } : state, NO_EVENTS]
+     655              : }
+     656              : 
+     657            0 : const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepResult => {
+     658            0 :   const block = event.content_block
+     659            0 :   if (!block) return [state, NO_EVENTS]
+     660            0 : 
+     661            0 :   if ((block.type === "tool_use" || block.type === "server_tool_use") && event.index !== undefined) {
+     662            0 :     const events: LLMEvent[] = []
+     663            0 :     const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
+     664            0 :     return [
+     665            0 :       {
+     666            0 :         ...state,
+     667            0 :         lifecycle,
+     668            0 :         tools: ToolStream.start(state.tools, event.index, {
+     669            0 :           id: block.id ?? String(event.index),
+     670            0 :           name: block.name ?? "",
+     671            0 :           providerExecuted: block.type === "server_tool_use",
+     672            0 :         }),
+     673            0 :       },
+     674            0 :       [...events, LLMEvent.toolInputStart({ id: block.id ?? String(event.index), name: block.name ?? "" })],
+     675            0 :     ]
+     676            0 :   }
+     677            0 : 
+     678            0 :   if (block.type === "text" && block.text) {
+     679            0 :     const events: LLMEvent[] = []
+     680            0 :     return [
+     681            0 :       { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, `text-${event.index ?? 0}`, block.text) },
+     682            0 :       events,
+     683            0 :     ]
+     684            0 :   }
+     685            0 : 
+     686            0 :   if (block.type === "thinking" && block.thinking) {
+     687            0 :     const events: LLMEvent[] = []
+     688            0 :     return [
+     689            0 :       {
+     690            0 :         ...state,
+     691            0 :         lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${event.index ?? 0}`, block.thinking),
+     692            0 :       },
+     693            0 :       events,
+     694            0 :     ]
+     695            0 :   }
+     696            0 : 
+     697            0 :   const result = serverToolResultEvent(block)
+     698            0 :   if (!result) return [state, NO_EVENTS]
+     699            0 :   const events: LLMEvent[] = []
+     700            2 :   return [{ ...state, lifecycle: Lifecycle.stepStart(state.lifecycle, events) }, [...events, result]]
+     701              : }
+     702              : 
+     703            0 : const onContentBlockDelta = Effect.fn("AnthropicMessages.onContentBlockDelta")(function* (
+     704            0 :   state: ParserState,
+     705            0 :   event: AnthropicEvent,
+     706            0 : ) {
+     707            0 :   const delta = event.delta
+     708            0 : 
+     709            0 :   if (delta?.type === "text_delta" && delta.text) {
+     710            0 :     const events: LLMEvent[] = []
+     711            0 :     return [
+     712            0 :       { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, `text-${event.index ?? 0}`, delta.text) },
+     713            0 :       events,
+     714            0 :     ] satisfies StepResult
+     715            0 :   }
+     716            0 : 
+     717            0 :   if (delta?.type === "thinking_delta" && delta.thinking) {
+     718            0 :     const events: LLMEvent[] = []
+     719            0 :     return [
+     720            0 :       {
+     721            0 :         ...state,
+     722            0 :         lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${event.index ?? 0}`, delta.thinking),
+     723            0 :       },
+     724            0 :       events,
+     725            0 :     ] satisfies StepResult
+     726            0 :   }
+     727            0 : 
+     728            0 :   if (delta?.type === "signature_delta" && delta.signature) {
+     729            0 :     const events: LLMEvent[] = []
+     730            0 :     return [
+     731            0 :       {
+     732            0 :         ...state,
+     733            0 :         lifecycle: Lifecycle.reasoningEnd(
+     734            0 :           state.lifecycle,
+     735            0 :           events,
+     736            0 :           `reasoning-${event.index ?? 0}`,
+     737            0 :           anthropicMetadata({ signature: delta.signature }),
+     738            0 :         ),
+     739            0 :       },
+     740            0 :       events,
+     741            0 :     ] satisfies StepResult
+     742            0 :   }
+     743            0 : 
+     744            0 :   if (delta?.type === "input_json_delta" && event.index !== undefined) {
+     745            0 :     if (!delta.partial_json) return [state, NO_EVENTS] satisfies StepResult
+     746            0 :     const result = ToolStream.appendExisting(
+     747            0 :       ADAPTER,
+     748            0 :       state.tools,
+     749            0 :       event.index,
+     750            0 :       delta.partial_json,
+     751            0 :       "Anthropic Messages tool argument delta is missing its tool call",
+     752            0 :     )
+     753            0 :     if (ToolStream.isError(result)) return yield* result
+     754            0 :     const events: LLMEvent[] = []
+     755            0 :     const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle
+     756            0 :     events.push(...result.events)
+     757            0 :     return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult
+     758            0 :   }
+     759            0 : 
+     760              :   return [state, NO_EVENTS] satisfies StepResult
+     761            3 : })
+     762              : 
+     763            0 : const onContentBlockStop = Effect.fn("AnthropicMessages.onContentBlockStop")(function* (
+     764            0 :   state: ParserState,
+     765            0 :   event: AnthropicEvent,
+     766            0 : ) {
+     767            0 :   if (event.index === undefined) return [state, NO_EVENTS] satisfies StepResult
+     768            0 :   const result = yield* ToolStream.finish(ADAPTER, state.tools, event.index)
+     769            0 :   const events: LLMEvent[] = []
+     770            0 :   const resultEvents = result.events ?? []
+     771            0 :   const lifecycle = resultEvents.length
+     772            0 :     ? Lifecycle.stepStart(state.lifecycle, events)
+     773            0 :     : Lifecycle.reasoningEnd(
+     774            0 :         Lifecycle.textEnd(state.lifecycle, events, `text-${event.index}`),
+     775            0 :         events,
+     776            0 :         `reasoning-${event.index}`,
+     777            0 :       )
+     778            0 :   events.push(...resultEvents)
+     779              :   return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult
+     780            3 : })
+     781              : 
+     782            0 : const onMessageDelta = (state: ParserState, event: AnthropicEvent): StepResult => {
+     783            0 :   const usage = mergeUsage(state.usage, mapUsage(event.usage))
+     784            0 :   const events: LLMEvent[] = []
+     785            0 :   const lifecycle = Lifecycle.finish(state.lifecycle, events, {
+     786            0 :     reason: mapFinishReason(event.delta?.stop_reason),
+     787            0 :     usage,
+     788            0 :     providerMetadata: event.delta?.stop_sequence
+     789            0 :       ? anthropicMetadata({ stopSequence: event.delta.stop_sequence })
+     790            0 :       : undefined,
+     791            0 :   })
+     792            2 :   return [{ ...state, lifecycle, usage }, events]
+     793              : }
+     794              : 
+     795              : // Prefix `error.type` so overloads, rate limits, and quota errors are visible
+     796              : // even when the provider message is generic or empty.
+     797            0 : const providerErrorMessage = (event: AnthropicEvent): string => {
+     798            0 :   const type = event.error?.type
+     799            0 :   const message = event.error?.message
+     800            0 :   if (type && message) return `${type}: ${message}`
+     801            2 :   return message || type || "Anthropic Messages stream error"
+     802              : }
+     803              : 
+     804            0 : const onError = (state: ParserState, event: AnthropicEvent): StepResult => [
+     805            0 :   state,
+     806            0 :   [
+     807            0 :     LLMEvent.providerError({
+     808            0 :       message: providerErrorMessage(event),
+     809            0 :       classification: isContextOverflow(event.error?.message ?? "") ? "context-overflow" : undefined,
+     810            0 :     }),
+     811              :   ],
+     812            2 : ]
+     813              : 
+     814            0 : const step = (state: ParserState, event: AnthropicEvent) => {
+     815            0 :   if (event.type === "message_start") return Effect.succeed(onMessageStart(state, event))
+     816            0 :   if (event.type === "content_block_start") return Effect.succeed(onContentBlockStart(state, event))
+     817            0 :   if (event.type === "content_block_delta") return onContentBlockDelta(state, event)
+     818            0 :   if (event.type === "content_block_stop") return onContentBlockStop(state, event)
+     819            0 :   if (event.type === "message_delta") return Effect.succeed(onMessageDelta(state, event))
+     820            0 :   if (event.type === "error") return Effect.succeed(onError(state, event))
+     821            2 :   return Effect.succeed<StepResult>([state, NO_EVENTS])
+     822              : }
+     823              : 
+     824              : // =============================================================================
+     825              : // Protocol And Anthropic Route
+     826              : // =============================================================================
+     827              : /**
+     828              :  * The Anthropic Messages protocol — request body construction, body schema,
+     829              :  * and the streaming-event state machine. Used by native Anthropic Cloud and
+     830              :  * (once registered) Vertex Anthropic / Bedrock-hosted Anthropic passthrough.
+     831              :  */
+     832           40 : export const protocol = Protocol.make({
+     833           14 :   id: ADAPTER,
+     834           11 :   body: {
+     835           34 :     schema: AnthropicMessagesBody,
+     836           19 :     from: fromRequest,
+     837            4 :   },
+     838           13 :   stream: {
+     839           46 :     event: Protocol.jsonEvent(AnthropicEvent),
+     840           13 :     initial: () => ({ tools: ToolStream.empty<number>(), lifecycle: Lifecycle.initial() }),
+     841            6 :     step,
+     842            2 :   },
+     843            3 : })
+     844              : 
+     845           34 : export const route = Route.make({
+     846           14 :   id: ADAPTER,
+     847           24 :   provider: "anthropic",
+     848           11 :   protocol,
+     849           63 :   endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
+     850           18 :   auth: Auth.none,
+     851           23 :   framing: Framing.sse,
+     852            9 :   headers: () => ({ "anthropic-version": "2023-06-01" }),
+     853            3 : })
+     854              : 
+     855           57 : export * as AnthropicMessages from "./anthropic-messages"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/index-sort-f.html b/packages/core/llm/src/protocols/index-sort-f.html new file mode 100644 index 00000000..b1c33bca --- /dev/null +++ b/packages/core/llm/src/protocols/index-sort-f.html @@ -0,0 +1,134 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocolsCoverageTotalHit
Test:opencode-lcov.infoLines:43.9 %2021888
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
anthropic-messages.ts +
33.0%33.0%
+
33.0 %660218
openai-chat.ts +
70.6%70.6%
+
70.6 %384271
openai-compatible-chat.ts +
100.0%
+
100.0 %1212
openai-responses.ts +
40.1%40.1%
+
40.1 %793318
shared.ts +
40.1%40.1%
+
40.1 %17269
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/index-sort-l.html b/packages/core/llm/src/protocols/index-sort-l.html new file mode 100644 index 00000000..32135deb --- /dev/null +++ b/packages/core/llm/src/protocols/index-sort-l.html @@ -0,0 +1,134 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocolsCoverageTotalHit
Test:opencode-lcov.infoLines:43.9 %2021888
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
anthropic-messages.ts +
33.0%33.0%
+
33.0 %660218
shared.ts +
40.1%40.1%
+
40.1 %17269
openai-responses.ts +
40.1%40.1%
+
40.1 %793318
openai-chat.ts +
70.6%70.6%
+
70.6 %384271
openai-compatible-chat.ts +
100.0%
+
100.0 %1212
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/index.html b/packages/core/llm/src/protocols/index.html new file mode 100644 index 00000000..d888713e --- /dev/null +++ b/packages/core/llm/src/protocols/index.html @@ -0,0 +1,134 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocolsCoverageTotalHit
Test:opencode-lcov.infoLines:43.9 %2021888
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
anthropic-messages.ts +
33.0%33.0%
+
33.0 %660218
openai-chat.ts +
70.6%70.6%
+
70.6 %384271
openai-compatible-chat.ts +
100.0%
+
100.0 %1212
openai-responses.ts +
40.1%40.1%
+
40.1 %793318
shared.ts +
40.1%40.1%
+
40.1 %17269
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/openai-chat.ts.gcov.html b/packages/core/llm/src/protocols/openai-chat.ts.gcov.html new file mode 100644 index 00000000..ea6c871d --- /dev/null +++ b/packages/core/llm/src/protocols/openai-chat.ts.gcov.html @@ -0,0 +1,582 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols/openai-chat.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocols - openai-chat.tsCoverageTotalHit
Test:opencode-lcov.infoLines:70.6 %384271
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           40 : import { Effect, Schema } from "effect"
+       2           40 : import { Route } from "../route/client"
+       3           37 : import { Auth } from "../route/auth"
+       4           45 : import { Endpoint } from "../route/endpoint"
+       5           51 : import { HttpTransport } from "../route/transport"
+       6           45 : import { Protocol } from "../route/protocol"
+       7           34 : import {
+       8              :   LLMEvent,
+       9              :   Usage,
+      10              :   type FinishReason,
+      11              :   type JsonSchema,
+      12              :   type LLMRequest,
+      13              :   type MediaPart,
+      14              :   type ReasoningPart,
+      15              :   type TextPart,
+      16              :   type ToolCallPart,
+      17              :   type ToolDefinition,
+      18              :   type ToolContent,
+      19              : } from "../schema"
+      20           93 : import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
+      21           55 : import { OpenAIOptions } from "./utils/openai-options"
+      22           46 : import { Lifecycle } from "./utils/lifecycle"
+      23           59 : import { ToolSchemaProjection } from "./utils/tool-schema"
+      24           49 : import { ToolStream } from "./utils/tool-stream"
+      25              : 
+      26           30 : const ADAPTER = "openai-chat"
+      27           56 : const IMAGE_MIMES = new Set<string>(ProviderShared.IMAGE_MIMES)
+      28           60 : export const DEFAULT_BASE_URL = "https://api.openai.com/v1"
+      29           40 : export const PATH = "/chat/completions"
+      30              : 
+      31              : // =============================================================================
+      32              : // Request Body Schema
+      33              : // =============================================================================
+      34              : // The body schema is the provider-native JSON body. `fromRequest` below builds
+      35              : // this shape from the common `LLMRequest`, then `Route.make` validates and
+      36              : // JSON-encodes it before transport.
+      37           43 : const OpenAIChatFunction = Schema.Struct({
+      38           22 :   name: Schema.String,
+      39           29 :   description: Schema.String,
+      40           23 :   parameters: JsonObject,
+      41            3 : })
+      42              : 
+      43           39 : const OpenAIChatTool = Schema.Struct({
+      44           31 :   type: Schema.tag("function"),
+      45           29 :   function: OpenAIChatFunction,
+      46            3 : })
+      47              : type OpenAIChatTool = Schema.Schema.Type<typeof OpenAIChatTool>
+      48              : 
+      49           52 : const OpenAIChatAssistantToolCall = Schema.Struct({
+      50           20 :   id: Schema.String,
+      51           31 :   type: Schema.tag("function"),
+      52           29 :   function: Schema.Struct({
+      53           24 :     name: Schema.String,
+      54           26 :     arguments: Schema.String,
+      55            3 :   }),
+      56            3 : })
+      57              : type OpenAIChatAssistantToolCall = Schema.Schema.Type<typeof OpenAIChatAssistantToolCall>
+      58              : 
+      59           45 : const OpenAIChatUserContent = Schema.Union([
+      60           71 :   Schema.Struct({ type: Schema.Literal("text"), text: Schema.String }),
+      61           19 :   Schema.Struct({
+      62           38 :     type: Schema.Literal("image_url"),
+      63           50 :     image_url: Schema.Struct({ url: Schema.String }),
+      64            3 :   }),
+      65            3 : ])
+      66              : 
+      67           41 : const OpenAIChatMessage = Schema.Union([
+      68           76 :   Schema.Struct({ role: Schema.Literal("system"), content: Schema.String }),
+      69           19 :   Schema.Struct({
+      70           33 :     role: Schema.Literal("user"),
+      71           77 :     content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]),
+      72            5 :   }),
+      73           19 :   Schema.Struct({
+      74           38 :     role: Schema.Literal("assistant"),
+      75           42 :     content: Schema.NullOr(Schema.String),
+      76           59 :     tool_calls: optionalArray(OpenAIChatAssistantToolCall),
+      77           51 :     reasoning_content: Schema.optional(Schema.String),
+      78            5 :   }),
+      79          101 :   Schema.Struct({ role: Schema.Literal("tool"), tool_call_id: Schema.String, content: Schema.String }),
+      80           38 : ]).pipe(Schema.toTaggedUnion("role"))
+      81              : type OpenAIChatMessage = Schema.Schema.Type<typeof OpenAIChatMessage>
+      82              : 
+      83           44 : const OpenAIChatToolChoice = Schema.Union([
+      84           48 :   Schema.Literals(["auto", "none", "required"]),
+      85           19 :   Schema.Struct({
+      86           33 :     type: Schema.tag("function"),
+      87           50 :     function: Schema.Struct({ name: Schema.String }),
+      88            3 :   }),
+      89            3 : ])
+      90              : 
+      91           28 : export const bodyFields = {
+      92           23 :   model: Schema.String,
+      93           44 :   messages: Schema.Array(OpenAIChatMessage),
+      94           39 :   tools: optionalArray(OpenAIChatTool),
+      95           53 :   tool_choice: Schema.optional(OpenAIChatToolChoice),
+      96           31 :   stream: Schema.Literal(true),
+      97           84 :   stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })),
+      98           41 :   store: Schema.optional(Schema.Boolean),
+      99           73 :   reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
+     100           45 :   max_tokens: Schema.optional(Schema.Number),
+     101           46 :   temperature: Schema.optional(Schema.Number),
+     102           40 :   top_p: Schema.optional(Schema.Number),
+     103           52 :   frequency_penalty: Schema.optional(Schema.Number),
+     104           51 :   presence_penalty: Schema.optional(Schema.Number),
+     105           39 :   seed: Schema.optional(Schema.Number),
+     106           35 :   stop: optionalArray(Schema.String),
+     107            2 : }
+     108           49 : const OpenAIChatBody = Schema.Struct(bodyFields)
+     109              : export type OpenAIChatBody = Schema.Schema.Type<typeof OpenAIChatBody>
+     110              : 
+     111              : // =============================================================================
+     112              : // Streaming Event Schema
+     113              : // =============================================================================
+     114              : // The event schema is one decoded SSE `data:` payload. `Framing.sse` splits the
+     115              : // byte stream into strings, then `Protocol.jsonEvent` decodes each string into
+     116              : // this provider-native event shape.
+     117           40 : const OpenAIChatUsage = Schema.Struct({
+     118           48 :   prompt_tokens: Schema.optional(Schema.Number),
+     119           52 :   completion_tokens: Schema.optional(Schema.Number),
+     120           47 :   total_tokens: Schema.optional(Schema.Number),
+     121           36 :   prompt_tokens_details: optionalNull(
+     122           19 :     Schema.Struct({
+     123           47 :       cached_tokens: Schema.optional(Schema.Number),
+     124            2 :     }),
+     125            4 :   ),
+     126           40 :   completion_tokens_details: optionalNull(
+     127           19 :     Schema.Struct({
+     128           50 :       reasoning_tokens: Schema.optional(Schema.Number),
+     129            2 :     }),
+     130            2 :   ),
+     131            3 : })
+     132              : 
+     133           56 : const OpenAIChatToolCallDeltaFunction = Schema.Struct({
+     134           36 :   name: optionalNull(Schema.String),
+     135           39 :   arguments: optionalNull(Schema.String),
+     136            3 : })
+     137              : 
+     138           48 : const OpenAIChatToolCallDelta = Schema.Struct({
+     139           23 :   index: Schema.Number,
+     140           34 :   id: optionalNull(Schema.String),
+     141           56 :   function: optionalNull(OpenAIChatToolCallDeltaFunction),
+     142            3 : })
+     143              : type OpenAIChatToolCallDelta = Schema.Schema.Type<typeof OpenAIChatToolCallDelta>
+     144              : 
+     145           40 : const OpenAIChatDelta = Schema.Struct({
+     146           39 :   content: optionalNull(Schema.String),
+     147           49 :   reasoning_content: optionalNull(Schema.String),
+     148           64 :   tool_calls: optionalNull(Schema.Array(OpenAIChatToolCallDelta)),
+     149            3 : })
+     150              : 
+     151           41 : const OpenAIChatChoice = Schema.Struct({
+     152           39 :   delta: optionalNull(OpenAIChatDelta),
+     153           43 :   finish_reason: optionalNull(Schema.String),
+     154            3 : })
+     155              : 
+     156           40 : const OpenAIChatEvent = Schema.Struct({
+     157           42 :   choices: Schema.Array(OpenAIChatChoice),
+     158           37 :   usage: optionalNull(OpenAIChatUsage),
+     159            3 : })
+     160              : type OpenAIChatEvent = Schema.Schema.Type<typeof OpenAIChatEvent>
+     161              : type OpenAIChatRequestMessage = LLMRequest["messages"][number]
+     162              : 
+     163              : interface ParserState {
+     164              :   readonly tools: ToolStream.State<number>
+     165              :   readonly toolCallEvents: ReadonlyArray<LLMEvent>
+     166              :   readonly usage?: Usage
+     167              :   readonly finishReason?: FinishReason
+     168              :   readonly lifecycle: Lifecycle.State
+     169              : }
+     170              : 
+     171           46 : const invalid = ProviderShared.invalidRequest
+     172              : 
+     173              : // =============================================================================
+     174              : // Request Lowering
+     175              : // =============================================================================
+     176              : // Lowering is the only place that knows how common LLM messages map onto the
+     177              : // OpenAI Chat wire format. Keep provider quirks here instead of leaking native
+     178              : // fields into `LLMRequest`.
+     179            0 : const lowerTool = (tool: ToolDefinition, inputSchema: JsonSchema): OpenAIChatTool => ({
+     180            0 :   type: "function",
+     181            0 :   function: {
+     182            0 :     name: tool.name,
+     183            0 :     description: tool.description,
+     184            0 :     parameters: ToolSchemaProjection.openAI(inputSchema),
+     185              :   },
+     186            2 : })
+     187              : 
+     188            0 : const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
+     189            0 :   ProviderShared.matchToolChoice("OpenAI Chat", toolChoice, {
+     190            0 :     auto: () => "auto" as const,
+     191            0 :     none: () => "none" as const,
+     192            0 :     required: () => "required" as const,
+     193              :     tool: (name) => ({ type: "function" as const, function: { name } }),
+     194            2 :   })
+     195              : 
+     196            0 : const lowerToolCall = (part: ToolCallPart): OpenAIChatAssistantToolCall => ({
+     197            0 :   id: part.id,
+     198            0 :   type: "function",
+     199            0 :   function: {
+     200            0 :     name: part.name,
+     201            0 :     arguments: ProviderShared.encodeJson(part.input),
+     202              :   },
+     203            2 : })
+     204              : 
+     205            0 : const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart) {
+     206            0 :   const media = yield* ProviderShared.validateMedia("OpenAI Chat", part, IMAGE_MIMES)
+     207              :   return { type: "image_url" as const, image_url: { url: media.dataUrl } }
+     208            3 : })
+     209              : 
+     210            0 : const openAICompatibleReasoningContent = (native: unknown) =>
+     211            2 :   isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined
+     212              : 
+     213           76 : const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message: OpenAIChatRequestMessage) {
+     214           21 :   const content: Array<Schema.Schema.Type<typeof OpenAIChatUserContent>> = []
+     215           40 :   for (const part of message.content) {
+     216           32 :     if (part.type === "text") {
+     217           54 :       content.push({ type: "text", text: part.text })
+     218            8 :       continue
+     219            0 :     }
+     220            0 :     if (part.type === "media") {
+     221            0 :       content.push(yield* lowerMedia(part))
+     222            0 :       continue
+     223            0 :     }
+     224            0 :     return yield* ProviderShared.unsupportedContent("OpenAI Chat", "user", ["text", "media"])
+     225            2 :   }
+     226           51 :   if (content.every((part) => part.type === "text"))
+     227           73 :     return { role: "user" as const, content: content.map((part) => part.text).join("") }
+     228            0 :   return { role: "user" as const, content }
+     229            3 : })
+     230              : 
+     231            0 : const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(function* (
+     232            0 :   message: OpenAIChatRequestMessage,
+     233            0 : ) {
+     234            0 :   const content: TextPart[] = []
+     235            0 :   const reasoning: ReasoningPart[] = []
+     236            0 :   const toolCalls: OpenAIChatAssistantToolCall[] = []
+     237            0 :   for (const part of message.content) {
+     238            0 :     if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
+     239            0 :       return yield* ProviderShared.unsupportedContent("OpenAI Chat", "assistant", ["text", "reasoning", "tool-call"])
+     240            0 :     if (part.type === "text") {
+     241            0 :       content.push(part)
+     242            0 :       continue
+     243            0 :     }
+     244            0 :     if (part.type === "reasoning") {
+     245            0 :       reasoning.push(part)
+     246            0 :       continue
+     247            0 :     }
+     248            0 :     if (part.type === "tool-call") {
+     249            0 :       toolCalls.push(lowerToolCall(part))
+     250            0 :       continue
+     251            0 :     }
+     252            0 :   }
+     253            0 :   return {
+     254            0 :     role: "assistant" as const,
+     255            0 :     content: content.length === 0 ? null : ProviderShared.joinText(content),
+     256            0 :     tool_calls: toolCalls.length === 0 ? undefined : toolCalls,
+     257            0 :     reasoning_content:
+     258            0 :       reasoning.length > 0
+     259            0 :         ? reasoning.map((part) => part.text).join("")
+     260            0 :         : openAICompatibleReasoningContent(message.native?.openaiCompatible),
+     261              :   }
+     262            3 : })
+     263              : 
+     264            0 : const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (message: OpenAIChatRequestMessage) {
+     265            0 :   const messages: OpenAIChatMessage[] = []
+     266            0 :   const images: Array<Schema.Schema.Type<typeof OpenAIChatUserContent>> = []
+     267            0 :   for (const part of message.content) {
+     268            0 :     if (!ProviderShared.supportsContent(part, ["tool-result"]))
+     269            0 :       return yield* ProviderShared.unsupportedContent("OpenAI Chat", "tool", ["tool-result"])
+     270            0 :     if (part.result.type !== "content") {
+     271            0 :       messages.push({ role: "tool", tool_call_id: part.id, content: ProviderShared.toolResultText(part) })
+     272            0 :       continue
+     273            0 :     }
+     274            0 :     const content: ReadonlyArray<ToolContent> = part.result.value
+     275            0 :     const text = content.filter((item) => item.type === "text").map((item) => item.text)
+     276            0 :     messages.push({ role: "tool", tool_call_id: part.id, content: text.join("\n") })
+     277            0 :     const files = content.filter((item) => item.type === "file")
+     278            0 :     images.push(
+     279            0 :       ...(yield* Effect.forEach(files, (item) =>
+     280            0 :         lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }),
+     281            0 :       )),
+     282            0 :     )
+     283            0 :   }
+     284              :   return { messages, images }
+     285            3 : })
+     286              : 
+     287           68 : const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message: OpenAIChatRequestMessage) {
+     288           72 :   if (message.role === "user") return [yield* lowerUserMessage(message)]
+     289            0 :   if (message.role === "assistant") return [yield* lowerAssistantMessage(message)]
+     290            0 :   return (yield* lowerToolMessages(message)).messages
+     291            3 : })
+     292              : 
+     293           70 : const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: LLMRequest) {
+     294           15 :   const system: OpenAIChatMessage[] =
+     295           33 :     request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }]
+     296           31 :   const messages = [...system]
+     297           27 :   const pendingImages: Array<Schema.Schema.Type<typeof OpenAIChatUserContent>> = []
+     298           30 :   const flushImages = () => {
+     299           42 :     if (pendingImages.length === 0) return
+     300            3 :     messages.push({ role: "user", content: pendingImages.splice(0) })
+     301              :   }
+     302           44 :   for (const message of request.messages) {
+     303           30 :     if (message.role === "system") {
+     304            0 :       const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Chat", message)
+     305            0 :       if (pendingImages.length > 0) {
+     306            0 :         messages.push({ role: "user", content: [...pendingImages.splice(0), { type: "text", text: part.text }] })
+     307            0 :         continue
+     308            0 :       }
+     309            0 :       const previous = messages.at(-1)
+     310            0 :       if (previous?.role === "user" && typeof previous.content === "string")
+     311            0 :         messages[messages.length - 1] = { role: "user", content: `${previous.content}\n${part.text}` }
+     312            0 :       else if (previous?.role === "user" && Array.isArray(previous.content))
+     313            0 :         messages[messages.length - 1] = {
+     314            0 :           role: "user",
+     315            0 :           content: [...previous.content, { type: "text", text: part.text }],
+     316            0 :         }
+     317            0 :       else messages.push({ role: "user", content: part.text })
+     318            0 :       continue
+     319            4 :     }
+     320           28 :     if (message.role === "tool") {
+     321            0 :       const lowered = yield* lowerToolMessages(message)
+     322            0 :       messages.push(...lowered.messages)
+     323            0 :       pendingImages.push(...lowered.images)
+     324            0 :       continue
+     325            4 :     }
+     326           18 :     flushImages()
+     327           49 :     messages.push(...(yield* lowerMessage(message)))
+     328            2 :   }
+     329           16 :   flushImages()
+     330           15 :   return messages
+     331            3 : })
+     332              : 
+     333           68 : const lowerOptions = Effect.fn("OpenAIChat.lowerOptions")(function* (request: LLMRequest) {
+     334           45 :   const store = OpenAIOptions.store(request)
+     335           65 :   const reasoningEffort = OpenAIOptions.reasoningEffort(request)
+     336           76 :   if (reasoningEffort && !OpenAIOptions.isReasoningEffort(reasoningEffort))
+     337            2 :     return yield* invalid(`OpenAI Chat does not support reasoning effort ${reasoningEffort}`)
+     338           15 :   return {
+     339           32 :     ...(store !== undefined ? { store } : {}),
+     340           22 :     ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
+     341            1 :   }
+     342            3 : })
+     343              : 
+     344           66 : const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (request: LLMRequest) {
+     345              :   // `fromRequest` returns the provider body only. Endpoint, auth, framing,
+     346              :   // validation, and HTTP execution are composed by `Route.make`.
+     347           40 :   const generation = request.generation
+     348           74 :   const toolSchemaCompatibility = request.model.compatibility?.toolSchema
+     349           12 :   return {
+     350           28 :     model: request.model.id,
+     351           44 :     messages: yield* lowerMessages(request),
+     352            7 :     tools:
+     353           28 :       request.tools.length === 0
+     354            9 :         ? undefined
+     355            0 :         : request.tools.map((tool) =>
+     356              :             lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility)),
+     357            4 :           ),
+     358           48 :     tool_choice: request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined,
+     359           17 :     stream: true as const,
+     360           44 :     stream_options: { include_usage: true },
+     361           38 :     max_tokens: generation?.maxTokens,
+     362           41 :     temperature: generation?.temperature,
+     363           28 :     top_p: generation?.topP,
+     364           52 :     frequency_penalty: generation?.frequencyPenalty,
+     365           50 :     presence_penalty: generation?.presencePenalty,
+     366           27 :     seed: generation?.seed,
+     367           30 :     stop: generation?.stop,
+     368           30 :     ...(yield* lowerOptions(request)),
+     369            1 :   }
+     370            3 : })
+     371              : 
+     372              : // =============================================================================
+     373              : // Stream Parsing
+     374              : // =============================================================================
+     375              : // Streaming parsers are small state machines: every event returns a new state
+     376              : // plus the common `LLMEvent`s produced by that event. Tool calls are accumulated
+     377              : // because OpenAI streams JSON arguments across multiple deltas.
+     378           37 : const mapFinishReason = (reason: string | null | undefined): FinishReason => {
+     379           38 :   if (reason === "stop") return "stop"
+     380            0 :   if (reason === "length") return "length"
+     381            0 :   if (reason === "content_filter") return "content-filter"
+     382            0 :   if (reason === "function_call" || reason === "tool_calls") return "tool-calls"
+     383            2 :   return "unknown"
+     384              : }
+     385              : 
+     386              : // OpenAI Chat reports `prompt_tokens` (inclusive total) with a
+     387              : // `cached_tokens` subset, and `completion_tokens` (inclusive total) with
+     388              : // a `reasoning_tokens` subset. We pass the inclusive totals through and
+     389              : // derive the non-cached breakdown so the `LLM.Usage` contract is
+     390              : // satisfied on both sides.
+     391           29 : const mapUsage = (usage: OpenAIChatEvent["usage"]): Usage | undefined => {
+     392           22 :   if (!usage) return undefined
+     393           60 :   const cached = usage.prompt_tokens_details?.cached_tokens
+     394           70 :   const reasoning = usage.completion_tokens_details?.reasoning_tokens
+     395           79 :   const nonCached = ProviderShared.subtractTokens(usage.prompt_tokens, cached)
+     396           22 :   return new Usage({
+     397           37 :     inputTokens: usage.prompt_tokens,
+     398           42 :     outputTokens: usage.completion_tokens,
+     399           36 :     nonCachedInputTokens: nonCached,
+     400           33 :     cacheReadInputTokens: cached,
+     401           31 :     reasoningTokens: reasoning,
+     402          110 :     totalTokens: ProviderShared.totalTokens(usage.prompt_tokens, usage.completion_tokens, usage.total_tokens),
+     403           37 :     providerMetadata: { openai: usage },
+     404            4 :   })
+     405              : }
+     406              : 
+     407           29 : const step = (state: ParserState, event: OpenAIChatEvent) =>
+     408           15 :   Effect.gen(function* () {
+     409           20 :     const events: LLMEvent[] = []
+     410           53 :     const usage = mapUsage(event.usage) ?? state.usage
+     411           34 :     const choice = event.choices[0]
+     412          103 :     const finishReason = choice?.finish_reason ? mapFinishReason(choice.finish_reason) : state.finishReason
+     413           30 :     const delta = choice?.delta
+     414           45 :     const toolDeltas = delta?.tool_calls ?? []
+     415           26 :     let tools = state.tools
+     416              : 
+     417           34 :     let lifecycle = state.lifecycle
+     418              : 
+     419           32 :     if (delta?.reasoning_content)
+     420            2 :       lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", delta.reasoning_content)
+     421              : 
+     422           24 :     if (delta?.content) {
+     423           73 :       lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0")
+     424           78 :       lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.content)
+     425            2 :     }
+     426              : 
+     427           27 :     if (toolDeltas.length) lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0")
+     428              : 
+     429           30 :     for (const tool of toolDeltas) {
+     430            0 :       const result = ToolStream.appendOrStart(
+     431            0 :         ADAPTER,
+     432            0 :         tools,
+     433            0 :         tool.index,
+     434            0 :         { id: tool.id ?? undefined, name: tool.function?.name ?? undefined, text: tool.function?.arguments ?? "" },
+     435            0 :         "OpenAI Chat tool call delta is missing id or name",
+     436            0 :       )
+     437            0 :       if (ToolStream.isError(result)) return yield* result
+     438            0 :       tools = result.tools
+     439            0 :       if (result.events.length) lifecycle = Lifecycle.stepStart(lifecycle, events)
+     440            0 :       events.push(...result.events)
+     441            2 :     }
+     442              : 
+     443              :     // Finalize accumulated tool inputs eagerly when finish_reason arrives so
+     444              :     // JSON parse failures fail the stream at the boundary rather than at halt.
+     445           17 :     const finished =
+     446           97 :       finishReason !== undefined && state.finishReason === undefined && Object.keys(tools).length > 0
+     447            2 :         ? yield* ToolStream.finishAll(ADAPTER, tools)
+     448           11 :         : undefined
+     449              : 
+     450           12 :     return [
+     451            7 :       {
+     452           38 :         tools: finished?.tools ?? tools,
+     453           63 :         toolCallEvents: finished?.events ?? state.toolCallEvents,
+     454           12 :         usage,
+     455           19 :         finishReason,
+     456           13 :         lifecycle,
+     457            6 :       },
+     458            8 :       events,
+     459            1 :     ] as const
+     460            2 :   })
+     461              : 
+     462           33 : const finishEvents = (state: ParserState): ReadonlyArray<LLMEvent> => {
+     463           20 :   const events: LLMEvent[] = []
+     464           55 :   const hasToolCalls = state.toolCallEvents.length > 0
+     465           84 :   const reason = state.finishReason === "stop" && hasToolCalls ? "tool-calls" : state.finishReason
+     466           66 :   const lifecycle = state.toolCallEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle
+     467           39 :   events.push(...state.toolCallEvents)
+     468           83 :   if (reason) Lifecycle.finish(lifecycle, events, { reason, usage: state.usage })
+     469           15 :   return events
+     470              : }
+     471              : 
+     472              : // =============================================================================
+     473              : // Protocol And OpenAI Route
+     474              : // =============================================================================
+     475              : /**
+     476              :  * The OpenAI Chat protocol — request body construction, body schema, and the
+     477              :  * streaming-event state machine. Reused by every route that speaks OpenAI Chat
+     478              :  * over HTTP+SSE: native OpenAI, DeepSeek, TogetherAI, Cerebras, Baseten,
+     479              :  * Fireworks, DeepInfra, and (once added) Azure OpenAI Chat.
+     480              :  */
+     481           40 : export const protocol = Protocol.make({
+     482           14 :   id: ADAPTER,
+     483           11 :   body: {
+     484           27 :     schema: OpenAIChatBody,
+     485           19 :     from: fromRequest,
+     486            4 :   },
+     487           13 :   stream: {
+     488           47 :     event: Protocol.jsonEvent(OpenAIChatEvent),
+     489          101 :     initial: () => ({ tools: ToolStream.empty<number>(), toolCallEvents: [], lifecycle: Lifecycle.initial() }),
+     490            9 :     step,
+     491           22 :     onHalt: finishEvents,
+     492            2 :   },
+     493            3 : })
+     494              : 
+     495           58 : export const httpTransport = HttpTransport.sseJson.with<OpenAIChatBody>()
+     496              : 
+     497           34 : export const route = Route.make({
+     498           14 :   id: ADAPTER,
+     499           21 :   provider: "openai",
+     500           11 :   protocol,
+     501           63 :   endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
+     502           18 :   auth: Auth.none,
+     503           25 :   transport: httpTransport,
+     504            3 : })
+     505              : 
+     506           43 : export * as OpenAIChat from "./openai-chat"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/openai-compatible-chat.ts.gcov.html b/packages/core/llm/src/protocols/openai-compatible-chat.ts.gcov.html new file mode 100644 index 00000000..21fb7f12 --- /dev/null +++ b/packages/core/llm/src/protocols/openai-compatible-chat.ts.gcov.html @@ -0,0 +1,100 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols/openai-compatible-chat.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocols - openai-compatible-chat.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %1212
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           40 : import { Route, type RouteRoutedModelInput } from "../route/client"
+       2           45 : import { Endpoint } from "../route/endpoint"
+       3           43 : import { Framing } from "../route/framing"
+       4           44 : import * as OpenAIChat from "./openai-chat"
+       5              : 
+       6           41 : const ADAPTER = "openai-compatible-chat"
+       7              : 
+       8              : export type OpenAICompatibleChatModelInput = RouteRoutedModelInput
+       9              : 
+      10              : /**
+      11              :  * Route for non-OpenAI providers that expose an OpenAI Chat-compatible
+      12              :  * `/chat/completions` endpoint. Reuses `OpenAIChat.protocol` end-to-end and
+      13              :  * overrides only the route id so providers can be resolved per-family without
+      14              :  * colliding with native OpenAI. Provider helpers configure the route endpoint
+      15              :  * before model selection.
+      16              :  */
+      17           34 : export const route = Route.make({
+      18           14 :   id: ADAPTER,
+      19           32 :   protocol: OpenAIChat.protocol,
+      20           47 :   endpoint: Endpoint.path("/chat/completions"),
+      21           21 :   framing: Framing.sse,
+      22            3 : })
+      23              : 
+      24           64 : export * as OpenAICompatibleChat from "./openai-compatible-chat"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/openai-responses.ts.gcov.html b/packages/core/llm/src/protocols/openai-responses.ts.gcov.html new file mode 100644 index 00000000..95865639 --- /dev/null +++ b/packages/core/llm/src/protocols/openai-responses.ts.gcov.html @@ -0,0 +1,1098 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols/openai-responses.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocols - openai-responses.tsCoverageTotalHit
Test:opencode-lcov.infoLines:40.1 %793318
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           40 : import { Effect, Schema } from "effect"
+       2           40 : import { Route } from "../route/client"
+       3           37 : import { Auth } from "../route/auth"
+       4           45 : import { Endpoint } from "../route/endpoint"
+       5           71 : import { HttpTransport, WebSocketTransport } from "../route/transport"
+       6           45 : import { Protocol } from "../route/protocol"
+       7           45 : import {
+       8              :   LLMEvent,
+       9              :   Usage,
+      10              :   type FinishReason,
+      11              :   type JsonSchema,
+      12              :   type LLMRequest,
+      13              :   type ProviderMetadata,
+      14              :   type ReasoningPart,
+      15              :   type TextPart,
+      16              :   type ToolCallPart,
+      17              :   type ToolDefinition,
+      18              :   type ToolContent,
+      19              :   type ToolResultPart,
+      20              : } from "../schema"
+      21           83 : import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
+      22           54 : import { isContextOverflow } from "../provider-error"
+      23           55 : import { OpenAIOptions } from "./utils/openai-options"
+      24           46 : import { Lifecycle } from "./utils/lifecycle"
+      25           59 : import { ToolSchemaProjection } from "./utils/tool-schema"
+      26           49 : import { ToolStream } from "./utils/tool-stream"
+      27              : 
+      28           35 : const ADAPTER = "openai-responses"
+      29           60 : export const DEFAULT_BASE_URL = "https://api.openai.com/v1"
+      30           33 : export const PATH = "/responses"
+      31              : 
+      32              : // =============================================================================
+      33              : // Request Body Schema
+      34              : // =============================================================================
+      35           49 : const OpenAIResponsesInputText = Schema.Struct({
+      36           33 :   type: Schema.tag("input_text"),
+      37           20 :   text: Schema.String,
+      38            3 : })
+      39           50 : const OpenAIResponsesInputImage = Schema.Struct({
+      40           34 :   type: Schema.tag("input_image"),
+      41           25 :   image_url: Schema.String,
+      42            3 : })
+      43          104 : const OpenAIResponsesInputContent = Schema.Union([OpenAIResponsesInputText, OpenAIResponsesInputImage])
+      44              : type OpenAIResponsesInputContent = Schema.Schema.Type<typeof OpenAIResponsesInputContent>
+      45              : 
+      46           50 : const OpenAIResponsesOutputText = Schema.Struct({
+      47           34 :   type: Schema.tag("output_text"),
+      48           20 :   text: Schema.String,
+      49            3 : })
+      50              : 
+      51           60 : const OpenAIResponsesReasoningSummaryText = Schema.Struct({
+      52           35 :   type: Schema.tag("summary_text"),
+      53           20 :   text: Schema.String,
+      54            3 : })
+      55              : 
+      56           53 : const OpenAIResponsesReasoningItem = Schema.Struct({
+      57           32 :   type: Schema.tag("reasoning"),
+      58           40 :   id: Schema.optionalKey(Schema.String),
+      59           61 :   summary: Schema.Array(OpenAIResponsesReasoningSummaryText),
+      60           47 :   encrypted_content: optionalNull(Schema.String),
+      61            3 : })
+      62              : 
+      63           53 : const OpenAIResponsesItemReference = Schema.Struct({
+      64           37 :   type: Schema.tag("item_reference"),
+      65           18 :   id: Schema.String,
+      66            3 : })
+      67              : 
+      68              : // `function_call_output.output` accepts either a plain string or an ordered
+      69              : // array of content items so tools can return images in addition to text.
+      70              : // https://platform.openai.com/docs/api-reference/responses/object
+      71          117 : const OpenAIResponsesFunctionCallOutputContent = Schema.Union([OpenAIResponsesInputText, OpenAIResponsesInputImage])
+      72              : 
+      73           57 : const OpenAIResponsesFunctionCallOutput = Schema.Union([
+      74           16 :   Schema.String,
+      75           55 :   Schema.Array(OpenAIResponsesFunctionCallOutputContent),
+      76            3 : ])
+      77              : 
+      78           48 : const OpenAIResponsesInputItem = Schema.Union([
+      79           72 :   Schema.Struct({ role: Schema.tag("system"), content: Schema.String }),
+      80           98 :   Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }),
+      81          101 :   Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }),
+      82           31 :   OpenAIResponsesReasoningItem,
+      83           31 :   OpenAIResponsesItemReference,
+      84           19 :   Schema.Struct({
+      85           38 :     type: Schema.tag("function_call"),
+      86           27 :     call_id: Schema.String,
+      87           24 :     name: Schema.String,
+      88           26 :     arguments: Schema.String,
+      89            5 :   }),
+      90           19 :   Schema.Struct({
+      91           45 :     type: Schema.tag("function_call_output"),
+      92           27 :     call_id: Schema.String,
+      93           43 :     output: OpenAIResponsesFunctionCallOutput,
+      94            3 :   }),
+      95            3 : ])
+      96              : type OpenAIResponsesInputItem = Schema.Schema.Type<typeof OpenAIResponsesInputItem>
+      97              : 
+      98              : // Mutable counterpart of the schema reasoning item so `lowerMessages` can fold
+      99              : // multiple streamed summary parts into the same item before flushing.
+     100              : type OpenAIResponsesReasoningInput = {
+     101              :   type: "reasoning"
+     102              :   id: string
+     103              :   summary: Array<{ type: "summary_text"; text: string }>
+     104              :   encrypted_content?: string | null
+     105              : }
+     106              : type OpenAIResponsesReasoningReplay = Omit<OpenAIResponsesReasoningInput, "id">
+     107              : 
+     108           44 : const OpenAIResponsesTool = Schema.Struct({
+     109           31 :   type: Schema.tag("function"),
+     110           22 :   name: Schema.String,
+     111           29 :   description: Schema.String,
+     112           25 :   parameters: JsonObject,
+     113           40 :   strict: Schema.optional(Schema.Boolean),
+     114            3 : })
+     115              : type OpenAIResponsesTool = Schema.Schema.Type<typeof OpenAIResponsesTool>
+     116              : 
+     117           49 : const OpenAIResponsesToolChoice = Schema.Union([
+     118           48 :   Schema.Literals(["auto", "none", "required"]),
+     119           69 :   Schema.Struct({ type: Schema.tag("function"), name: Schema.String }),
+     120            3 : ])
+     121              : 
+     122              : // Fields shared between the HTTP body and the WebSocket `response.create`
+     123              : // message. The HTTP body adds `stream: true`; the WebSocket message adds
+     124              : // `type: "response.create"`. Defining the shared shape once keeps the two
+     125              : // transports in sync without a destructure-and-strip dance.
+     126           36 : const OpenAIResponsesCoreFields = {
+     127           23 :   model: Schema.String,
+     128           48 :   input: Schema.Array(OpenAIResponsesInputItem),
+     129           47 :   instructions: Schema.optional(Schema.String),
+     130           44 :   tools: optionalArray(OpenAIResponsesTool),
+     131           58 :   tool_choice: Schema.optional(OpenAIResponsesToolChoice),
+     132           41 :   store: Schema.optional(Schema.Boolean),
+     133           65 :   service_tier: Schema.optional(OpenAIOptions.OpenAIServiceTier),
+     134           51 :   prompt_cache_key: Schema.optional(Schema.String),
+     135           65 :   include: optionalArray(OpenAIOptions.OpenAIResponseIncludable),
+     136           27 :   reasoning: Schema.optional(
+     137           19 :     Schema.Struct({
+     138           65 :       effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
+     139           50 :       summary: Schema.optional(Schema.Literal("auto")),
+     140            2 :     }),
+     141            4 :   ),
+     142           22 :   text: Schema.optional(
+     143           19 :     Schema.Struct({
+     144           63 :       verbosity: Schema.optional(OpenAIOptions.OpenAITextVerbosity),
+     145            2 :     }),
+     146            4 :   ),
+     147           52 :   max_output_tokens: Schema.optional(Schema.Number),
+     148           46 :   temperature: Schema.optional(Schema.Number),
+     149           38 :   top_p: Schema.optional(Schema.Number),
+     150            2 : }
+     151              : 
+     152           47 : const OpenAIResponsesBody = Schema.Struct({
+     153           28 :   ...OpenAIResponsesCoreFields,
+     154           29 :   stream: Schema.Literal(true),
+     155            3 : })
+     156              : export type OpenAIResponsesBody = Schema.Schema.Type<typeof OpenAIResponsesBody>
+     157              : 
+     158           61 : const OpenAIResponsesWebSocketMessage = Schema.StructWithRest(
+     159           17 :   Schema.Struct({
+     160           41 :     type: Schema.tag("response.create"),
+     161           26 :     ...OpenAIResponsesCoreFields,
+     162            3 :   }),
+     163           46 :   [Schema.Record(Schema.String, Schema.Unknown)],
+     164            3 : )
+     165              : type OpenAIResponsesWebSocketMessage = Schema.Schema.Type<typeof OpenAIResponsesWebSocketMessage>
+     166          105 : const encodeWebSocketMessage = Schema.encodeSync(Schema.fromJsonString(OpenAIResponsesWebSocketMessage))
+     167              : 
+     168           45 : const OpenAIResponsesUsage = Schema.Struct({
+     169           47 :   input_tokens: Schema.optional(Schema.Number),
+     170          103 :   input_tokens_details: optionalNull(Schema.Struct({ cached_tokens: Schema.optional(Schema.Number) })),
+     171           48 :   output_tokens: Schema.optional(Schema.Number),
+     172          107 :   output_tokens_details: optionalNull(Schema.Struct({ reasoning_tokens: Schema.optional(Schema.Number) })),
+     173           45 :   total_tokens: Schema.optional(Schema.Number),
+     174            3 : })
+     175              : type OpenAIResponsesUsage = Schema.Schema.Type<typeof OpenAIResponsesUsage>
+     176              : 
+     177           50 : const OpenAIResponsesStreamItem = Schema.Struct({
+     178           22 :   type: Schema.String,
+     179           37 :   id: Schema.optional(Schema.String),
+     180           42 :   call_id: Schema.optional(Schema.String),
+     181           39 :   name: Schema.optional(Schema.String),
+     182           44 :   arguments: Schema.optional(Schema.String),
+     183              :   // Hosted (provider-executed) tool fields. Each hosted tool item carries its
+     184              :   // own subset of these — we capture them generically so we can surface the
+     185              :   // call's typed input portion and round-trip the full result payload without
+     186              :   // hand-rolling a per-tool schema.
+     187           41 :   status: Schema.optional(Schema.String),
+     188           42 :   action: Schema.optional(Schema.Unknown),
+     189           43 :   queries: Schema.optional(Schema.Unknown),
+     190           43 :   results: Schema.optional(Schema.Unknown),
+     191           39 :   code: Schema.optional(Schema.String),
+     192           47 :   container_id: Schema.optional(Schema.String),
+     193           43 :   outputs: Schema.optional(Schema.Unknown),
+     194           47 :   server_label: Schema.optional(Schema.String),
+     195           42 :   output: Schema.optional(Schema.Unknown),
+     196           41 :   error: Schema.optional(Schema.Unknown),
+     197           47 :   encrypted_content: optionalNull(Schema.String),
+     198            3 : })
+     199              : type OpenAIResponsesStreamItem = Schema.Schema.Type<typeof OpenAIResponsesStreamItem>
+     200              : 
+     201              : // OpenAI Responses surfaces provider failures in two related shapes. The
+     202              : // streaming `error` event carries the details at the top level
+     203              : // (`{ type: "error", code, message, param, sequence_number }`), while
+     204              : // `response.failed` carries them under `response.error`. We capture both so
+     205              : // the parser can surface a useful provider-error message in either path.
+     206           52 : const OpenAIResponsesErrorPayload = Schema.Struct({
+     207           36 :   code: optionalNull(Schema.String),
+     208           39 :   message: optionalNull(Schema.String),
+     209           35 :   param: optionalNull(Schema.String),
+     210            3 : })
+     211              : 
+     212           45 : const OpenAIResponsesEvent = Schema.Struct({
+     213           22 :   type: Schema.String,
+     214           40 :   delta: Schema.optional(Schema.String),
+     215           42 :   item_id: Schema.optional(Schema.String),
+     216           48 :   summary_index: Schema.optional(Schema.Number),
+     217           51 :   item: Schema.optional(OpenAIResponsesStreamItem),
+     218           26 :   response: Schema.optional(
+     219           22 :     Schema.StructWithRest(
+     220           19 :       Schema.Struct({
+     221           39 :         id: Schema.optional(Schema.String),
+     222           46 :         service_tier: optionalNull(Schema.String),
+     223           79 :         incomplete_details: optionalNull(Schema.Struct({ reason: Schema.String })),
+     224           46 :         usage: optionalNull(OpenAIResponsesUsage),
+     225           50 :         error: optionalNull(OpenAIResponsesErrorPayload),
+     226            4 :       }),
+     227           46 :       [Schema.Record(Schema.String, Schema.Unknown)],
+     228            1 :     ),
+     229            4 :   ),
+     230           39 :   code: Schema.optional(Schema.String),
+     231           42 :   message: Schema.optional(Schema.String),
+     232           38 :   param: Schema.optional(Schema.String),
+     233            3 : })
+     234              : type OpenAIResponsesEvent = Schema.Schema.Type<typeof OpenAIResponsesEvent>
+     235              : 
+     236              : interface ParserState {
+     237              :   readonly tools: ToolStream.State<string>
+     238              :   readonly hasFunctionCall: boolean
+     239              :   readonly lifecycle: Lifecycle.State
+     240              :   readonly reasoningItems: Readonly<Record<string, ReasoningStreamItem>>
+     241              :   readonly store: boolean | undefined
+     242              : }
+     243              : 
+     244              : type ReasoningSummaryStatus = "active" | "can-conclude" | "concluded"
+     245              : 
+     246              : interface ReasoningStreamItem {
+     247              :   readonly encryptedContent: string | null | undefined
+     248              :   // Keyed by OpenAI's numeric `summary_index`. JS object keys coerce to
+     249              :   // strings, but typing the map as `Record<number, ...>` documents intent
+     250              :   // and matches the wire field.
+     251              :   readonly summaryParts: Readonly<Record<number, ReasoningSummaryStatus>>
+     252              : }
+     253              : 
+     254           46 : const invalid = ProviderShared.invalidRequest
+     255              : 
+     256              : // =============================================================================
+     257              : // Request Lowering
+     258              : // =============================================================================
+     259            0 : const lowerTool = (tool: ToolDefinition, inputSchema: JsonSchema): OpenAIResponsesTool => ({
+     260            0 :   type: "function",
+     261            0 :   name: tool.name,
+     262            0 :   description: tool.description,
+     263            0 :   parameters: ToolSchemaProjection.openAI(inputSchema),
+     264            0 :   // TODO: Read this from OpenAI-specific tool options so direct LLM callers can opt into strict schemas.
+     265              :   strict: false,
+     266            2 : })
+     267              : 
+     268            0 : const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
+     269            0 :   ProviderShared.matchToolChoice("OpenAI Responses", toolChoice, {
+     270            0 :     auto: () => "auto" as const,
+     271            0 :     none: () => "none" as const,
+     272            0 :     required: () => "required" as const,
+     273              :     tool: (name) => ({ type: "function" as const, name }),
+     274            2 :   })
+     275              : 
+     276            0 : const lowerToolCall = (part: ToolCallPart): OpenAIResponsesInputItem => ({
+     277            0 :   type: "function_call",
+     278            0 :   call_id: part.id,
+     279            0 :   name: part.name,
+     280              :   arguments: ProviderShared.encodeJson(part.input),
+     281            2 : })
+     282              : 
+     283            0 : const lowerReasoning = (part: ReasoningPart): OpenAIResponsesReasoningInput | undefined => {
+     284            0 :   const openai = part.providerMetadata?.openai
+     285            0 :   if (!ProviderShared.isRecord(openai) || typeof openai.itemId !== "string" || openai.itemId.length === 0)
+     286            0 :     return undefined
+     287            0 :   const encryptedContent =
+     288            0 :     typeof openai.reasoningEncryptedContent === "string"
+     289            0 :       ? openai.reasoningEncryptedContent
+     290            0 :       : openai.reasoningEncryptedContent === null
+     291            0 :         ? null
+     292            0 :         : undefined
+     293            0 :   return {
+     294            0 :     type: "reasoning",
+     295            0 :     id: openai.itemId,
+     296            0 :     summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [],
+     297            0 :     encrypted_content: encryptedContent,
+     298            2 :   }
+     299              : }
+     300              : 
+     301            0 : const hostedToolItemID = (part: ToolResultPart) => {
+     302            0 :   const openai = part.providerMetadata?.openai
+     303            0 :   return ProviderShared.isRecord(openai) && typeof openai.itemId === "string" && openai.itemId.length > 0
+     304            0 :     ? openai.itemId
+     305            2 :     : undefined
+     306              : }
+     307              : 
+     308           70 : const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* (
+     309            5 :   part: LLMRequest["messages"][number]["content"][number],
+     310            3 : ) {
+     311           74 :   if (part.type === "text") return { type: "input_text" as const, text: part.text }
+     312            0 :   if (part.type === "media") {
+     313            0 :     const media = yield* ProviderShared.validateMedia(
+     314            0 :       "OpenAI Responses",
+     315            0 :       part,
+     316            0 :       new Set<string>(ProviderShared.IMAGE_MIMES),
+     317            0 :     )
+     318            0 :     return { type: "input_image" as const, image_url: media.dataUrl }
+     319            0 :   }
+     320            0 :   return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text", "media"])
+     321            3 : })
+     322              : 
+     323              : // Tool results may carry structured text/images. Keep media as provider-native
+     324              : // content instead of JSON-stringifying base64 into a prompt string.
+     325            0 : const lowerToolResultContentItem = Effect.fn("OpenAIResponses.lowerToolResultContentItem")(function* (
+     326            0 :   item: ToolContent,
+     327            0 : ) {
+     328            0 :   if (item.type === "text") return { type: "input_text" as const, text: item.text }
+     329            0 :   const media = yield* ProviderShared.validateToolFile(
+     330            0 :     "OpenAI Responses",
+     331            0 :     item,
+     332            0 :     new Set<string>(ProviderShared.IMAGE_MIMES),
+     333            0 :   )
+     334              :   return { type: "input_image" as const, image_url: media.dataUrl }
+     335            3 : })
+     336              : 
+     337            0 : const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput")(function* (part: ToolResultPart) {
+     338            0 :   // Text/json/error results are encoded as a plain string for backward
+     339            0 :   // compatibility with existing cassettes and provider expectations.
+     340            0 :   if (part.result.type !== "content") return ProviderShared.toolResultText(part)
+     341            0 :   // Preserve the narrowed array element type when compiled through a consumer package.
+     342            0 :   const content: ReadonlyArray<ToolContent> = part.result.value
+     343              :   return yield* Effect.forEach(content, lowerToolResultContentItem)
+     344            3 : })
+     345              : 
+     346           75 : const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) {
+     347           15 :   const system: OpenAIResponsesInputItem[] =
+     348           33 :     request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }]
+     349           28 :   const input: OpenAIResponsesInputItem[] = [...system]
+     350           45 :   const store = OpenAIOptions.store(request)
+     351              : 
+     352           44 :   for (const message of request.messages) {
+     353           30 :     if (message.role === "system") {
+     354            0 :       const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Responses", message)
+     355            0 :       const previous = input.at(-1)
+     356            0 :       if (previous && "role" in previous && previous.role === "user")
+     357            0 :         input[input.length - 1] = {
+     358            0 :           role: "user",
+     359            0 :           content: [...previous.content, { type: "input_text", text: part.text }],
+     360            0 :         }
+     361            0 :       else input.push({ role: "user", content: [{ type: "input_text", text: part.text }] })
+     362            0 :       continue
+     363            4 :     }
+     364              : 
+     365           35 :     if (message.role === "user") {
+     366          102 :       input.push({ role: "user", content: yield* Effect.forEach(message.content, lowerUserContent) })
+     367            8 :       continue
+     368            0 :     }
+     369              : 
+     370            0 :     if (message.role === "assistant") {
+     371            0 :       const content: TextPart[] = []
+     372            0 :       const reasoningItems: Record<string, OpenAIResponsesReasoningReplay> = {}
+     373            0 :       const reasoningReferences = new Set<string>()
+     374            0 :       const hostedToolReferences = new Set<string>()
+     375            0 :       const flushText = () => {
+     376            0 :         if (content.length === 0) return
+     377            0 :         input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) })
+     378            0 :         content.splice(0, content.length)
+     379              :       }
+     380            0 :       for (const part of message.content) {
+     381            0 :         if (part.type === "text") {
+     382            0 :           content.push(part)
+     383            0 :           continue
+     384            0 :         }
+     385            0 :         if (part.type === "reasoning") {
+     386            0 :           flushText()
+     387            0 :           const reasoning = lowerReasoning(part)
+     388            0 :           if (!reasoning) continue
+     389            0 :           if (store !== false) {
+     390            0 :             if (!reasoningReferences.has(reasoning.id)) input.push({ type: "item_reference", id: reasoning.id })
+     391            0 :             reasoningReferences.add(reasoning.id)
+     392            0 :             continue
+     393            0 :           }
+     394            0 :           const existing = reasoningItems[reasoning.id]
+     395            0 :           if (existing) {
+     396            0 :             existing.summary.push(...reasoning.summary)
+     397            0 :             if (typeof reasoning.encrypted_content === "string")
+     398            0 :               existing.encrypted_content = reasoning.encrypted_content
+     399            0 :             continue
+     400            0 :           }
+     401            0 :           const replay = {
+     402            0 :             type: reasoning.type,
+     403            0 :             summary: reasoning.summary,
+     404            0 :             encrypted_content: reasoning.encrypted_content,
+     405            0 :           }
+     406            0 :           reasoningItems[reasoning.id] = replay
+     407            0 :           input.push(replay)
+     408            0 :           continue
+     409            0 :         }
+     410            0 :         if (part.type === "tool-call") {
+     411            0 :           flushText()
+     412            0 :           if (part.providerExecuted === true) continue
+     413            0 :           input.push(lowerToolCall(part))
+     414            0 :           continue
+     415            0 :         }
+     416            0 :         if (part.type === "tool-result" && part.providerExecuted === true) {
+     417            0 :           flushText()
+     418            0 :           const itemID = hostedToolItemID(part)
+     419            0 :           if (store !== false && itemID && !hostedToolReferences.has(itemID))
+     420            0 :             input.push({ type: "item_reference", id: itemID })
+     421            0 :           if (itemID) hostedToolReferences.add(itemID)
+     422            0 :           continue
+     423            0 :         }
+     424            0 :         return yield* ProviderShared.unsupportedContent("OpenAI Responses", "assistant", [
+     425            0 :           "text",
+     426            0 :           "reasoning",
+     427            0 :           "tool-call",
+     428            0 :           "tool-result",
+     429            0 :         ])
+     430            0 :       }
+     431            0 :       flushText()
+     432            0 :       continue
+     433            0 :     }
+     434              : 
+     435            0 :     for (const part of message.content) {
+     436            0 :       if (!ProviderShared.supportsContent(part, ["tool-result"]))
+     437            0 :         return yield* ProviderShared.unsupportedContent("OpenAI Responses", "tool", ["tool-result"])
+     438            0 :       input.push({
+     439            0 :         type: "function_call_output",
+     440            0 :         call_id: part.id,
+     441            0 :         output: yield* lowerToolResultOutput(part),
+     442            0 :       })
+     443            0 :     }
+     444            2 :   }
+     445              : 
+     446              :   // With store:false, OpenAI only accepts previous reasoning items when the
+     447              :   // complete item has encrypted state. Summary blocks for one item may carry
+     448              :   // that state only on the last block, so filter after they have been joined.
+     449           24 :   return store === false
+     450           12 :     ? input.filter(
+     451          101 :         (item) => !("type" in item) || item.type !== "reasoning" || typeof item.encrypted_content === "string",
+     452            1 :       )
+     453            0 :     : input
+     454            3 : })
+     455              : 
+     456           73 : const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (request: LLMRequest) {
+     457           45 :   const store = OpenAIOptions.store(request)
+     458           63 :   const promptCacheKey = OpenAIOptions.promptCacheKey(request)
+     459           56 :   const effort = OpenAIOptions.reasoningEffort(request)
+     460           58 :   if (effort && !OpenAIOptions.isReasoningEffort(effort))
+     461            2 :     return yield* invalid(`OpenAI Responses does not support reasoning effort ${effort}`)
+     462           58 :   const summary = OpenAIOptions.reasoningSummary(request)
+     463           49 :   const include = OpenAIOptions.include(request)
+     464           57 :   const verbosity = OpenAIOptions.textVerbosity(request)
+     465           59 :   const instructions = OpenAIOptions.instructions(request)
+     466           57 :   const serviceTier = OpenAIOptions.serviceTier(request)
+     467           15 :   return {
+     468           25 :     ...(instructions ? { instructions } : {}),
+     469           37 :     ...(store !== undefined ? { store } : {}),
+     470           27 :     ...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}),
+     471           20 :     ...(include ? { include } : {}),
+     472           30 :     ...(effort || summary ? { reasoning: { effort, summary } } : {}),
+     473           22 :     ...(verbosity ? { text: { verbosity } } : {}),
+     474           18 :     ...(serviceTier ? { service_tier: serviceTier } : {}),
+     475            1 :   }
+     476            3 : })
+     477              : 
+     478           71 : const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request: LLMRequest) {
+     479           40 :   const generation = request.generation
+     480           47 :   const options = yield* lowerOptions(request)
+     481           74 :   const toolSchemaCompatibility = request.model.compatibility?.toolSchema
+     482           12 :   return {
+     483           28 :     model: request.model.id,
+     484           41 :     input: yield* lowerMessages(request),
+     485            7 :     tools:
+     486           28 :       request.tools.length === 0
+     487            9 :         ? undefined
+     488            0 :         : request.tools.map((tool) =>
+     489              :             lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility)),
+     490            4 :           ),
+     491           48 :     tool_choice: request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined,
+     492           17 :     stream: true as const,
+     493           45 :     max_output_tokens: generation?.maxTokens,
+     494           41 :     temperature: generation?.temperature,
+     495           31 :     top_p: generation?.topP,
+     496            9 :     ...options,
+     497            1 :   }
+     498            3 : })
+     499              : 
+     500              : // =============================================================================
+     501              : // Stream Parsing
+     502              : // =============================================================================
+     503              : // OpenAI Responses reports `input_tokens` (inclusive total) with a
+     504              : // `cached_tokens` subset, and `output_tokens` (inclusive total) with a
+     505              : // `reasoning_tokens` subset. Pass the totals through and derive the
+     506              : // non-cached breakdown.
+     507            0 : const mapUsage = (usage: OpenAIResponsesUsage | null | undefined) => {
+     508            0 :   if (!usage) return undefined
+     509            0 :   const cached = usage.input_tokens_details?.cached_tokens
+     510            0 :   const reasoning = usage.output_tokens_details?.reasoning_tokens
+     511            0 :   const nonCached = ProviderShared.subtractTokens(usage.input_tokens, cached)
+     512            0 :   return new Usage({
+     513            0 :     inputTokens: usage.input_tokens,
+     514            0 :     outputTokens: usage.output_tokens,
+     515            0 :     nonCachedInputTokens: nonCached,
+     516            0 :     cacheReadInputTokens: cached,
+     517            0 :     reasoningTokens: reasoning,
+     518            0 :     totalTokens: ProviderShared.totalTokens(usage.input_tokens, usage.output_tokens, usage.total_tokens),
+     519            0 :     providerMetadata: { openai: usage },
+     520            2 :   })
+     521              : }
+     522              : 
+     523            0 : const mapFinishReason = (event: OpenAIResponsesEvent, hasFunctionCall: boolean): FinishReason => {
+     524            0 :   const reason = event.response?.incomplete_details?.reason
+     525            0 :   if (reason === undefined || reason === null) return hasFunctionCall ? "tool-calls" : "stop"
+     526            0 :   if (reason === "max_output_tokens") return "length"
+     527            0 :   if (reason === "content_filter") return "content-filter"
+     528            2 :   return hasFunctionCall ? "tool-calls" : "unknown"
+     529              : }
+     530              : 
+     531           23 : const openaiMetadata = (metadata: Record<string, unknown>): ProviderMetadata => ({ openai: metadata })
+     532              : 
+     533              : // Hosted tool items (provider-executed) ship their typed input + status +
+     534              : // result fields all in one item. We expose them as a `tool-call` +
+     535              : // `tool-result` pair so consumers can treat them uniformly with client tools,
+     536              : // only differentiated by `providerExecuted: true`.
+     537              : //
+     538              : // One record per OpenAI Responses item type that represents a hosted
+     539              : // (provider-executed) tool call: the common name we surface, plus an `input`
+     540              : // extractor that picks the fields the model actually populated for that tool.
+     541              : // Falling back to `{}` when an entry isn't fully typed keeps unknown tools
+     542              : // observable without rolling a per-tool schema.
+     543           23 : const HOSTED_TOOLS = {
+     544           50 :   web_search_call: { name: "web_search", input: (item) => item.action ?? {} },
+     545           66 :   web_search_preview_call: { name: "web_search_preview", input: (item) => item.action ?? {} },
+     546           52 :   file_search_call: { name: "file_search", input: (item) => ({ queries: item.queries ?? [] }) },
+     547           28 :   code_interpreter_call: {
+     548           29 :     name: "code_interpreter",
+     549            8 :     input: (item) => ({ code: item.code, container_id: item.container_id }),
+     550            4 :   },
+     551           54 :   computer_use_call: { name: "computer_use", input: (item) => item.action ?? {} },
+     552           62 :   image_generation_call: { name: "image_generation", input: () => ({}) },
+     553           15 :   mcp_call: {
+     554           16 :     name: "mcp",
+     555            8 :     input: (item) => ({ server_label: item.server_label, name: item.name, arguments: item.arguments }),
+     556            4 :   },
+     557           50 :   local_shell_call: { name: "local_shell", input: (item) => item.action ?? {} },
+     558            2 : } as const satisfies Record<
+     559              :   string,
+     560              :   { readonly name: string; readonly input: (item: OpenAIResponsesStreamItem) => unknown }
+     561              : >
+     562              : 
+     563              : type HostedToolType = keyof typeof HOSTED_TOOLS
+     564              : 
+     565            0 : const isHostedToolItem = (
+     566            0 :   item: OpenAIResponsesStreamItem,
+     567            0 : ): item is OpenAIResponsesStreamItem & { type: HostedToolType; id: string } =>
+     568            2 :   item.type in HOSTED_TOOLS && typeof item.id === "string" && item.id.length > 0
+     569              : 
+     570            0 : const isReasoningItem = (
+     571            0 :   item: OpenAIResponsesStreamItem,
+     572            0 : ): item is OpenAIResponsesStreamItem & { type: "reasoning"; id: string } =>
+     573            2 :   item.type === "reasoning" && typeof item.id === "string" && item.id.length > 0
+     574              : 
+     575              : // Round-trip the full item as the structured result so consumers can extract
+     576              : // outputs / sources / status without re-decoding.
+     577            0 : const hostedToolResult = (item: OpenAIResponsesStreamItem) => {
+     578            0 :   const isError = typeof item.error !== "undefined" && item.error !== null
+     579            2 :   return isError ? { type: "error" as const, value: item.error } : { type: "json" as const, value: item }
+     580              : }
+     581              : 
+     582            0 : const hostedToolEvents = (
+     583            0 :   item: OpenAIResponsesStreamItem & { type: HostedToolType; id: string },
+     584            0 : ): ReadonlyArray<LLMEvent> => {
+     585            0 :   const tool = HOSTED_TOOLS[item.type]
+     586            0 :   const providerMetadata = openaiMetadata({ itemId: item.id })
+     587            0 :   return [
+     588            0 :     LLMEvent.toolCall({
+     589            0 :       id: item.id,
+     590            0 :       name: tool.name,
+     591            0 :       input: tool.input(item),
+     592            0 :       providerExecuted: true,
+     593            0 :       providerMetadata,
+     594            0 :     }),
+     595            0 :     LLMEvent.toolResult({
+     596            0 :       id: item.id,
+     597            0 :       name: tool.name,
+     598            0 :       result: hostedToolResult(item),
+     599            0 :       providerExecuted: true,
+     600            0 :       providerMetadata,
+     601            0 :     }),
+     602            2 :   ]
+     603              : }
+     604              : 
+     605              : type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
+     606              : 
+     607           21 : const NO_EVENTS: StepResult["1"] = []
+     608              : 
+     609              : // `response.completed` / `response.incomplete` are clean finishes that emit a
+     610              : // `finish` event; `response.failed` is a hard failure that emits a
+     611              : // `provider-error`. All three end the stream — kept in one set so `step` and
+     612              : // the protocol's `terminal` predicate stay in sync.
+     613           97 : const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"])
+     614              : 
+     615            0 : const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
+     616            0 :   if (!event.delta) return [state, NO_EVENTS]
+     617            0 :   const events: LLMEvent[] = []
+     618            0 :   return [
+     619            0 :     { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, event.item_id ?? "text-0", event.delta) },
+     620            0 :     events,
+     621            2 :   ]
+     622              : }
+     623              : 
+     624            0 : const onReasoningDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
+     625            0 :   if (!event.delta) return [state, NO_EVENTS]
+     626            0 :   const events: LLMEvent[] = []
+     627            0 :   const itemID = event.item_id ?? "reasoning-0"
+     628            0 :   const id =
+     629            0 :     event.summary_index !== undefined || state.reasoningItems[itemID] ? `${itemID}:${event.summary_index ?? 0}` : itemID
+     630            0 :   return [
+     631            0 :     {
+     632            0 :       ...state,
+     633            0 :       lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, id, event.delta),
+     634            0 :     },
+     635            0 :     events,
+     636            2 :   ]
+     637              : }
+     638              : 
+     639           24 : const onReasoningDone = (state: ParserState, _event: OpenAIResponsesEvent): StepResult => [state, NO_EVENTS]
+     640              : 
+     641            0 : const reasoningMetadata = (item: OpenAIResponsesStreamItem & { id: string }) =>
+     642            2 :   openaiMetadata({ itemId: item.id, reasoningEncryptedContent: item.encrypted_content ?? null })
+     643              : 
+     644              : // OpenAI Responses streams reasoning items in a stable order:
+     645              : //   `output_item.added` (reasoning) →
+     646              : //     `reasoning_summary_part.added` (index=0) →
+     647              : //     `reasoning_summary_text.delta` →
+     648              : //     `reasoning_summary_part.done` (index=0) →
+     649              : //     (repeat for index>0) →
+     650              : //   `output_item.done` (reasoning).
+     651              : // The handlers below rely on this ordering: `onOutputItemAdded` seeds the
+     652              : // per-item entry, `onReasoningSummaryPartAdded` for `summary_index === 0`
+     653              : // short-circuits when the entry already exists, and higher-index handlers
+     654              : // fold against the same entry. Behaviour for out-of-order events is
+     655              : // best-effort, not guaranteed.
+     656            0 : const onOutputItemAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
+     657            0 :   const item = event.item
+     658            0 :   if (item && isReasoningItem(item)) {
+     659            0 :     const events: LLMEvent[] = []
+     660            0 :     return [
+     661            0 :       {
+     662            0 :         ...state,
+     663            0 :         lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${item.id}:0`, reasoningMetadata(item)),
+     664            0 :         reasoningItems: {
+     665            0 :           ...state.reasoningItems,
+     666            0 :           [item.id]: { encryptedContent: item.encrypted_content, summaryParts: { 0: "active" } },
+     667            0 :         },
+     668            0 :       },
+     669            0 :       events,
+     670            0 :     ]
+     671            0 :   }
+     672            0 :   if (item?.type !== "function_call" || !item.id) return [state, NO_EVENTS]
+     673            0 :   const providerMetadata = openaiMetadata({ itemId: item.id })
+     674            0 :   const events: LLMEvent[] = []
+     675            0 :   const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
+     676            0 :   return [
+     677            0 :     {
+     678            0 :       ...state,
+     679            0 :       lifecycle,
+     680            0 :       hasFunctionCall: state.hasFunctionCall,
+     681            0 :       tools: ToolStream.start(state.tools, item.id, {
+     682            0 :         id: item.call_id ?? item.id,
+     683            0 :         name: item.name ?? "",
+     684            0 :         input: item.arguments ?? "",
+     685            0 :         providerMetadata,
+     686            0 :       }),
+     687            0 :     },
+     688            0 :     [...events, LLMEvent.toolInputStart({ id: item.call_id ?? item.id, name: item.name ?? "", providerMetadata })],
+     689            2 :   ]
+     690              : }
+     691              : 
+     692            0 : const onReasoningSummaryPartAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
+     693            0 :   if (!event.item_id || event.summary_index === undefined) return [state, NO_EVENTS]
+     694            0 :   const item = state.reasoningItems[event.item_id] ?? { encryptedContent: undefined, summaryParts: {} }
+     695            0 :   if (event.summary_index === 0) {
+     696            0 :     if (state.reasoningItems[event.item_id]) return [state, NO_EVENTS]
+     697            0 :     const events: LLMEvent[] = []
+     698            0 :     return [
+     699            0 :       {
+     700            0 :         ...state,
+     701            0 :         lifecycle: Lifecycle.reasoningStart(
+     702            0 :           state.lifecycle,
+     703            0 :           events,
+     704            0 :           `${event.item_id}:0`,
+     705            0 :           openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: null }),
+     706            0 :         ),
+     707            0 :         reasoningItems: {
+     708            0 :           ...state.reasoningItems,
+     709            0 :           [event.item_id]: { ...item, summaryParts: { 0: "active" } },
+     710            0 :         },
+     711            0 :       },
+     712            0 :       events,
+     713            0 :     ]
+     714            0 :   }
+     715            0 : 
+     716            0 :   const events: LLMEvent[] = []
+     717            0 :   const closed = Object.entries(item.summaryParts)
+     718            0 :     .filter((entry) => entry[1] === "can-conclude")
+     719            0 :     .reduce(
+     720            0 :       (lifecycle, entry) =>
+     721            0 :         Lifecycle.reasoningEnd(
+     722            0 :           lifecycle,
+     723            0 :           events,
+     724            0 :           `${event.item_id}:${entry[0]}`,
+     725            0 :           openaiMetadata({ itemId: event.item_id }),
+     726            0 :         ),
+     727            0 :       state.lifecycle,
+     728            0 :     )
+     729            0 :   return [
+     730            0 :     {
+     731            0 :       ...state,
+     732            0 :       lifecycle: Lifecycle.reasoningStart(
+     733            0 :         closed,
+     734            0 :         events,
+     735            0 :         `${event.item_id}:${event.summary_index}`,
+     736            0 :         openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: item.encryptedContent ?? null }),
+     737            0 :       ),
+     738            0 :       reasoningItems: {
+     739            0 :         ...state.reasoningItems,
+     740            0 :         [event.item_id]: {
+     741            0 :           ...item,
+     742            0 :           summaryParts: {
+     743            0 :             ...Object.fromEntries(
+     744            0 :               Object.entries(item.summaryParts).map((entry) =>
+     745            0 :                 entry[1] === "can-conclude" ? [entry[0], "concluded" as const] : entry,
+     746            0 :               ),
+     747            0 :             ),
+     748            0 :             [event.summary_index]: "active",
+     749            0 :           },
+     750            0 :         },
+     751            0 :       },
+     752            0 :     },
+     753            0 :     events,
+     754            2 :   ]
+     755              : }
+     756              : 
+     757            0 : const onReasoningSummaryPartDone = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
+     758            0 :   if (!event.item_id || event.summary_index === undefined) return [state, NO_EVENTS]
+     759            0 :   const item = state.reasoningItems[event.item_id]
+     760            0 :   if (!item) return [state, NO_EVENTS]
+     761            0 :   const events: LLMEvent[] = []
+     762            0 :   return [
+     763            0 :     {
+     764            0 :       ...state,
+     765            0 :       lifecycle:
+     766            0 :         state.store !== false
+     767            0 :           ? Lifecycle.reasoningEnd(
+     768            0 :               state.lifecycle,
+     769            0 :               events,
+     770            0 :               `${event.item_id}:${event.summary_index}`,
+     771            0 :               openaiMetadata({ itemId: event.item_id }),
+     772            0 :             )
+     773            0 :           : state.lifecycle,
+     774            0 :       reasoningItems: {
+     775            0 :         ...state.reasoningItems,
+     776            0 :         [event.item_id]: {
+     777            0 :           ...item,
+     778            0 :           summaryParts: {
+     779            0 :             ...item.summaryParts,
+     780            0 :             [event.summary_index]: state.store !== false ? "concluded" : "can-conclude",
+     781            0 :           },
+     782            0 :         },
+     783            0 :       },
+     784            0 :     },
+     785            0 :     events,
+     786            2 :   ]
+     787              : }
+     788              : 
+     789            0 : const onFunctionCallArgumentsDelta = Effect.fn("OpenAIResponses.onFunctionCallArgumentsDelta")(function* (
+     790            0 :   state: ParserState,
+     791            0 :   event: OpenAIResponsesEvent,
+     792            0 : ) {
+     793            0 :   if (!event.item_id || !event.delta) return [state, NO_EVENTS] satisfies StepResult
+     794            0 :   const result = ToolStream.appendExisting(
+     795            0 :     ADAPTER,
+     796            0 :     state.tools,
+     797            0 :     event.item_id,
+     798            0 :     event.delta,
+     799            0 :     "OpenAI Responses tool argument delta is missing its tool call",
+     800            0 :   )
+     801            0 :   if (ToolStream.isError(result)) return yield* result
+     802            0 :   const events: LLMEvent[] = []
+     803            0 :   const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle
+     804            0 :   events.push(...result.events)
+     805              :   return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult
+     806            3 : })
+     807              : 
+     808            0 : const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function* (
+     809            0 :   state: ParserState,
+     810            0 :   event: OpenAIResponsesEvent,
+     811            0 : ) {
+     812            0 :   const item = event.item
+     813            0 :   if (!item) return [state, NO_EVENTS] satisfies StepResult
+     814            0 : 
+     815            0 :   if (item.type === "function_call") {
+     816            0 :     if (!item.id || !item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult
+     817            0 :     const tools = state.tools[item.id]
+     818            0 :       ? state.tools
+     819            0 :       : ToolStream.start(state.tools, item.id, { id: item.call_id, name: item.name })
+     820            0 :     const result =
+     821            0 :       item.arguments === undefined
+     822            0 :         ? yield* ToolStream.finish(ADAPTER, tools, item.id)
+     823            0 :         : yield* ToolStream.finishWithInput(ADAPTER, tools, item.id, item.arguments)
+     824            0 :     const events: LLMEvent[] = []
+     825            0 :     const resultEvents = result.events ?? []
+     826            0 :     const lifecycle = resultEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle
+     827            0 :     events.push(...resultEvents)
+     828            0 :     return [
+     829            0 :       {
+     830            0 :         ...state,
+     831            0 :         lifecycle,
+     832            0 :         hasFunctionCall: resultEvents.some(LLMEvent.is.toolCall) ? true : state.hasFunctionCall,
+     833            0 :         tools: result.tools,
+     834            0 :       },
+     835            0 :       events,
+     836            0 :     ] satisfies StepResult
+     837            0 :   }
+     838            0 : 
+     839            0 :   if (isHostedToolItem(item)) {
+     840            0 :     const events: LLMEvent[] = []
+     841            0 :     const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
+     842            0 :     events.push(...hostedToolEvents(item))
+     843            0 :     return [{ ...state, lifecycle }, events] satisfies StepResult
+     844            0 :   }
+     845            0 : 
+     846            0 :   if (isReasoningItem(item)) {
+     847            0 :     const events: LLMEvent[] = []
+     848            0 :     const providerMetadata = reasoningMetadata(item)
+     849            0 :     const reasoningItem = state.reasoningItems[item.id]
+     850            0 :     if (reasoningItem) {
+     851            0 :       const lifecycle = Object.entries(reasoningItem.summaryParts)
+     852            0 :         .filter((entry) => entry[1] === "active" || entry[1] === "can-conclude")
+     853            0 :         .reduce(
+     854            0 :           (lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${entry[0]}`, providerMetadata),
+     855            0 :           state.lifecycle,
+     856            0 :         )
+     857            0 :       const { [item.id]: _removed, ...reasoningItems } = state.reasoningItems
+     858            0 :       return [{ ...state, lifecycle, reasoningItems }, events] satisfies StepResult
+     859            0 :     }
+     860            0 :     if (!state.lifecycle.reasoning.has(item.id)) {
+     861            0 :       const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
+     862            0 :       events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata }))
+     863            0 :       events.push(LLMEvent.reasoningEnd({ id: item.id, providerMetadata }))
+     864            0 :       return [{ ...state, lifecycle }, events] satisfies StepResult
+     865            0 :     }
+     866            0 :     return [
+     867            0 :       { ...state, lifecycle: Lifecycle.reasoningEnd(state.lifecycle, events, item.id, providerMetadata) },
+     868            0 :       events,
+     869            0 :     ] satisfies StepResult
+     870            0 :   }
+     871            0 : 
+     872              :   return [state, NO_EVENTS] satisfies StepResult
+     873            3 : })
+     874              : 
+     875            0 : const onResponseFinish = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
+     876            0 :   const events: LLMEvent[] = []
+     877            0 :   const lifecycle = Lifecycle.finish(state.lifecycle, events, {
+     878            0 :     reason: mapFinishReason(event, state.hasFunctionCall),
+     879            0 :     usage: mapUsage(event.response?.usage),
+     880            0 :     providerMetadata:
+     881            0 :       event.response?.id || event.response?.service_tier
+     882            0 :         ? openaiMetadata({
+     883            0 :             responseId: event.response.id,
+     884            0 :             serviceTier: event.response.service_tier,
+     885            0 :           })
+     886            0 :         : undefined,
+     887            0 :   })
+     888            2 :   return [{ ...state, lifecycle }, events]
+     889              : }
+     890              : 
+     891              : // Build a single human-readable message from whatever the provider supplied.
+     892              : // When both code and message are present, prefix the code so consumers see
+     893              : // the failure mode (e.g. `rate_limit_exceeded: Slow down`) instead of just
+     894              : // the bare message — production rate limits and context-length failures used
+     895              : // to be indistinguishable from generic stream drops.
+     896            0 : const providerErrorMessage = (event: OpenAIResponsesEvent, fallback: string): string => {
+     897            0 :   const nested = event.response?.error ?? undefined
+     898            0 :   const message = event.message || nested?.message || undefined
+     899            0 :   const code = event.code || nested?.code || undefined
+     900            0 :   if (message && code) return `${code}: ${message}`
+     901            2 :   return message || code || fallback
+     902              : }
+     903              : 
+     904            0 : const providerError = (event: OpenAIResponsesEvent, fallback: string) => {
+     905            0 :   const code = event.code || event.response?.error?.code || undefined
+     906            0 :   const message = providerErrorMessage(event, fallback)
+     907            0 :   return LLMEvent.providerError({
+     908            0 :     message,
+     909            0 :     classification: code === "context_length_exceeded" || isContextOverflow(message) ? "context-overflow" : undefined,
+     910            2 :   })
+     911              : }
+     912              : 
+     913            0 : const onResponseFailed = (state: ParserState, event: OpenAIResponsesEvent): StepResult => [
+     914            0 :   state,
+     915              :   [providerError(event, "OpenAI Responses response failed")],
+     916            2 : ]
+     917              : 
+     918            0 : const onError = (state: ParserState, event: OpenAIResponsesEvent): StepResult => [
+     919            0 :   state,
+     920              :   [providerError(event, "OpenAI Responses stream error")],
+     921            2 : ]
+     922              : 
+     923            0 : const step = (state: ParserState, event: OpenAIResponsesEvent) => {
+     924            0 :   if (event.type === "response.output_text.delta") return Effect.succeed(onOutputTextDelta(state, event))
+     925            0 :   if (
+     926            0 :     event.type === "response.reasoning_text.delta" ||
+     927            0 :     event.type === "response.reasoning_summary.delta" ||
+     928            0 :     event.type === "response.reasoning_summary_text.delta"
+     929            0 :   )
+     930            0 :     return Effect.succeed(onReasoningDelta(state, event))
+     931            0 :   if (
+     932            0 :     event.type === "response.reasoning_text.done" ||
+     933            0 :     event.type === "response.reasoning_summary.done" ||
+     934            0 :     event.type === "response.reasoning_summary_text.done"
+     935            0 :   )
+     936            0 :     return Effect.succeed(onReasoningDone(state, event))
+     937            0 :   if (event.type === "response.reasoning_summary_part.added")
+     938            0 :     return Effect.succeed(onReasoningSummaryPartAdded(state, event))
+     939            0 :   if (event.type === "response.reasoning_summary_part.done")
+     940            0 :     return Effect.succeed(onReasoningSummaryPartDone(state, event))
+     941            0 :   if (event.type === "response.output_item.added") return Effect.succeed(onOutputItemAdded(state, event))
+     942            0 :   if (event.type === "response.function_call_arguments.delta") return onFunctionCallArgumentsDelta(state, event)
+     943            0 :   if (event.type === "response.output_item.done") return onOutputItemDone(state, event)
+     944            0 :   if (event.type === "response.completed" || event.type === "response.incomplete")
+     945            0 :     return Effect.succeed(onResponseFinish(state, event))
+     946            0 :   if (event.type === "response.failed") return Effect.succeed(onResponseFailed(state, event))
+     947            0 :   if (event.type === "error") return Effect.succeed(onError(state, event))
+     948            2 :   return Effect.succeed<StepResult>([state, NO_EVENTS])
+     949              : }
+     950              : 
+     951              : // =============================================================================
+     952              : // Protocol And OpenAI Route
+     953              : // =============================================================================
+     954              : /**
+     955              :  * The OpenAI Responses protocol — request body construction, body schema, and
+     956              :  * the streaming-event state machine. Used by native OpenAI and (once
+     957              :  * registered) Azure OpenAI Responses.
+     958              :  */
+     959           40 : export const protocol = Protocol.make({
+     960           14 :   id: ADAPTER,
+     961           11 :   body: {
+     962           32 :     schema: OpenAIResponsesBody,
+     963           19 :     from: fromRequest,
+     964            4 :   },
+     965           13 :   stream: {
+     966           52 :     event: Protocol.jsonEvent(OpenAIResponsesEvent),
+     967            0 :     initial: (request) => ({
+     968            0 :       hasFunctionCall: false,
+     969            0 :       tools: ToolStream.empty<string>(),
+     970            0 :       lifecycle: Lifecycle.initial(),
+     971            0 :       reasoningItems: {},
+     972            0 :       store: OpenAIOptions.store(request),
+     973            5 :     }),
+     974            9 :     step,
+     975           11 :     terminal: (event) => TERMINAL_TYPES.has(event.type),
+     976            2 :   },
+     977            3 : })
+     978              : 
+     979           68 : const endpoint = Endpoint.path<OpenAIResponsesBody>(PATH, { baseURL: DEFAULT_BASE_URL })
+     980           23 : const auth = Auth.none
+     981              : 
+     982           58 : export const httpTransport = HttpTransport.sseJson.with<OpenAIResponsesBody>()
+     983              : 
+     984           34 : export const route = Route.make({
+     985           14 :   id: ADAPTER,
+     986           21 :   provider: "openai",
+     987           11 :   protocol,
+     988           11 :   endpoint,
+     989            7 :   auth,
+     990           27 :   transport: httpTransport,
+     991           60 :   defaults: { providerOptions: { openai: { store: false } } },
+     992            3 : })
+     993              : 
+     994          120 : const decodeWebSocketMessage = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIResponsesWebSocketMessage))
+     995              : 
+     996            0 : const webSocketMessage = (body: OpenAIResponsesBody | Record<string, unknown>) =>
+     997            0 :   Effect.gen(function* () {
+     998            0 :     if (!ProviderShared.isRecord(body))
+     999            0 :       return yield* ProviderShared.invalidRequest("OpenAI Responses WebSocket body must be a JSON object")
+    1000            0 :     const { stream: _stream, ...message } = body
+    1001              :     return yield* decodeWebSocketMessage({ ...message, type: "response.create" })
+    1002            2 :   })
+    1003              : 
+    1004           71 : export const webSocketTransport = WebSocketTransport.jsonTransport.with<
+    1005              :   OpenAIResponsesBody,
+    1006              :   OpenAIResponsesWebSocketMessage
+    1007            3 : >({
+    1008           30 :   toMessage: webSocketMessage,
+    1009           38 :   encodeMessage: encodeWebSocketMessage,
+    1010            3 : })
+    1011              : 
+    1012           43 : export const webSocketRoute = Route.make({
+    1013           29 :   id: `${ADAPTER}-websocket`,
+    1014           21 :   provider: "openai",
+    1015           11 :   protocol,
+    1016           11 :   endpoint,
+    1017            7 :   auth,
+    1018           32 :   transport: webSocketTransport,
+    1019           60 :   defaults: { providerOptions: { openai: { store: false } } },
+    1020            3 : })
+    1021              : 
+    1022           53 : export * as OpenAIResponses from "./openai-responses"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/shared.ts.gcov.html b/packages/core/llm/src/protocols/shared.ts.gcov.html new file mode 100644 index 00000000..f0181307 --- /dev/null +++ b/packages/core/llm/src/protocols/shared.ts.gcov.html @@ -0,0 +1,402 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols/shared.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocols - shared.tsCoverageTotalHit
Test:opencode-lcov.infoLines:40.1 %17269
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           37 : import { Buffer } from "node:buffer"
+       2           48 : import { Effect, Schema, Stream } from "effect"
+       3           52 : import * as Sse from "effect/unstable/encoding/Sse"
+       4           66 : import { Headers, HttpClientRequest } from "effect/unstable/http"
+       5           90 : import {
+       6              :   InvalidProviderOutputReason,
+       7              :   InvalidRequestReason,
+       8              :   LLMError,
+       9              :   type ContentPart,
+      10              :   type LLMRequest,
+      11              :   type MediaPart,
+      12              :   type ToolFileContent,
+      13              :   type TextPart,
+      14              :   type ToolResultPart,
+      15              : } from "../schema"
+      16           43 : import { isRecord } from "../utils/record"
+      17           20 : export { isRecord }
+      18              : 
+      19           58 : export const Json = Schema.fromJsonString(Schema.Unknown)
+      20           57 : export const decodeJson = Schema.decodeUnknownSync(Json)
+      21           50 : export const encodeJson = Schema.encodeSync(Json)
+      22           38 : const isJson = Schema.is(Schema.Json)
+      23           71 : export const JsonObject = Schema.Record(Schema.String, Schema.Unknown)
+      24           77 : export const optionalArray = <const S extends Schema.Top>(schema: S) => Schema.optional(Schema.Array(schema))
+      25           77 : export const optionalNull = <const S extends Schema.Top>(schema: S) => Schema.optional(Schema.NullOr(schema))
+      26              : 
+      27              : /**
+      28              :  * Streaming tool-call accumulator. Adapters that build a tool call across
+      29              :  * multiple `tool-input-delta` chunks store the partial JSON input string here
+      30              :  * and finalize it with `parseToolInput` once the call completes.
+      31              :  */
+      32              : export interface ToolAccumulator {
+      33              :   readonly id: string
+      34              :   readonly name: string
+      35              :   readonly input: string
+      36              : }
+      37              : 
+      38              : /**
+      39              :  * `Usage.totalTokens` policy shared by every route. Honors a provider-
+      40              :  * supplied total; otherwise falls back to `inputTokens + outputTokens` only
+      41              :  * when at least one is defined. Returns `undefined` when neither input nor
+      42              :  * output is known so routes don't publish a misleading `0`.
+      43              :  *
+      44              :  * Under the additive `LLM.Usage` contract, `inputTokens` and `outputTokens`
+      45              :  * are the non-cached input and visible output only. The provider-supplied
+      46              :  * `total` is the source of truth when present; the computed fallback
+      47              :  * under-counts cache and reasoning by design and exists mainly so
+      48              :  * Anthropic-style providers (which don't surface a total) still get a
+      49              :  * sensible aggregate on the input + output axes.
+      50              :  */
+      51           26 : export const totalTokens = (
+      52           13 :   inputTokens: number | undefined,
+      53           14 :   outputTokens: number | undefined,
+      54           10 :   total: number | undefined,
+      55            3 : ) => {
+      56           39 :   if (total !== undefined) return total
+      57            0 :   if (inputTokens === undefined && outputTokens === undefined) return undefined
+      58            2 :   return (inputTokens ?? 0) + (outputTokens ?? 0)
+      59              : }
+      60              : 
+      61              : /**
+      62              :  * Subtract `subtrahend` from `total`, clamping to zero if the provider
+      63              :  * reports a non-sensical breakdown (e.g. `cached_tokens > prompt_tokens`).
+      64              :  * Used by protocol mappers when deriving a non-overlapping breakdown field
+      65              :  * from a provider's inclusive total — `nonCachedInputTokens` from
+      66              :  * `inputTokens - cacheReadInputTokens - cacheWriteInputTokens`.
+      67              :  *
+      68              :  * If `total` is `undefined`, returns `undefined` (we don't fabricate
+      69              :  * counts). If `subtrahend` is `undefined`, returns `total` unchanged. The
+      70              :  * provider-native breakdown stays available on `Usage.native` for debugging.
+      71              :  */
+      72           54 : export const subtractTokens = (total: number | undefined, subtrahend: number | undefined): number | undefined => {
+      73           29 :   if (total === undefined) return undefined
+      74           34 :   if (subtrahend === undefined) return total
+      75           40 :   return Math.max(0, total - subtrahend)
+      76              : }
+      77              : 
+      78              : /**
+      79              :  * Sum a list of optional token counts, returning `undefined` only when
+      80              :  * every value is `undefined` (so we don't fabricate a `0`). Used by
+      81              :  * protocol mappers to derive the inclusive `inputTokens` total from a
+      82              :  * provider that natively reports a non-overlapping breakdown
+      83              :  * (e.g. Anthropic, whose `input_tokens` is already non-cached only).
+      84              :  */
+      85            0 : export const sumTokens = (...values: ReadonlyArray<number | undefined>): number | undefined => {
+      86            0 :   if (values.every((value) => value === undefined)) return undefined
+      87            2 :   return values.reduce((acc: number, value) => acc + (value ?? 0), 0)
+      88              : }
+      89              : 
+      90            0 : export const eventError = (route: string, message: string, raw?: string) =>
+      91            0 :   new LLMError({
+      92            0 :     module: "ProviderShared",
+      93            0 :     method: "stream",
+      94              :     reason: new InvalidProviderOutputReason({ route, message, raw }),
+      95            2 :   })
+      96              : 
+      97            0 : export const parseJson = (route: string, input: string, message: string) =>
+      98            0 :   Effect.try({
+      99            0 :     try: () => decodeJson(input),
+     100              :     catch: () => eventError(route, message, input),
+     101            2 :   })
+     102              : 
+     103              : /**
+     104              :  * Join the `text` field of a list of parts with newlines. Used by routes
+     105              :  * that flatten system / message content arrays into a single provider string
+     106              :  * (OpenAI Chat `system` content, OpenAI Responses `system` content, Gemini
+     107              :  * `systemInstruction.parts[].text`).
+     108              :  */
+     109           24 : export const joinText = (parts: ReadonlyArray<{ readonly text: string }>) => parts.map((part) => part.text).join("\n")
+     110              : 
+     111            0 : const escapeSystemUpdateText = (text: string) =>
+     112            2 :   text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
+     113              : 
+     114              : /**
+     115              :  * Stable fallback representation for chronological `Message.system(...)`
+     116              :  * updates on routes that do not support that privileged role natively. The
+     117              :  * wrapper remains visibly lower-authority user text, preserves the original
+     118              :  * temporal position, and XML-escapes content so it cannot close the wrapper.
+     119              :  */
+     120            0 : export const wrapSystemUpdate = (parts: ReadonlyArray<{ readonly text: string }>) =>
+     121            2 :   `<system-update>\n${escapeSystemUpdateText(joinText(parts))}\n</system-update>`
+     122              : 
+     123              : /**
+     124              :  * Chronological system updates deliberately accept text only. Do not insert
+     125              :  * raw retrieved, tool, or web content into privileged updates: keep untrusted
+     126              :  * data in ordinary user/tool messages instead.
+     127              :  */
+     128            0 : export const systemUpdateText = Effect.fn("ProviderShared.systemUpdateText")(function* (
+     129            0 :   route: string,
+     130            0 :   message: LLMRequest["messages"][number],
+     131            0 : ) {
+     132            0 :   const content: TextPart[] = []
+     133            0 :   for (const part of message.content) {
+     134            0 :     if (!supportsContent(part, ["text"])) return yield* unsupportedContent(route, "system", ["text"])
+     135            0 :     content.push(part)
+     136            0 :   }
+     137              :   return content
+     138            3 : })
+     139              : 
+     140              : /** Lower an unsupported privileged update into visible, in-order user text. */
+     141            0 : export const wrappedSystemUpdate = Effect.fn("ProviderShared.wrappedSystemUpdate")(function* (
+     142            0 :   route: string,
+     143            0 :   message: LLMRequest["messages"][number],
+     144            0 : ) {
+     145            0 :   const content = yield* systemUpdateText(route, message)
+     146              :   return { type: "text" as const, text: wrapSystemUpdate(content), cache: content.at(-1)?.cache }
+     147            3 : })
+     148              : 
+     149              : /**
+     150              :  * Parse the streamed JSON input of a tool call. Treats an empty string as
+     151              :  * `"{}"` — providers occasionally finish a tool call without ever emitting
+     152              :  * input deltas (e.g. zero-arg tools). The error message is uniform across
+     153              :  * routes: `Invalid JSON input for <route> tool call <name>`.
+     154              :  */
+     155            0 : export const parseToolInput = (route: string, name: string, raw: string) =>
+     156            2 :   parseJson(route, raw || "{}", `Invalid JSON input for ${route} tool call ${name}`)
+     157              : 
+     158           82 : export const IMAGE_MIMES = ["image/png", "image/jpeg", "image/gif", "image/webp"] as const
+     159           74 : export const VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"] as const
+     160          108 : export const AUDIO_MIMES = ["audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac"] as const
+     161           76 : export const MEDIA_MIMES = [...IMAGE_MIMES, ...VIDEO_MIMES, ...AUDIO_MIMES] as const
+     162           48 : export const MAX_MEDIA_ENCODED_BYTES = 28 * 1024 * 1024
+     163           48 : export const MAX_MEDIA_DECODED_BYTES = 20 * 1024 * 1024
+     164              : 
+     165           89 : const base64Pattern = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
+     166              : 
+     167              : export interface ValidatedMedia {
+     168              :   readonly mime: string
+     169              :   readonly base64: string
+     170              :   readonly dataUrl: string
+     171              :   readonly bytes: Uint8Array
+     172              : }
+     173              : 
+     174            0 : export const validateMedia = Effect.fn("ProviderShared.validateMedia")(function* (
+     175            0 :   route: string,
+     176            0 :   part: MediaPart,
+     177            0 :   supportedMimes: ReadonlySet<string>,
+     178            0 : ) {
+     179            0 :   const mime = part.mediaType.toLowerCase()
+     180            0 :   if (!supportedMimes.has(mime)) return yield* invalidRequest(`${route} does not support media type ${part.mediaType}`)
+     181            0 : 
+     182            0 :   let base64: string
+     183            0 :   if (typeof part.data !== "string") {
+     184            0 :     if (part.data.byteLength > MAX_MEDIA_DECODED_BYTES)
+     185            0 :       return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_DECODED_BYTES} byte decoded limit`)
+     186            0 :     base64 = Buffer.from(part.data).toString("base64")
+     187            0 :   } else if (part.data.startsWith("data:")) {
+     188            0 :     const match = /^data:([^;,]+);base64,([A-Za-z0-9+/]*={0,2})$/s.exec(part.data)
+     189            0 :     if (!match) return yield* invalidRequest(`${route} media data URL must contain valid base64`)
+     190            0 :     if (match[1]!.toLowerCase() !== mime)
+     191            0 :       return yield* invalidRequest(`${route} media type ${part.mediaType} does not match data URL type ${match[1]}`)
+     192            0 :     base64 = match[2]!
+     193            0 :   } else {
+     194            0 :     base64 = part.data
+     195            0 :   }
+     196            0 : 
+     197            0 :   if (Buffer.byteLength(base64, "utf8") > MAX_MEDIA_ENCODED_BYTES)
+     198            0 :     return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_ENCODED_BYTES} byte encoded limit`)
+     199            0 :   if (!base64 || base64.length % 4 !== 0 || !base64Pattern.test(base64))
+     200            0 :     return yield* invalidRequest(`${route} media must contain valid base64`)
+     201            0 :   const bytes = Buffer.from(base64, "base64")
+     202            0 :   if (bytes.byteLength > MAX_MEDIA_DECODED_BYTES)
+     203            0 :     return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_DECODED_BYTES} byte decoded limit`)
+     204            0 :   if (bytes.toString("base64") !== base64) return yield* invalidRequest(`${route} media must contain canonical base64`)
+     205              :   return { mime, base64, dataUrl: `data:${mime};base64,${base64}`, bytes } satisfies ValidatedMedia
+     206            3 : })
+     207              : 
+     208            0 : export const validateToolFile = (route: string, part: ToolFileContent, supportedMimes: ReadonlySet<string>) =>
+     209            2 :   validateMedia(route, { type: "media", mediaType: part.mime, data: part.uri, filename: part.name }, supportedMimes)
+     210              : 
+     211           62 : export const trimBaseUrl = (value: string) => value.replace(/\/+$/, "")
+     212              : 
+     213            0 : export const toolResultText = (part: ToolResultPart) => {
+     214            0 :   if (part.result.type === "text") return String(part.result.value)
+     215            0 :   if (part.result.type === "error") {
+     216            0 :     const value = part.result.value
+     217            0 :     const prototype =
+     218            0 :       typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value)
+     219            0 :     const structured = Array.isArray(value) || prototype === Object.prototype || prototype === null
+     220            0 :     return structured && isJson(value) ? encodeJson(value) : String(value)
+     221            0 :   }
+     222            2 :   return encodeJson(part.result.value)
+     223              : }
+     224              : 
+     225            0 : export const errorText = (error: unknown) => {
+     226            0 :   if (error instanceof Error) return error.message
+     227            0 :   if (typeof error === "string") return error
+     228            0 :   if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") return String(error)
+     229            0 :   if (error === null) return "null"
+     230            0 :   if (error === undefined) return "undefined"
+     231            2 :   return "Unknown stream error"
+     232              : }
+     233              : 
+     234              : /**
+     235              :  * `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
+     236              :  * decoder, and drops empty / `[DONE]` keep-alive events so the downstream
+     237              :  * `decodeChunk` sees one JSON string per element. The SSE channel emits a
+     238              :  * `Retry` control event on its error channel; we drop it here (we don't
+     239              :  * implement client-driven retries) so the public error channel stays
+     240              :  * `LLMError`.
+     241              :  */
+     242           35 : export const sseFraming = (bytes: Stream.Stream<Uint8Array, LLMError>): Stream.Stream<string, LLMError> =>
+     243           11 :   bytes.pipe(
+     244           21 :     Stream.decodeText(),
+     245           41 :     Stream.pipeThroughChannel(Sse.decode()),
+     246           27 :     Stream.catchTag("Retry", () => Stream.empty),
+     247           74 :     Stream.filter((event) => event.data.length > 0 && event.data !== "[DONE]"),
+     248           31 :     Stream.map((event) => event.data),
+     249            2 :   )
+     250              : 
+     251              : /**
+     252              :  * Canonical invalid-request constructor. Lift one-line `const invalid =
+     253              :  * (message) => invalidRequest(message)` aliases out of every
+     254              :  * route so the error constructor lives in one place. If we ever extend
+     255              :  * `InvalidRequestReason` with route context or trace metadata, the change
+     256              :  * lands here.
+     257              :  */
+     258            0 : export const invalidRequest = (message: string) =>
+     259            0 :   new LLMError({
+     260            0 :     module: "ProviderShared",
+     261            0 :     method: "request",
+     262              :     reason: new InvalidRequestReason({ message }),
+     263            2 :   })
+     264              : 
+     265            0 : export const matchToolChoice = <Auto, None, Required, Tool>(
+     266            0 :   route: string,
+     267            0 :   toolChoice: NonNullable<LLMRequest["toolChoice"]>,
+     268            0 :   cases: {
+     269            0 :     readonly auto: () => Auto
+     270            0 :     readonly none: () => None
+     271            0 :     readonly required: () => Required
+     272            0 :     readonly tool: (name: string) => Tool
+     273            0 :   },
+     274            0 : ) =>
+     275            0 :   Effect.gen(function* () {
+     276            0 :     if (toolChoice.type === "auto") return cases.auto()
+     277            0 :     if (toolChoice.type === "none") return cases.none()
+     278            0 :     if (toolChoice.type === "required") return cases.required()
+     279            0 :     if (!toolChoice.name) return yield* invalidRequest(`${route} tool choice requires a tool name`)
+     280              :     return cases.tool(toolChoice.name)
+     281            2 :   })
+     282              : 
+     283              : type ContentType = ContentPart["type"]
+     284              : 
+     285            0 : const formatContentTypes = (types: ReadonlyArray<ContentType>) => {
+     286            0 :   if (types.length <= 1) return types[0] ?? ""
+     287            0 :   if (types.length === 2) return `${types[0]} and ${types[1]}`
+     288            2 :   return `${types.slice(0, -1).join(", ")}, and ${types.at(-1)}`
+     289              : }
+     290              : 
+     291            0 : export const supportsContent = <const Type extends ContentType>(
+     292            0 :   part: ContentPart,
+     293            0 :   types: ReadonlyArray<Type>,
+     294            2 : ): part is Extract<ContentPart, { readonly type: Type }> => (types as ReadonlyArray<ContentType>).includes(part.type)
+     295              : 
+     296            0 : export const unsupportedContent = (
+     297            0 :   route: string,
+     298            0 :   role: LLMRequest["messages"][number]["role"],
+     299            0 :   types: ReadonlyArray<ContentType>,
+     300            2 : ) => invalidRequest(`${route} ${role} messages only support ${formatContentTypes(types)} content for now`)
+     301              : 
+     302              : /**
+     303              :  * Build a `validate` step from a Schema decoder. Replaces the per-route
+     304              :  * lambda body `(payload) => decode(payload).pipe(Effect.mapError((e) =>
+     305              :  * invalid(e.message)))`. Any decode error is translated into
+     306              :  * `LLMError` carrying the original parse-error message.
+     307              :  */
+     308           26 : export const validateWith =
+     309           11 :   <A, I, E extends { readonly message: string }>(decode: (input: I) => Effect.Effect<A, E>) =>
+     310           13 :   (payload: I) =>
+     311           40 :     decode(payload).pipe(Effect.mapError((error) => invalidRequest(error.message)))
+     312              : 
+     313              : /**
+     314              :  * Build an HTTP POST with a JSON body. Sets `content-type: application/json`
+     315              :  * automatically after caller-supplied headers so routes cannot accidentally
+     316              :  * send JSON with a stale content type. The body is passed pre-encoded so
+     317              :  * routes can choose between
+     318              :  * `Schema.encodeSync(payload)` and `ProviderShared.encodeJson(payload)`.
+     319              :  */
+     320           33 : export const jsonPost = (input: { readonly url: string; readonly body: string; readonly headers?: Headers.Input }) =>
+     321           39 :   HttpClientRequest.post(input.url).pipe(
+     322          113 :     HttpClientRequest.setHeaders(Headers.set(Headers.fromInput(input.headers), "content-type", "application/json")),
+     323           58 :     HttpClientRequest.bodyText(input.body, "application/json"),
+     324            2 :   )
+     325              : 
+     326           42 : export * as ProviderShared from "./shared"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/utils/cache.ts.gcov.html b/packages/core/llm/src/protocols/utils/cache.ts.gcov.html new file mode 100644 index 00000000..43a71172 --- /dev/null +++ b/packages/core/llm/src/protocols/utils/cache.ts.gcov.html @@ -0,0 +1,92 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols/utils/cache.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocols/utils - cache.tsCoverageTotalHit
Test:opencode-lcov.infoLines:66.7 %32
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : // Shared helpers for provider cache-marker lowering. Anthropic and Bedrock
+       2              : // both enforce a 4-breakpoint cap per request and accept the same `5m`/`1h`
+       3              : // TTL buckets, so the counter and TTL mapping live here.
+       4              : 
+       5              : export interface Breakpoints {
+       6              :   remaining: number
+       7              :   dropped: number
+       8              : }
+       9              : 
+      10           30 : export const newBreakpoints = (cap: number): Breakpoints => ({ remaining: cap, dropped: 0 })
+      11              : 
+      12              : // Returns `"1h"` for any `ttlSeconds >= 3600`, otherwise `undefined` (the
+      13              : // provider default 5m). Anthropic & Bedrock both treat anything shorter than
+      14              : // an hour as 5m.
+      15            0 : export const ttlBucket = (ttlSeconds: number | undefined): "1h" | undefined =>
+      16            1 :   ttlSeconds !== undefined && ttlSeconds >= 3600 ? "1h" : undefined
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/utils/gemini-tool-schema.ts.gcov.html b/packages/core/llm/src/protocols/utils/gemini-tool-schema.ts.gcov.html new file mode 100644 index 00000000..39ed4855 --- /dev/null +++ b/packages/core/llm/src/protocols/utils/gemini-tool-schema.ts.gcov.html @@ -0,0 +1,175 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols/utils/gemini-tool-schema.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocols/utils - gemini-tool-schema.tsCoverageTotalHit
Test:opencode-lcov.infoLines:27.9 %8624
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           46 : import { isRecord } from "../../utils/record"
+       2              : 
+       3              : // Gemini accepts a JSON Schema-like dialect for tool parameters, but rejects a
+       4              : // handful of common JSON Schema shapes. Keep this projection isolated so the
+       5              : // Gemini protocol file still reads like the other protocol modules.
+       6           29 : const SCHEMA_INTENT_KEYS = [
+       7            9 :   "type",
+       8           15 :   "properties",
+       9           10 :   "items",
+      10           16 :   "prefixItems",
+      11            9 :   "enum",
+      12           10 :   "const",
+      13            9 :   "$ref",
+      14           25 :   "additionalProperties",
+      15           22 :   "patternProperties",
+      16           13 :   "required",
+      17            8 :   "not",
+      18            7 :   "if",
+      19            9 :   "then",
+      20            7 :   "else",
+      21            2 : ]
+      22              : 
+      23            0 : const hasCombiner = (schema: unknown) =>
+      24            2 :   isRecord(schema) && (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf) || Array.isArray(schema.allOf))
+      25              : 
+      26            0 : const hasSchemaIntent = (schema: unknown) =>
+      27            2 :   isRecord(schema) && (hasCombiner(schema) || SCHEMA_INTENT_KEYS.some((key) => key in schema))
+      28              : 
+      29            0 : const sanitizeNode = (schema: unknown): unknown => {
+      30            0 :   if (!isRecord(schema)) return Array.isArray(schema) ? schema.map(sanitizeNode) : schema
+      31            0 : 
+      32            0 :   const result: Record<string, unknown> = Object.fromEntries(
+      33            0 :     Object.entries(schema).map(([key, value]) => [
+      34            0 :       key,
+      35            0 :       key === "enum" && Array.isArray(value) ? value.map(String) : sanitizeNode(value),
+      36            0 :     ]),
+      37            0 :   )
+      38            0 : 
+      39            0 :   if (Array.isArray(result.enum) && (result.type === "integer" || result.type === "number")) result.type = "string"
+      40            0 : 
+      41            0 :   const properties = result.properties
+      42            0 :   if (result.type === "object" && isRecord(properties) && Array.isArray(result.required)) {
+      43            0 :     result.required = result.required.filter((field) => typeof field === "string" && field in properties)
+      44            0 :   }
+      45            0 : 
+      46            0 :   if (result.type === "array" && !hasCombiner(result)) {
+      47            0 :     result.items = result.items ?? {}
+      48            0 :     if (isRecord(result.items) && !hasSchemaIntent(result.items)) result.items = { ...result.items, type: "string" }
+      49            0 :   }
+      50            0 : 
+      51            0 :   if (typeof result.type === "string" && result.type !== "object" && !hasCombiner(result)) {
+      52            0 :     delete result.properties
+      53            0 :     delete result.required
+      54            0 :   }
+      55            0 : 
+      56            2 :   return result
+      57              : }
+      58              : 
+      59            0 : const emptyObjectSchema = (schema: Record<string, unknown>) =>
+      60            0 :   schema.type === "object" &&
+      61            0 :   (!isRecord(schema.properties) || Object.keys(schema.properties).length === 0) &&
+      62            2 :   !schema.additionalProperties
+      63              : 
+      64            0 : const projectNode = (schema: unknown): Record<string, unknown> | undefined => {
+      65            0 :   if (!isRecord(schema)) return undefined
+      66            0 :   if (emptyObjectSchema(schema)) return undefined
+      67            0 :   return Object.fromEntries(
+      68            0 :     [
+      69            0 :       ["description", schema.description],
+      70            0 :       ["required", schema.required],
+      71            0 :       ["format", schema.format],
+      72            0 :       ["type", Array.isArray(schema.type) ? schema.type.filter((type) => type !== "null")[0] : schema.type],
+      73            0 :       ["nullable", Array.isArray(schema.type) && schema.type.includes("null") ? true : undefined],
+      74            0 :       ["enum", schema.const !== undefined ? [schema.const] : schema.enum],
+      75            0 :       [
+      76            0 :         "properties",
+      77            0 :         isRecord(schema.properties)
+      78            0 :           ? Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [key, projectNode(value)]))
+      79            0 :           : undefined,
+      80            0 :       ],
+      81            0 :       [
+      82            0 :         "items",
+      83            0 :         Array.isArray(schema.items)
+      84            0 :           ? schema.items.map(projectNode)
+      85            0 :           : schema.items === undefined
+      86            0 :             ? undefined
+      87            0 :             : projectNode(schema.items),
+      88            0 :       ],
+      89            0 :       ["allOf", Array.isArray(schema.allOf) ? schema.allOf.map(projectNode) : undefined],
+      90            0 :       ["anyOf", Array.isArray(schema.anyOf) ? schema.anyOf.map(projectNode) : undefined],
+      91            0 :       ["oneOf", Array.isArray(schema.oneOf) ? schema.oneOf.map(projectNode) : undefined],
+      92            0 :       ["minLength", schema.minLength],
+      93            0 :     ].filter((entry) => entry[1] !== undefined),
+      94            2 :   )
+      95              : }
+      96              : 
+      97           23 : export const convert = (schema: unknown) => projectNode(sanitizeNode(schema))
+      98              : 
+      99           56 : export * as GeminiToolSchema from "./gemini-tool-schema"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/utils/index-sort-f.html b/packages/core/llm/src/protocols/utils/index-sort-f.html new file mode 100644 index 00000000..b40de6ea --- /dev/null +++ b/packages/core/llm/src/protocols/utils/index-sort-f.html @@ -0,0 +1,143 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols/utils + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocols/utilsCoverageTotalHit
Test:opencode-lcov.infoLines:38.7 %416161
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
cache.ts +
66.7%66.7%
+
66.7 %32
gemini-tool-schema.ts +
27.9%27.9%
+
27.9 %8624
lifecycle.ts +
62.2%62.2%
+
62.2 %7446
openai-options.ts +
96.6%96.6%
+
96.6 %5856
tool-schema.ts +
21.4%21.4%
+
21.4 %7015
tool-stream.ts +
14.4%14.4%
+
14.4 %12518
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/utils/index-sort-l.html b/packages/core/llm/src/protocols/utils/index-sort-l.html new file mode 100644 index 00000000..6196b460 --- /dev/null +++ b/packages/core/llm/src/protocols/utils/index-sort-l.html @@ -0,0 +1,143 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols/utils + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocols/utilsCoverageTotalHit
Test:opencode-lcov.infoLines:38.7 %416161
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
tool-stream.ts +
14.4%14.4%
+
14.4 %12518
tool-schema.ts +
21.4%21.4%
+
21.4 %7015
gemini-tool-schema.ts +
27.9%27.9%
+
27.9 %8624
lifecycle.ts +
62.2%62.2%
+
62.2 %7446
cache.ts +
66.7%66.7%
+
66.7 %32
openai-options.ts +
96.6%96.6%
+
96.6 %5856
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/utils/index.html b/packages/core/llm/src/protocols/utils/index.html new file mode 100644 index 00000000..af1e4982 --- /dev/null +++ b/packages/core/llm/src/protocols/utils/index.html @@ -0,0 +1,143 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols/utils + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocols/utilsCoverageTotalHit
Test:opencode-lcov.infoLines:38.7 %416161
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
cache.ts +
66.7%66.7%
+
66.7 %32
gemini-tool-schema.ts +
27.9%27.9%
+
27.9 %8624
lifecycle.ts +
62.2%62.2%
+
62.2 %7446
openai-options.ts +
96.6%96.6%
+
96.6 %5856
tool-schema.ts +
21.4%21.4%
+
21.4 %7015
tool-stream.ts +
14.4%14.4%
+
14.4 %12518
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/utils/lifecycle.ts.gcov.html b/packages/core/llm/src/protocols/utils/lifecycle.ts.gcov.html new file mode 100644 index 00000000..53bcd15c --- /dev/null +++ b/packages/core/llm/src/protocols/utils/lifecycle.ts.gcov.html @@ -0,0 +1,178 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols/utils/lifecycle.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocols/utils - lifecycle.tsCoverageTotalHit
Test:opencode-lcov.infoLines:62.2 %7446
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           40 : import { LLMEvent, type FinishReason, type ProviderMetadata, type Usage } from "../../schema"
+       2              : 
+       3              : export interface State {
+       4              :   readonly stepStarted: boolean
+       5              :   readonly text: ReadonlySet<string>
+       6              :   readonly reasoning: ReadonlySet<string>
+       7              : }
+       8              : 
+       9           87 : export const initial = (): State => ({ stepStarted: false, text: new Set(), reasoning: new Set() })
+      10              : 
+      11           45 : export const stepStart = (state: State, events: LLMEvent[]): State => {
+      12           39 :   if (state.stepStarted) return state
+      13           48 :   events.push(LLMEvent.stepStart({ index: 0 }))
+      14           40 :   return { ...state, stepStarted: true }
+      15              : }
+      16              : 
+      17           55 : export const textDelta = (state: State, events: LLMEvent[], id: string, text: string): State => {
+      18           43 :   const stepped = stepStart(state, events)
+      19           30 :   if (stepped.text.has(id)) {
+      20           50 :     events.push(LLMEvent.textDelta({ id, text }))
+      21           14 :     return stepped
+      22            2 :   }
+      23           76 :   events.push(LLMEvent.textStart({ id }), LLMEvent.textDelta({ id, text }))
+      24           61 :   return { ...stepped, text: new Set([...stepped.text, id]) }
+      25              : }
+      26              : 
+      27            0 : export const reasoningStart = (
+      28            0 :   state: State,
+      29            0 :   events: LLMEvent[],
+      30            0 :   id: string,
+      31            0 :   providerMetadata?: ProviderMetadata,
+      32            0 : ): State => {
+      33            0 :   if (state.reasoning.has(id)) return state
+      34            0 :   const stepped = stepStart(state, events)
+      35            0 :   events.push(LLMEvent.reasoningStart({ id, providerMetadata }))
+      36            2 :   return { ...stepped, reasoning: new Set([...stepped.reasoning, id]) }
+      37              : }
+      38              : 
+      39            0 : export const reasoningDelta = (
+      40            0 :   state: State,
+      41            0 :   events: LLMEvent[],
+      42            0 :   id: string,
+      43            0 :   text: string,
+      44            0 :   providerMetadata?: ProviderMetadata,
+      45            0 : ): State => {
+      46            0 :   const started = reasoningStart(state, events, id, providerMetadata)
+      47            0 :   events.push(LLMEvent.reasoningDelta({ id, text }))
+      48            2 :   return started
+      49              : }
+      50              : 
+      51           27 : export const reasoningEnd = (
+      52            7 :   state: State,
+      53            8 :   events: LLMEvent[],
+      54            4 :   id: string,
+      55           21 :   providerMetadata?: ProviderMetadata,
+      56            3 : ): State => {
+      57           44 :   if (!state.reasoning.has(id)) return state
+      58            0 :   const stepped = stepStart(state, events)
+      59            0 :   events.push(LLMEvent.reasoningEnd({ id, providerMetadata }))
+      60            0 :   const reasoning = new Set(stepped.reasoning)
+      61            0 :   reasoning.delete(id)
+      62            2 :   return { ...stepped, reasoning }
+      63              : }
+      64              : 
+      65            0 : export const textEnd = (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata): State => {
+      66            0 :   if (!state.text.has(id)) return state
+      67            0 :   const stepped = stepStart(state, events)
+      68            0 :   events.push(LLMEvent.textEnd({ id, providerMetadata }))
+      69            0 :   const text = new Set(stepped.text)
+      70            0 :   text.delete(id)
+      71            2 :   return { ...stepped, text }
+      72              : }
+      73              : 
+      74           44 : const closeOpenBlocks = (state: State, events: LLMEvent[]): State => {
+      75           38 :   for (const id of state.reasoning) events.push(LLMEvent.reasoningEnd({ id }))
+      76           70 :   for (const id of state.text) events.push(LLMEvent.textEnd({ id }))
+      77           56 :   return { ...state, text: new Set(), reasoning: new Set() }
+      78              : }
+      79              : 
+      80           21 : export const finish = (
+      81            7 :   state: State,
+      82            8 :   events: LLMEvent[],
+      83           10 :   input: {
+      84              :     readonly reason: FinishReason
+      85              :     readonly usage?: Usage
+      86              :     readonly providerMetadata?: ProviderMetadata
+      87              :   },
+      88            3 : ): State => {
+      89           68 :   const stepped = closeOpenBlocks(stepStart(state, events), events)
+      90           12 :   events.push(
+      91           25 :     LLMEvent.stepFinish({
+      92           13 :       index: 0,
+      93           25 :       reason: input.reason,
+      94           23 :       usage: input.usage,
+      95           42 :       providerMetadata: input.providerMetadata,
+      96            4 :     }),
+      97           22 :     LLMEvent.finish(input),
+      98            4 :   )
+      99           43 :   return { ...stepped, stepStarted: false }
+     100              : }
+     101              : 
+     102           40 : export * as Lifecycle from "./lifecycle"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/utils/openai-options.ts.gcov.html b/packages/core/llm/src/protocols/utils/openai-options.ts.gcov.html new file mode 100644 index 00000000..eed1749e --- /dev/null +++ b/packages/core/llm/src/protocols/utils/openai-options.ts.gcov.html @@ -0,0 +1,169 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols/utils/openai-options.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocols/utils - openai-options.tsCoverageTotalHit
Test:opencode-lcov.infoLines:96.6 %5856
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Schema } from "effect"
+       2              : import type { LLMRequest, ReasoningEffort, TextVerbosity as TextVerbosityValue } from "../../schema"
+       3           63 : import { ReasoningEfforts, TextVerbosity } from "../../schema"
+       4              : 
+       5           60 : export const OpenAIReasoningEfforts = ReasoningEfforts.filter(
+       6           27 :   (effort): effort is Exclude<ReasoningEffort, "max"> => effort !== "max",
+       7            3 : )
+       8              : export type OpenAIReasoningEffort = (typeof OpenAIReasoningEfforts)[number]
+       9              : 
+      10              : // Mirrors OpenAI's `ResponseIncludable` union from the official SDK. Keep this
+      11              : // in lockstep with `openai-node/src/resources/responses/responses.ts`.
+      12           43 : export const OpenAIResponseIncludables = [
+      13           29 :   "file_search_call.results",
+      14           28 :   "web_search_call.results",
+      15           35 :   "web_search_call.action.sources",
+      16           34 :   "message.input_image.image_url",
+      17           42 :   "computer_call_output.output.image_url",
+      18           34 :   "code_interpreter_call.outputs",
+      19           32 :   "reasoning.encrypted_content",
+      20           31 :   "message.output_text.logprobs",
+      21            2 : ] as const
+      22              : export type OpenAIResponseIncludable = (typeof OpenAIResponseIncludables)[number]
+      23           74 : export const OpenAIServiceTiers = ["auto", "default", "flex", "priority"] as const
+      24              : export type OpenAIServiceTier = (typeof OpenAIServiceTiers)[number]
+      25              : 
+      26           52 : const REASONING_EFFORTS = new Set<string>(ReasoningEfforts)
+      27           65 : const OPENAI_REASONING_EFFORTS = new Set<string>(OpenAIReasoningEfforts)
+      28           58 : const TEXT_VERBOSITY = new Set<string>(["low", "medium", "high"])
+      29           55 : const INCLUDABLES = new Set<string>(OpenAIResponseIncludables)
+      30           50 : const SERVICE_TIERS = new Set<string>(OpenAIServiceTiers)
+      31              : 
+      32           77 : export const OpenAIReasoningEffort = Schema.Literals(OpenAIReasoningEfforts)
+      33           49 : export const OpenAITextVerbosity = TextVerbosity
+      34           83 : export const OpenAIResponseIncludable = Schema.Literals(OpenAIResponseIncludables)
+      35           69 : export const OpenAIServiceTier = Schema.Literals(OpenAIServiceTiers)
+      36              : 
+      37           39 : const isAnyReasoningEffort = (effort: unknown): effort is ReasoningEffort =>
+      38           60 :   typeof effort === "string" && REASONING_EFFORTS.has(effort)
+      39              : 
+      40            0 : export const isReasoningEffort = (effort: unknown): effort is OpenAIReasoningEffort =>
+      41            2 :   typeof effort === "string" && OPENAI_REASONING_EFFORTS.has(effort)
+      42              : 
+      43           33 : const isTextVerbosity = (value: unknown): value is TextVerbosityValue =>
+      44           55 :   typeof value === "string" && TEXT_VERBOSITY.has(value)
+      45              : 
+      46           59 : const options = (request: LLMRequest) => request.providerOptions?.openai
+      47              : 
+      48           35 : export const store = (request: LLMRequest): boolean | undefined => {
+      49           40 :   const value = options(request)?.store
+      50           53 :   return typeof value === "boolean" ? value : undefined
+      51              : }
+      52              : 
+      53           45 : export const reasoningEffort = (request: LLMRequest): ReasoningEffort | undefined => {
+      54           50 :   const value = options(request)?.reasoningEffort
+      55           49 :   return isAnyReasoningEffort(value) ? value : undefined
+      56              : }
+      57              : 
+      58           43 : export const reasoningSummary = (request: LLMRequest): "auto" | undefined =>
+      59           60 :   options(request)?.reasoningSummary === "auto" ? "auto" : undefined
+      60              : 
+      61              : // Resolve the OpenAI Responses `include` field. Filters out unknown
+      62              : // includable values defensively so a typo in upstream config drops the
+      63              : // invalid entry instead of poisoning the wire body. An empty array (either
+      64              : // passed directly or produced by filtering) is treated as "no include" and
+      65              : // returns undefined so the request body omits the field entirely.
+      66           37 : export const include = (request: LLMRequest): ReadonlyArray<OpenAIResponseIncludable> | undefined => {
+      67           42 :   const value = options(request)?.include
+      68           35 :   if (!Array.isArray(value)) return undefined
+      69            0 :   const filtered = value.filter((entry): entry is OpenAIResponseIncludable => INCLUDABLES.has(entry))
+      70            2 :   return filtered.length > 0 ? filtered : undefined
+      71              : }
+      72              : 
+      73           44 : export const promptCacheKey = (request: LLMRequest) => {
+      74           49 :   const value = options(request)?.promptCacheKey
+      75           47 :   return typeof value === "string" ? value : undefined
+      76              : }
+      77              : 
+      78           43 : export const textVerbosity = (request: LLMRequest) => {
+      79           48 :   const value = options(request)?.textVerbosity
+      80           44 :   return isTextVerbosity(value) ? value : undefined
+      81              : }
+      82              : 
+      83           41 : export const serviceTier = (request: LLMRequest) => {
+      84           46 :   const value = options(request)?.serviceTier
+      85           75 :   return typeof value === "string" && SERVICE_TIERS.has(value) ? (value as OpenAIServiceTier) : undefined
+      86              : }
+      87              : 
+      88           42 : export const instructions = (request: LLMRequest) => {
+      89           47 :   const value = options(request)?.instructions
+      90           47 :   return typeof value === "string" ? value : undefined
+      91              : }
+      92              : 
+      93           49 : export * as OpenAIOptions from "./openai-options"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/utils/tool-schema.ts.gcov.html b/packages/core/llm/src/protocols/utils/tool-schema.ts.gcov.html new file mode 100644 index 00000000..4342db8f --- /dev/null +++ b/packages/core/llm/src/protocols/utils/tool-schema.ts.gcov.html @@ -0,0 +1,162 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols/utils/tool-schema.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocols/utils - tool-schema.tsCoverageTotalHit
Test:opencode-lcov.infoLines:21.4 %7015
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import type { JsonSchema, ModelToolSchemaCompatibility } from "../../schema"
+       2           46 : import { isRecord } from "../../utils/record"
+       3           56 : import { GeminiToolSchema } from "./gemini-tool-schema"
+       4              : 
+       5            0 : const removeNullSchemas = (value: unknown): unknown => {
+       6            0 :   if (Array.isArray(value)) return value.map(removeNullSchemas)
+       7            0 :   if (!isRecord(value)) return value
+       8            0 :   const fields = Object.fromEntries(
+       9            0 :     Object.entries(value)
+      10            0 :       .filter(([key]) => key !== "anyOf")
+      11            0 :       .map(([key, field]) => [key, removeNullSchemas(field)]),
+      12            0 :   )
+      13            0 :   if (!Array.isArray(value.anyOf)) return fields
+      14            0 :   const variants = value.anyOf.filter((variant) => !isRecord(variant) || variant.type !== "null").map(removeNullSchemas)
+      15            0 :   if (variants.length === 1 && isRecord(variants[0])) return { ...fields, ...variants[0] }
+      16            2 :   return { ...fields, anyOf: variants }
+      17              : }
+      18              : 
+      19            0 : const tupleItemsSchema = (items: ReadonlyArray<unknown>) => {
+      20            0 :   const projected = items.map(moonshotNode)
+      21            0 :   if (projected.length === 0) return {}
+      22            0 :   if (projected.length === 1) return projected[0]
+      23            2 :   return { anyOf: projected }
+      24              : }
+      25              : 
+      26            0 : const moonshotNode = (schema: unknown): unknown => {
+      27            0 :   if (Array.isArray(schema)) return schema.map(moonshotNode)
+      28            0 :   if (!isRecord(schema)) return schema
+      29            0 :   if (typeof schema.$ref === "string") return { $ref: schema.$ref }
+      30            0 :   return Object.fromEntries(
+      31            0 :     Object.entries(schema).flatMap(([key, value]) => {
+      32            0 :       if (key === "items" && Array.isArray(value)) return [[key, tupleItemsSchema(value)]]
+      33            0 :       if (key === "prefixItems") {
+      34            0 :         if ("items" in schema) return []
+      35            0 :         return [["items", tupleItemsSchema(Array.isArray(value) ? value : [])]]
+      36            0 :       }
+      37            0 :       if (key === "unevaluatedItems") return []
+      38            0 :       return [[key, moonshotNode(value)]]
+      39            0 :     }),
+      40            2 :   )
+      41              : }
+      42              : 
+      43            0 : const moonshot = (schema: JsonSchema): JsonSchema => {
+      44            0 :   const projected = moonshotNode(schema)
+      45            2 :   return isRecord(projected) ? projected : {}
+      46              : }
+      47              : 
+      48            0 : const openAI = (schema: JsonSchema): JsonSchema => {
+      49            0 :   const variants = Array.isArray(schema.anyOf) ? schema.anyOf.filter(isRecord) : []
+      50            0 :   const flattened =
+      51            0 :     variants.length === 0
+      52            0 :       ? { ...schema, type: "object" }
+      53            0 :       : {
+      54            0 :           ...Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "anyOf")),
+      55            0 :           type: "object",
+      56            0 :           properties: variants.reduce(
+      57            0 :             (properties, variant) => ({ ...(isRecord(variant.properties) ? variant.properties : {}), ...properties }),
+      58            0 :             {},
+      59            0 :           ),
+      60            0 :           additionalProperties: false,
+      61            0 :         }
+      62            0 :   const normalized = removeNullSchemas(flattened)
+      63            2 :   return isRecord(normalized) ? normalized : { type: "object" }
+      64              : }
+      65              : 
+      66           15 : const gemini = (schema: JsonSchema): JsonSchema => GeminiToolSchema.convert(schema) ?? {}
+      67              : 
+      68            0 : const modelCompatibility = (
+      69            0 :   schema: JsonSchema,
+      70            0 :   compatibility: ModelToolSchemaCompatibility | undefined,
+      71            0 : ): JsonSchema => {
+      72            0 :   if (compatibility === undefined) return schema
+      73            0 :   switch (compatibility) {
+      74            0 :     case "gemini":
+      75            0 :       return gemini(schema)
+      76            0 :     case "moonshot":
+      77            2 :       return moonshot(schema)
+      78              :   }
+      79              : }
+      80              : 
+      81           38 : export const ToolSchemaProjection = {
+      82            9 :   gemini,
+      83           21 :   modelCompatibility,
+      84           11 :   moonshot,
+      85            7 :   openAI,
+      86            1 : } as const
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/protocols/utils/tool-stream.ts.gcov.html b/packages/core/llm/src/protocols/utils/tool-stream.ts.gcov.html new file mode 100644 index 00000000..d055e218 --- /dev/null +++ b/packages/core/llm/src/protocols/utils/tool-stream.ts.gcov.html @@ -0,0 +1,294 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/protocols/utils/tool-stream.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/protocols/utils - tool-stream.tsCoverageTotalHit
Test:opencode-lcov.infoLines:14.4 %12518
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Effect } from "effect"
+       2           50 : import { LLMError, LLMEvent, type ProviderMetadata, type ToolCall } from "../../schema"
+       3           55 : import { eventError, parseToolInput, type ToolAccumulator } from "../shared"
+       4              : 
+       5              : type StreamKey = string | number
+       6              : 
+       7              : /**
+       8              :  * One pending streamed tool call. Providers emit the tool identity and JSON
+       9              :  * argument text across separate chunks; `input` is the raw JSON string collected
+      10              :  * so far, not the parsed object.
+      11              :  */
+      12              : export interface PendingTool extends ToolAccumulator {
+      13              :   readonly providerExecuted?: boolean
+      14              :   readonly providerMetadata?: ProviderMetadata
+      15              : }
+      16              : 
+      17              : /**
+      18              :  * Sparse parser state keyed by the provider's stream-local tool identifier.
+      19              :  *
+      20              :  * This key is not the final tool-call id (`call_...`). It is the id/index the
+      21              :  * provider uses while streaming a partial call: OpenAI Chat / Anthropic /
+      22              :  * Bedrock use numeric content indexes, while OpenAI Responses uses string
+      23              :  * `item_id`s. The generic keeps each protocol internally consistent.
+      24              :  */
+      25              : export type State<K extends StreamKey> = Partial<Record<K, PendingTool>>
+      26              : 
+      27              : /**
+      28              :  * Result of adding argument text to one pending tool call. It returns both the
+      29              :  * next `tools` state and the updated `tool` because parsers often need the
+      30              :  * current id/name immediately. `events` contains lifecycle and delta events
+      31              :  * produced by the append; metadata-only deltas update identity without output.
+      32              :  */
+      33              : export interface AppendOutcome<K extends StreamKey> {
+      34              :   readonly tools: State<K>
+      35              :   readonly tool: PendingTool
+      36              :   readonly events: ReadonlyArray<LLMEvent>
+      37              : }
+      38              : 
+      39              : /** Create empty accumulator state for one provider stream. */
+      40           30 : export const empty = <K extends StreamKey>(): State<K> => ({})
+      41              : 
+      42            0 : const withTool = <K extends StreamKey>(tools: State<K>, key: K, tool: PendingTool): State<K> => {
+      43            2 :   return { ...tools, [key]: tool }
+      44              : }
+      45              : 
+      46            0 : const withoutTool = <K extends StreamKey>(tools: State<K>, key: K): State<K> => {
+      47            0 :   const next = { ...tools }
+      48            0 :   delete next[key]
+      49            2 :   return next
+      50              : }
+      51              : 
+      52            0 : const inputStart = (tool: PendingTool) =>
+      53            0 :   LLMEvent.toolInputStart({
+      54            0 :     id: tool.id,
+      55            0 :     name: tool.name,
+      56              :     providerMetadata: tool.providerMetadata,
+      57            2 :   })
+      58              : 
+      59            0 : const inputDelta = (tool: PendingTool, text: string) =>
+      60            0 :   LLMEvent.toolInputDelta({
+      61            0 :     id: tool.id,
+      62            0 :     name: tool.name,
+      63              :     text,
+      64            2 :   })
+      65              : 
+      66            0 : const toolCall = (route: string, tool: PendingTool, inputOverride?: string) =>
+      67            0 :   parseToolInput(route, tool.name, inputOverride ?? tool.input).pipe(
+      68            0 :     Effect.map(
+      69            0 :       (input): ToolCall =>
+      70            0 :         LLMEvent.toolCall({
+      71            0 :           id: tool.id,
+      72            0 :           name: tool.name,
+      73            0 :           input,
+      74            0 :           providerExecuted: tool.providerExecuted ? true : undefined,
+      75            0 :           providerMetadata: tool.providerMetadata,
+      76            0 :         }),
+      77              :     ),
+      78            2 :   )
+      79              : 
+      80              : /** Store the updated tool and produce the optional public delta event. */
+      81            0 : const appendTool = <K extends StreamKey>(
+      82            0 :   tools: State<K>,
+      83            0 :   key: K,
+      84            0 :   tool: PendingTool,
+      85            0 :   text: string,
+      86            0 : ): AppendOutcome<K> => {
+      87            0 :   const events: LLMEvent[] = []
+      88            0 :   if (!tools[key]) events.push(inputStart(tool))
+      89            0 :   if (text.length > 0) events.push(inputDelta(tool, text))
+      90            0 :   return {
+      91            0 :     tools: withTool(tools, key, tool),
+      92            0 :     tool,
+      93            0 :     events,
+      94            2 :   }
+      95              : }
+      96              : 
+      97            0 : export const isError = <K extends StreamKey>(result: AppendOutcome<K> | LLMError): result is LLMError =>
+      98            2 :   result instanceof LLMError
+      99              : 
+     100              : /**
+     101              :  * Register a tool call whose start event arrived before any argument deltas.
+     102              :  * Used by Anthropic `content_block_start`, Bedrock `contentBlockStart`, and
+     103              :  * OpenAI Responses `response.output_item.added`.
+     104              :  */
+     105            0 : export const start = <K extends StreamKey>(
+     106            0 :   tools: State<K>,
+     107            0 :   key: K,
+     108            0 :   tool: Omit<PendingTool, "input"> & { readonly input?: string },
+     109            2 : ) => withTool(tools, key, { ...tool, input: tool.input ?? "" })
+     110              : 
+     111              : /**
+     112              :  * Append a streamed argument delta, starting the tool if this provider encodes
+     113              :  * identity on the first delta instead of a separate start event. OpenAI Chat has
+     114              :  * this shape: `tool_calls[].index` is the stream key, and `id` / `name` may only
+     115              :  * appear on the first delta for that index.
+     116              :  */
+     117            0 : export const appendOrStart = <K extends StreamKey>(
+     118            0 :   route: string,
+     119            0 :   tools: State<K>,
+     120            0 :   key: K,
+     121            0 :   delta: { readonly id?: string; readonly name?: string; readonly text: string },
+     122            0 :   missingToolMessage: string,
+     123            0 : ): AppendOutcome<K> | LLMError => {
+     124            0 :   const current = tools[key]
+     125            0 :   const id = delta.id ?? current?.id
+     126            0 :   const name = delta.name ?? current?.name
+     127            0 :   if (!id || !name) return eventError(route, missingToolMessage)
+     128            0 : 
+     129            0 :   const tool = {
+     130            0 :     id,
+     131            0 :     name,
+     132            0 :     input: `${current?.input ?? ""}${delta.text}`,
+     133            0 :     providerExecuted: current?.providerExecuted,
+     134            0 :     providerMetadata: current?.providerMetadata,
+     135            0 :   }
+     136            0 :   if (current && delta.text.length === 0 && current.id === id && current.name === name)
+     137            0 :     return { tools, tool: current, events: [] }
+     138            2 :   return appendTool(tools, key, tool, delta.text)
+     139              : }
+     140              : 
+     141              : /**
+     142              :  * Append argument text to a tool that must already have been started. This keeps
+     143              :  * protocols honest when their stream grammar promises a start event before any
+     144              :  * argument delta.
+     145              :  */
+     146            0 : export const appendExisting = <K extends StreamKey>(
+     147            0 :   route: string,
+     148            0 :   tools: State<K>,
+     149            0 :   key: K,
+     150            0 :   text: string,
+     151            0 :   missingToolMessage: string,
+     152            0 : ): AppendOutcome<K> | LLMError => {
+     153            0 :   const current = tools[key]
+     154            0 :   if (!current) return eventError(route, missingToolMessage)
+     155            0 :   if (text.length === 0) return { tools, tool: current, events: [] }
+     156            2 :   return appendTool(tools, key, { ...current, input: `${current.input}${text}` }, text)
+     157              : }
+     158              : 
+     159              : /**
+     160              :  * Finalize one pending tool call: parse the accumulated raw JSON, remove it
+     161              :  * from state, and return the optional public `tool-call` event. Missing keys are
+     162              :  * a no-op because some providers emit stop events for non-tool content blocks.
+     163              :  */
+     164            0 : export const finish = <K extends StreamKey>(route: string, tools: State<K>, key: K) =>
+     165            0 :   Effect.gen(function* () {
+     166            0 :     const tool = tools[key]
+     167            0 :     if (!tool) return { tools }
+     168            0 :     return {
+     169            0 :       tools: withoutTool(tools, key),
+     170            0 :       events: [
+     171            0 :         LLMEvent.toolInputEnd({ id: tool.id, name: tool.name, providerMetadata: tool.providerMetadata }),
+     172            0 :         yield* toolCall(route, tool),
+     173            0 :       ],
+     174              :     }
+     175            2 :   })
+     176              : 
+     177              : /**
+     178              :  * Finalize one pending tool call with an authoritative final input string.
+     179              :  * OpenAI Responses can send accumulated deltas and then repeat the completed
+     180              :  * arguments on `response.output_item.done`; the final value wins.
+     181              :  */
+     182            0 : export const finishWithInput = <K extends StreamKey>(route: string, tools: State<K>, key: K, input: string) =>
+     183            0 :   Effect.gen(function* () {
+     184            0 :     const tool = tools[key]
+     185            0 :     if (!tool) return { tools }
+     186            0 :     return {
+     187            0 :       tools: withoutTool(tools, key),
+     188            0 :       events: [
+     189            0 :         LLMEvent.toolInputEnd({ id: tool.id, name: tool.name, providerMetadata: tool.providerMetadata }),
+     190            0 :         yield* toolCall(route, tool, input),
+     191            0 :       ],
+     192              :     }
+     193            2 :   })
+     194              : 
+     195              : /**
+     196              :  * Finalize every pending tool call at once. OpenAI Chat has this shape: it does
+     197              :  * not emit per-tool stop events, so all accumulated calls finish when the choice
+     198              :  * receives a terminal `finish_reason`.
+     199              :  */
+     200            0 : export const finishAll = <K extends StreamKey>(route: string, tools: State<K>) =>
+     201            0 :   Effect.gen(function* () {
+     202            0 :     const pending = Object.values<PendingTool | undefined>(tools).filter(
+     203            0 :       (tool): tool is PendingTool => tool !== undefined,
+     204            0 :     )
+     205            0 :     return {
+     206            0 :       tools: empty<K>(),
+     207            0 :       events: yield* Effect.forEach(pending, (tool) =>
+     208            0 :         toolCall(route, tool).pipe(
+     209            0 :           Effect.map((call) => [
+     210            0 :             LLMEvent.toolInputEnd({ id: tool.id, name: tool.name, providerMetadata: tool.providerMetadata }),
+     211            0 :             call,
+     212            0 :           ]),
+     213            0 :         ),
+     214            0 :       ).pipe(Effect.map((events) => events.flat())),
+     215              :     }
+     216            2 :   })
+     217              : 
+     218           43 : export * as ToolStream from "./tool-stream"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/provider-error.ts.gcov.html b/packages/core/llm/src/provider-error.ts.gcov.html new file mode 100644 index 00000000..1120abae --- /dev/null +++ b/packages/core/llm/src/provider-error.ts.gcov.html @@ -0,0 +1,119 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/provider-error.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src - provider-error.tsCoverageTotalHit
Test:opencode-lcov.infoLines:94.9 %3937
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Schema } from "effect"
+       2           56 : import { LLMError, ProviderErrorEvent } from "./schema"
+       3              : 
+       4           19 : const patterns = [
+       5           24 :   /prompt is too long/i,
+       6           23 :   /request_too_large/i,
+       7           43 :   /input is too long for requested model/i,
+       8           32 :   /exceeds the context window/i,
+       9           95 :   /exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i,
+      10           44 :   /input token count.*exceeds the maximum/i,
+      11           52 :   /tokens in request more than max tokens allowed/i,
+      12           34 :   /maximum prompt length is \d+/i,
+      13           39 :   /reduce the length of the messages/i,
+      14           42 :   /maximum context length is \d+ tokens/i,
+      15           69 :   /exceeds (?:the )?maximum allowed input length of [\d,]+ tokens?/i,
+      16           84 :   /input \(\d+ tokens\) is longer than the model'?s context length \(\d+ tokens\)/i,
+      17           30 :   /exceeds the limit of \d+/i,
+      18           40 :   /exceeds the available context size/i,
+      19           37 :   /greater than the context length/i,
+      20           34 :   /context window exceeds limit/i,
+      21           32 :   /exceeded model token limit/i,
+      22           35 :   /context[_ ]length[_ ]exceeded/i,
+      23           30 :   /request entity too large/i,
+      24           39 :   /context length is only \d+ tokens/i,
+      25           43 :   /input length.*exceeds.*context length/i,
+      26           55 :   /prompt too long; exceeded (?:max )?context length/i,
+      27           57 :   /too large for model with \d+ maximum context length/i,
+      28           82 :   /prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i,
+      29           35 :   /model_context_window_exceeded/i,
+      30           21 :   /too many tokens/i,
+      31           24 :   /token limit exceeded/i,
+      32            2 : ]
+      33              : 
+      34          102 : const exclusions = [/^(throttling error|service unavailable):/i, /rate limit/i, /too many requests/i]
+      35              : 
+      36            0 : export const isContextOverflow = (message: string) =>
+      37            0 :   !exclusions.some((pattern) => pattern.test(message)) &&
+      38            2 :   (patterns.some((pattern) => pattern.test(message)) || /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message))
+      39              : 
+      40           51 : export const isContextOverflowFailure = (failure: unknown) =>
+      41           29 :   failure instanceof LLMError
+      42           98 :     ? failure.reason._tag === "InvalidRequest" && failure.reason.classification === "context-overflow"
+      43           88 :     : Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/provider.ts.gcov.html b/packages/core/llm/src/provider.ts.gcov.html new file mode 100644 index 00000000..57268b09 --- /dev/null +++ b/packages/core/llm/src/provider.ts.gcov.html @@ -0,0 +1,112 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/provider.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src - provider.tsCoverageTotalHit
Test:opencode-lcov.infoLines:50.0 %42
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import type { Model, ModelID, ProviderID } from "./schema"
+       2              : 
+       3              : export type ModelOptions = Pick<Model.Input, "defaults" | "compatibility">
+       4              : 
+       5              : /**
+       6              :  * Advanced structural provider definition helper. Built-in providers should
+       7              :  * prefer explicit `configure(options).model(id)` facades so deployment config is
+       8              :  * chosen before model selection. The optional `apis` map remains for external
+       9              :  * structural providers that expose multiple route selectors behind one provider.
+      10              :  */
+      11              : export type ModelFactory<Options extends ModelOptions = ModelOptions> = (
+      12              :   id: string | ModelID,
+      13              :   options?: Options,
+      14              : ) => Model
+      15              : 
+      16              : type AnyModelFactory = (...args: never[]) => Model
+      17              : 
+      18              : export interface Definition<Factory extends AnyModelFactory = ModelFactory> {
+      19              :   readonly id: ProviderID
+      20              :   readonly model: Factory
+      21              :   readonly apis?: Record<string, AnyModelFactory>
+      22              : }
+      23              : 
+      24              : type DefinitionShape = {
+      25              :   readonly id: ProviderID
+      26              :   readonly model: (...args: never[]) => Model
+      27              :   readonly apis?: Record<string, (...args: never[]) => Model>
+      28              : }
+      29              : 
+      30              : type NoExtraFields<Input, Shape> = Input & Record<Exclude<keyof Input, keyof Shape>, never>
+      31              : 
+      32            0 : export const make = <DefinitionType extends DefinitionShape>(
+      33            0 :   definition: NoExtraFields<DefinitionType, DefinitionShape>,
+      34            2 : ) => definition
+      35              : 
+      36           38 : export * as Provider from "./provider"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/auth-options.ts.gcov.html b/packages/core/llm/src/route/auth-options.ts.gcov.html new file mode 100644 index 00000000..94019043 --- /dev/null +++ b/packages/core/llm/src/route/auth-options.ts.gcov.html @@ -0,0 +1,133 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route/auth-options.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/route - auth-options.tsCoverageTotalHit
Test:opencode-lcov.infoLines:30.0 %103
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import type { Config, Redacted } from "effect"
+       2           30 : import { Auth } from "./auth"
+       3              : 
+       4              : export type ApiKeyMode = "optional" | "required"
+       5              : 
+       6              : export type AuthOverride = {
+       7              :   readonly auth: Auth
+       8              :   readonly apiKey?: never
+       9              : }
+      10              : 
+      11              : export type OptionalApiKeyAuth = {
+      12              :   readonly apiKey?: string | Redacted.Redacted<string> | Config.Config<string | Redacted.Redacted<string>>
+      13              :   readonly auth?: never
+      14              : }
+      15              : 
+      16              : export type RequiredApiKeyAuth = {
+      17              :   readonly apiKey: string | Redacted.Redacted<string> | Config.Config<string | Redacted.Redacted<string>>
+      18              :   readonly auth?: never
+      19              : }
+      20              : 
+      21              : export type ProviderAuthOption<Mode extends ApiKeyMode> =
+      22              :   | AuthOverride
+      23              :   | (Mode extends "optional" ? OptionalApiKeyAuth : RequiredApiKeyAuth)
+      24              : 
+      25              : export type ModelOptions<Base, Mode extends ApiKeyMode> = Omit<Base, "apiKey" | "auth"> & ProviderAuthOption<Mode>
+      26              : 
+      27              : export type ModelArgs<Base, Mode extends ApiKeyMode> = Mode extends "optional"
+      28              :   ? readonly [options?: ModelOptions<Base, Mode>]
+      29              :   : readonly [options: ModelOptions<Base, Mode>]
+      30              : 
+      31              : export type ModelFactory<Base, Mode extends ApiKeyMode, Model> = (id: string, ...args: ModelArgs<Base, Mode>) => Model
+      32              : 
+      33              : /**
+      34              :  * Require at least one of the keys in `T`. Use for option shapes where any
+      35              :  * subset of fields is acceptable but at least one must be present (e.g. Azure
+      36              :  * accepts `resourceName` or `baseURL`).
+      37              :  */
+      38              : export type AtLeastOne<T> = {
+      39              :   [K in keyof T]: Required<Pick<T, K>> & Partial<Omit<T, K>>
+      40              : }[keyof T]
+      41              : 
+      42              : /**
+      43              :  * Standard bearer-auth resolution for providers: honor an explicit `auth`
+      44              :  * override, otherwise resolve `apiKey` (option > config var) and apply it as
+      45              :  * a bearer token.
+      46              :  */
+      47            0 : export const bearer = (options: ProviderAuthOption<"optional">, envVar: string | ReadonlyArray<string>): Auth => {
+      48            0 :   if ("auth" in options && options.auth) return options.auth
+      49            0 :   return (Array.isArray(envVar) ? envVar : [envVar])
+      50            0 :     .reduce(
+      51            0 :       (auth, name) => auth.orElse(Auth.config(name)),
+      52            0 :       Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey"),
+      53            0 :     )
+      54            2 :     .bearer()
+      55              : }
+      56              : 
+      57           45 : export * as AuthOptions from "./auth-options"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/auth.ts.gcov.html b/packages/core/llm/src/route/auth.ts.gcov.html new file mode 100644 index 00000000..f2c0f416 --- /dev/null +++ b/packages/core/llm/src/route/auth.ts.gcov.html @@ -0,0 +1,232 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route/auth.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/route - auth.tsCoverageTotalHit
Test:opencode-lcov.infoLines:73.3 %8663
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import { Config, Effect, Redacted } from "effect"
+       2              : import { Headers } from "effect/unstable/http"
+       3              : import { AuthenticationReason, InvalidRequestReason, LLMError, type LLMRequest } from "../schema"
+       4              : 
+       5              : export class MissingCredentialError extends Error {
+       6            1 :   readonly _tag = "MissingCredentialError"
+       7              : 
+       8            0 :   constructor(readonly source: string) {
+       9           48 :     super(`Missing auth credential: ${source}`)
+      10              :   }
+      11            2 : }
+      12              : 
+      13              : export type CredentialError = MissingCredentialError | Config.ConfigError
+      14              : export type AuthError = CredentialError | LLMError
+      15              : type Secret = string | Redacted.Redacted | Config.Config<string | Redacted.Redacted>
+      16              : 
+      17              : export interface AuthInput {
+      18              :   readonly request: LLMRequest
+      19              :   readonly method: "POST" | "GET"
+      20              :   readonly url: string
+      21              :   readonly body: string
+      22              :   readonly headers: Headers.Headers
+      23              : }
+      24              : 
+      25              : export interface Credential {
+      26              :   readonly load: Effect.Effect<Redacted.Redacted, CredentialError>
+      27              :   readonly orElse: (that: Credential) => Credential
+      28              :   readonly bearer: () => Auth
+      29              :   readonly header: (name: string) => Auth
+      30              :   readonly pipe: <A>(f: (self: Credential) => A) => A
+      31              : }
+      32              : 
+      33              : export interface Auth {
+      34              :   readonly apply: (input: AuthInput) => Effect.Effect<Headers.Headers, AuthError>
+      35              :   readonly andThen: (that: Auth) => Auth
+      36              :   readonly orElse: (that: Auth) => Auth
+      37              :   readonly pipe: <A>(f: (self: Auth) => A) => A
+      38              : }
+      39              : 
+      40            0 : export const isAuth = (input: unknown): input is Auth =>
+      41          106 :   typeof input === "object" && input !== null && "apply" in input && typeof input.apply === "function"
+      42              : 
+      43           61 : const credential = (load: Effect.Effect<Redacted.Redacted, CredentialError>): Credential => {
+      44           36 :   const self: Credential = {
+      45           18 :     load,
+      46           87 :     orElse: (that) => credential(load.pipe(Effect.catch(() => that.load))),
+      47          180 :     bearer: () => fromCredential(self, (secret) => ({ authorization: `Bearer ${secret}` })),
+      48          119 :     header: (name) => fromCredential(self, (secret) => ({ [name]: secret })),
+      49           29 :     pipe: (f) => f(self),
+      50            8 :   }
+      51           28 :   return self
+      52              : }
+      53              : 
+      54           51 : const auth = (apply: Auth["apply"]): Auth => {
+      55           36 :   const self: Auth = {
+      56           20 :     apply,
+      57            0 :     andThen: (that) =>
+      58          108 :       auth((input) => apply(input).pipe(Effect.flatMap((headers) => that.apply({ ...input, headers })))),
+      59          108 :     orElse: (that) => auth((input) => apply(input).pipe(Effect.catch(() => that.apply(input)))),
+      60           29 :     pipe: (f) => f(self),
+      61            8 :   }
+      62           28 :   return self
+      63              : }
+      64              : 
+      65           83 : const fromCredential = (source: Credential, render: (secret: string) => Headers.Input) =>
+      66           31 :   auth((input) =>
+      67          203 :     source.load.pipe(Effect.map((secret) => Headers.setAll(input.headers, render(Redacted.value(secret))))),
+      68            5 :   )
+      69              : 
+      70           85 : const secretEffect = (secret: string | Redacted.Redacted, source: string) => {
+      71          147 :   const redacted = typeof secret === "string" ? Redacted.make(secret) : secret
+      72          138 :   if (Redacted.value(redacted) === "") return Effect.fail(new MissingCredentialError(source))
+      73           68 :   return Effect.succeed(redacted)
+      74              : }
+      75              : 
+      76          101 : const credentialFromSecret = (secret: Secret, source: string) => {
+      77          228 :   if (typeof secret === "string" || Redacted.isRedacted(secret)) return credential(secretEffect(secret, source))
+      78           18 :   return credential(
+      79            0 :     Effect.gen(function* () {
+      80           53 :       return yield* secretEffect(yield* secret, source)
+      81            1 :     }),
+      82            7 :   )
+      83              : }
+      84              : 
+      85          174 : export const value = (secret: string, source = "value") => credentialFromSecret(secret, source)
+      86              : 
+      87            0 : export const optional = (secret: Secret | undefined, source = "optional value") =>
+      88            0 :   secret === undefined
+      89            0 :     ? credential(Effect.fail(new MissingCredentialError(source)))
+      90           40 :     : credentialFromSecret(secret, source)
+      91              : 
+      92          104 : export const config = (name: string) => credentialFromSecret(Config.redacted(name), name)
+      93              : 
+      94           71 : export const effect = (load: Effect.Effect<Redacted.Redacted, CredentialError>) => credential(load)
+      95              : 
+      96           93 : export const none = auth((input) => Effect.succeed(input.headers))
+      97              : 
+      98            0 : export const headers = (input: Headers.Input) =>
+      99           81 :   auth((inputAuth) => Effect.succeed(Headers.setAll(inputAuth.headers, input)))
+     100              : 
+     101          123 : export const remove = (name: string) => auth((input) => Effect.succeed(Headers.remove(input.headers, name)))
+     102              : 
+     103           67 : export const custom = (apply: (input: AuthInput) => Effect.Effect<Headers.Headers, LLMError>) => auth(apply)
+     104              : 
+     105           64 : export const passthrough = none
+     106              : 
+     107           69 : const credentialInput = (source: Secret | Credential) =>
+     108          173 :   typeof source === "string" || Redacted.isRedacted(source) || Config.isConfig(source)
+     109           79 :     ? credentialFromSecret(source, "value")
+     110           16 :     : source
+     111              : 
+     112              : export function bearer(source: Secret | Credential): Auth
+     113           50 : export function bearer(source: Secret | Credential) {
+     114           82 :   return credentialInput(source).bearer()
+     115              : }
+     116              : 
+     117           58 : export const apiKey = bearer
+     118              : 
+     119              : export function header(name: string): (source: Secret | Credential) => Auth
+     120              : export function header(name: string, source: Secret | Credential): Auth
+     121           62 : export function header(name: string, source?: Secret | Credential) {
+     122           56 :   if (source === undefined) {
+     123           54 :     return (next: Secret | Credential) => credentialInput(next).header(name)
+     124            5 :   }
+     125           90 :   return credentialInput(source).header(name)
+     126              : }
+     127              : 
+     128              : export function bearerHeader(name: string): (source: Secret | Credential) => Auth
+     129              : export function bearerHeader(name: string, source: Secret | Credential): Auth
+     130            0 : export function bearerHeader(name: string, source?: Secret | Credential) {
+     131            0 :   const render = (input: Secret | Credential) =>
+     132            0 :     fromCredential(credentialInput(input), (secret) => ({ [name]: `Bearer ${secret}` }))
+     133            0 :   if (source === undefined) return render
+     134           25 :   return render(source)
+     135              : }
+     136              : 
+     137            0 : const toLLMError = (error: AuthError): LLMError => {
+     138            0 :   if (error instanceof MissingCredentialError || error instanceof Config.ConfigError) {
+     139            0 :     return new LLMError({
+     140            0 :       module: "Auth",
+     141            0 :       method: "apply",
+     142            0 :       reason:
+     143            0 :         error instanceof MissingCredentialError
+     144            0 :           ? new AuthenticationReason({ message: error.message, kind: "missing" })
+     145            0 :           : new InvalidRequestReason({ message: `Failed to resolve auth config: ${error.message}` }),
+     146            0 :     })
+     147            0 :   }
+     148           18 :   return error
+     149              : }
+     150              : 
+     151           45 : export const toEffect =
+     152           21 :   (input: Auth) =>
+     153           30 :   (authInput: AuthInput): Effect.Effect<Headers.Headers, LLMError> =>
+     154          116 :     input.apply(authInput).pipe(Effect.mapError(toLLMError))
+     155              : 
+     156           61 : export * as Auth from "./auth"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/client.ts.gcov.html b/packages/core/llm/src/route/client.ts.gcov.html new file mode 100644 index 00000000..764a2b70 --- /dev/null +++ b/packages/core/llm/src/route/client.ts.gcov.html @@ -0,0 +1,512 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route/client.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/route - client.tsCoverageTotalHit
Test:opencode-lcov.infoLines:88.3 %222196
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           33 : import { Cause, Context, Effect, Layer, Schema, Stream } from "effect"
+       2           40 : import * as Option from "effect/Option"
+       3           30 : import { Auth, type Auth as AuthDef } from "./auth"
+       4           38 : import { Endpoint, type EndpointPatch } from "./endpoint"
+       5           45 : import { RequestExecutor } from "./executor"
+       6              : import type { Framing } from "./framing"
+       7           44 : import { HttpTransport } from "./transport"
+       8              : import type { Transport, TransportRuntime } from "./transport"
+       9           48 : import { WebSocketExecutor } from "./transport"
+      10              : import type { Protocol } from "./protocol"
+      11           51 : import { applyCachePolicy } from "../cache-policy"
+      12           54 : import * as ProviderShared from "../protocols/shared"
+      13              : import type { LLMError, LLMEvent, PreparedRequestOf, ProtocolID, ProviderOptions } from "../schema"
+      14          104 : import {
+      15              :   GenerationOptions,
+      16              :   HttpOptions,
+      17              :   LLMRequest,
+      18              :   LLMResponse,
+      19              :   Model,
+      20              :   ModelLimits,
+      21          131 :   LLMError as LLMErrorClass,
+      22              :   PreparedRequest,
+      23              :   ProviderID,
+      24              :   mergeGenerationOptions,
+      25              :   mergeHttpOptions,
+      26              :   mergeProviderOptions,
+      27              : } from "../schema"
+      28              : 
+      29              : export interface RouteBody<Body> {
+      30              :   /** Schema for the validated provider-native body sent as the JSON request. */
+      31              :   readonly schema: Schema.Codec<Body, unknown>
+      32              :   /** Build the provider-native body from a common `LLMRequest`. */
+      33              :   readonly from: (request: LLMRequest) => Effect.Effect<Body, LLMError>
+      34              : }
+      35              : 
+      36              : export interface Route<Body, Prepared = unknown> {
+      37              :   readonly id: string
+      38              :   readonly provider?: ProviderID
+      39              :   readonly protocol: ProtocolID
+      40              :   readonly endpoint: Endpoint<Body>
+      41              :   readonly auth: AuthDef
+      42              :   readonly transport: Transport<Body, Prepared, unknown>
+      43              :   readonly defaults: RouteDefaults
+      44              :   readonly body: RouteBody<Body>
+      45              :   readonly with: (patch: RoutePatch<Body, Prepared>) => Route<Body, Prepared>
+      46              :   readonly model: (input: RouteMappedModelInput) => Model
+      47              :   readonly prepareTransport: (body: Body, request: LLMRequest) => Effect.Effect<Prepared, LLMError>
+      48              :   readonly streamPrepared: (
+      49              :     prepared: Prepared,
+      50              :     request: LLMRequest,
+      51              :     runtime: TransportRuntime,
+      52              :   ) => Stream.Stream<LLMEvent, LLMError>
+      53              : }
+      54              : 
+      55              : // Route registries intentionally erase body generics after construction.
+      56              : // Normal call sites use `OpenAIChat.route`; callers only need body types
+      57              : // when preparing a request with a protocol-specific type assertion.
+      58              : // oxlint-disable-next-line typescript-eslint/no-explicit-any
+      59              : export type AnyRoute = Route<any, any>
+      60              : 
+      61              : export type HttpOptionsInput = HttpOptions.Input
+      62              : 
+      63              : export type RouteModelInput = Omit<Model.Input, "provider" | "route">
+      64              : 
+      65              : export type RouteRoutedModelInput = Omit<Model.Input, "route">
+      66              : 
+      67              : export interface RouteDefaults {
+      68              :   readonly headers?: Record<string, string>
+      69              :   readonly limits?: ModelLimits
+      70              :   readonly generation?: GenerationOptions
+      71              :   readonly providerOptions?: ProviderOptions
+      72              :   readonly http?: HttpOptions
+      73              : }
+      74              : 
+      75              : export interface RouteDefaultsInput {
+      76              :   readonly headers?: Record<string, string>
+      77              :   readonly limits?: ModelLimits.Input
+      78              :   readonly generation?: GenerationOptions.Input
+      79              :   readonly providerOptions?: ProviderOptions
+      80              :   readonly http?: HttpOptions.Input
+      81              : }
+      82              : 
+      83              : export interface RoutePatch<Body, Prepared> extends RouteDefaultsInput {
+      84              :   readonly id?: string
+      85              :   readonly provider?: string | ProviderID
+      86              :   readonly auth?: AuthDef
+      87              :   readonly transport?: Transport<Body, Prepared, unknown>
+      88              :   readonly endpoint?: EndpointPatch<Body>
+      89              : }
+      90              : 
+      91              : type RouteMappedModelInput = RouteModelInput | RouteRoutedModelInput
+      92              : 
+      93           43 : const makeRouteModel = (route: AnyRoute, mapped: RouteMappedModelInput) => {
+      94           58 :   const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined)
+      95            2 :   if (!provider) throw new Error(`Route.model(${route.id}) requires a provider`)
+      96           40 :   if (!endpointBaseURL(route.endpoint))
+      97            2 :     throw new Error(`Route.model(${route.id}) requires an endpoint baseURL — configure it on the route first`)
+      98           26 :   return Model.make({
+      99           11 :     ...mapped,
+     100           13 :     provider,
+     101            7 :     route,
+     102            4 :   })
+     103              : }
+     104              : 
+     105           45 : const mergeRouteDefaults = (base: RouteDefaults | undefined, patch: RouteDefaultsInput): RouteDefaults => {
+     106           61 :   const headers = mergeHeaders(base?.headers, patch.headers)
+     107           15 :   return {
+     108           12 :     ...base,
+     109           10 :     ...patch,
+     110           12 :     headers,
+     111           84 :     limits: patch.limits === undefined ? base?.limits : ModelLimits.make(patch.limits),
+     112          113 :     generation: mergeGenerationOptions(generationOptions(base?.generation), generationOptions(patch.generation)),
+     113           88 :     providerOptions: mergeProviderOptions(base?.providerOptions, patch.providerOptions),
+     114           23 :     http: mergeHttpOptions(
+     115           12 :       base?.http,
+     116           25 :       httpOptions(patch.http),
+     117           62 :       headers === undefined ? undefined : new HttpOptions({ headers }),
+     118            2 :     ),
+     119            3 :   }
+     120              : }
+     121              : 
+     122           36 : const endpointBaseURL = <Body>(endpoint: Endpoint<Body>) =>
+     123           56 :   typeof endpoint.baseURL === "string" ? endpoint.baseURL : undefined
+     124              : 
+     125           36 : const mergeHeaders = (...items: ReadonlyArray<Record<string, string> | undefined>) => {
+     126           39 :   const entries = items.flatMap((item) =>
+     127           84 :     item === undefined ? [] : Object.entries(item).filter((entry): entry is [string, string] => entry[1] !== undefined),
+     128            4 :   )
+     129           36 :   if (entries.length === 0) return undefined
+     130           36 :   return Object.fromEntries(entries)
+     131              : }
+     132              : 
+     133           42 : export const generationOptions = (input: GenerationOptions.Input | undefined) =>
+     134           63 :   input === undefined ? undefined : GenerationOptions.make(input)
+     135              : 
+     136           39 : export const httpOptions = (input: HttpOptionsInput | undefined) => {
+     137           41 :   if (input === undefined) return input
+     138           32 :   return HttpOptions.make(input)
+     139              : }
+     140              : 
+     141              : export interface Interface {
+     142              :   /**
+     143              :    * Compile a request through protocol body construction, validation, and HTTP
+     144              :    * preparation without sending it. Returns the prepared request including the
+     145              :    * provider-native body.
+     146              :    *
+     147              :    * Pass a `Body` type argument to statically expose the route's body
+     148              :    * shape (e.g. `prepare<OpenAIChatBody>(...)`) — the runtime body is
+     149              :    * identical, so this is a type-level assertion the caller makes about which
+     150              :    * route the request will resolve to.
+     151              :    */
+     152              :   readonly prepare: <Body = unknown>(request: LLMRequest) => Effect.Effect<PreparedRequestOf<Body>, LLMError>
+     153              :   readonly stream: StreamMethod
+     154              :   readonly generate: GenerateMethod
+     155              : }
+     156              : 
+     157              : export interface StreamMethod {
+     158              :   (request: LLMRequest): Stream.Stream<LLMEvent, LLMError>
+     159              : }
+     160              : 
+     161              : export interface GenerateMethod {
+     162              :   (request: LLMRequest): Effect.Effect<LLMResponse, LLMError>
+     163              : }
+     164              : 
+     165           72 : export class Service extends Context.Service<Service, Interface>()("@opencode/LLMClient") {}
+     166              : 
+     167           44 : const resolveRequestOptions = (request: LLMRequest) => {
+     168           53 :   const routeDefaults = request.model.route.defaults
+     169           47 :   const modelDefaults = request.model.defaults
+     170          117 :   const generation = mergeGenerationOptions(routeDefaults.generation, modelDefaults?.generation, request.generation)
+     171           39 :   return LLMRequest.update(request, {
+     172           56 :     generation: generation ?? new GenerationOptions({}),
+     173           38 :     providerOptions: mergeProviderOptions(
+     174           31 :       routeDefaults.providerOptions,
+     175           32 :       modelDefaults?.providerOptions,
+     176           23 :       request.providerOptions,
+     177            6 :     ),
+     178           79 :     http: mergeHttpOptions(routeDefaults.http, modelDefaults?.http, request.http),
+     179            4 :   })
+     180              : }
+     181              : 
+     182              : export interface MakeInput<Body, Frame, Event, State> {
+     183              :   /** Route id used in diagnostics and prepared request metadata. */
+     184              :   readonly id: string
+     185              :   /** Provider identity for route-owned model construction. */
+     186              :   readonly provider?: string | ProviderID
+     187              :   /** Semantic API contract — owns body construction, body schema, and parsing. */
+     188              :   readonly protocol: Protocol<Body, Frame, Event, State>
+     189              :   /** Where the request is sent. */
+     190              :   readonly endpoint: Endpoint<Body>
+     191              :   /** Per-request transport auth. Provider facades override this via `route.with(...)`. */
+     192              :   readonly auth?: AuthDef
+     193              :   /** Stream framing — bytes -> frames before `protocol.stream.event` decoding. */
+     194              :   readonly framing: Framing<Frame>
+     195              :   /** Static / per-request headers added before `auth` runs. */
+     196              :   readonly headers?: (input: { readonly request: LLMRequest }) => Record<string, string>
+     197              :   /** Route/request defaults used when compiling requests for this route. */
+     198              :   readonly defaults?: RouteDefaultsInput
+     199              : }
+     200              : 
+     201              : export interface MakeTransportInput<Body, Prepared, Frame, Event, State> {
+     202              :   /** Route id used in diagnostics and prepared request metadata. */
+     203              :   readonly id: string
+     204              :   /** Provider identity for route-owned model construction. */
+     205              :   readonly provider?: string | ProviderID
+     206              :   /** Semantic API contract — owns body construction, body schema, and parsing. */
+     207              :   readonly protocol: Protocol<Body, Frame, Event, State>
+     208              :   /** Where the request is sent. */
+     209              :   readonly endpoint: Endpoint<Body>
+     210              :   /** Per-request transport auth. Provider facades override this via `route.with(...)`. */
+     211              :   readonly auth?: AuthDef
+     212              :   /** Static / per-request headers added before `auth` runs. */
+     213              :   readonly headers?: (input: { readonly request: LLMRequest }) => Record<string, string>
+     214              :   /** Runnable transport route. */
+     215              :   readonly transport: Transport<Body, Prepared, Frame>
+     216              :   /** Route/request defaults used when compiling requests for this route. */
+     217              :   readonly defaults?: RouteDefaultsInput
+     218              : }
+     219              : 
+     220            0 : const streamError = (route: string, message: string, cause: Cause.Cause<unknown>) => {
+     221            0 :   const failed = cause.reasons.find(Cause.isFailReason)?.error
+     222            0 :   if (failed instanceof LLMErrorClass) return failed
+     223            1 :   return ProviderShared.eventError(route, message, Cause.pretty(cause))
+     224              : }
+     225              : 
+     226            1 : function makeFromTransport<Body, Prepared, Frame, Event, State>(
+     227            7 :   input: MakeTransportInput<Body, Prepared, Frame, Event, State>,
+     228            3 : ): Route<Body, Prepared> {
+     229           34 :   const protocol = input.protocol
+     230           84 :   const encodeBody = Schema.encodeSync(Schema.fromJsonString(protocol.body.schema))
+     231           78 :   const decodeEventEffect = Schema.decodeUnknownEffect(protocol.stream.event)
+     232           40 :   const decodeEvent = (route: string) => (frame: Frame) =>
+     233           30 :     decodeEventEffect(frame).pipe(
+     234            0 :       Effect.mapError(() =>
+     235            0 :         ProviderShared.eventError(
+     236            0 :           input.id,
+     237            0 :           `Invalid ${route} stream event`,
+     238              :           typeof frame === "string" ? frame : ProviderShared.encodeJson(frame),
+     239              :         ),
+     240            1 :       ),
+     241            4 :     )
+     242              : 
+     243              :   type BuiltRouteInput = Omit<MakeTransportInput<Body, Prepared, Frame, Event, State>, "defaults"> & {
+     244              :     readonly defaults?: RouteDefaults
+     245              :   }
+     246              : 
+     247           34 :   const build = (routeInput: BuiltRouteInput): Route<Body, Prepared> => {
+     248           21 :     const route: Route<Body, Prepared> = {
+     249           24 :       id: routeInput.id,
+     250           98 :       provider: routeInput.provider === undefined ? undefined : ProviderID.make(routeInput.provider),
+     251           28 :       protocol: protocol.id,
+     252           36 :       endpoint: routeInput.endpoint,
+     253           41 :       auth: routeInput.auth ?? Auth.none,
+     254           38 :       transport: routeInput.transport,
+     255           42 :       defaults: routeInput.defaults ?? {},
+     256           26 :       body: protocol.body,
+     257           25 :       with: (patch: RoutePatch<Body, Prepared>) => {
+     258           79 :         const { id, provider, auth, transport, endpoint, ...defaults } = patch
+     259           27 :         return build({
+     260           21 :           ...routeInput,
+     261           34 :           id: id ?? routeInput.id,
+     262           52 :           provider: provider ?? routeInput.provider,
+     263           40 :           auth: auth ?? routeInput.auth,
+     264           96 :           endpoint: endpoint ? Endpoint.merge(routeInput.endpoint, endpoint) : routeInput.endpoint,
+     265           55 :           transport: (transport as Transport<Body, Prepared, Frame> | undefined) ?? routeInput.transport,
+     266           62 :           defaults: mergeRouteDefaults(route.defaults, defaults),
+     267            9 :         })
+     268              :       },
+     269           51 :       model: (input) => makeRouteModel(route, input),
+     270           36 :       prepareTransport: (body, request) =>
+     271           38 :         routeInput.transport.prepare({
+     272           13 :           body,
+     273           16 :           request,
+     274           38 :           endpoint: routeInput.endpoint,
+     275           43 :           auth: routeInput.auth ?? Auth.none,
+     276           19 :           encodeBody,
+     277           33 :           headers: routeInput.headers,
+     278            8 :         }),
+     279           56 :       streamPrepared: (prepared: Prepared, request: LLMRequest, runtime: TransportRuntime) => {
+     280           76 :         const route = `${request.model.provider}/${request.model.route.id}`
+     281           36 :         const events = routeInput.transport
+     282           35 :           .frames(prepared, request, runtime)
+     283            5 :           .pipe(
+     284           38 :             Stream.mapEffect(decodeEvent(route)),
+     285           44 :             protocol.stream.terminal ? Stream.takeUntil(protocol.stream.terminal) : (stream) => stream,
+     286            9 :           )
+     287           19 :         return events.pipe(
+     288           21 :           Stream.mapAccumEffect(
+     289           39 :             () => protocol.stream.initial(request),
+     290           22 :             protocol.stream.step,
+     291           58 :             protocol.stream.onHalt ? { onHalt: protocol.stream.onHalt } : undefined,
+     292            2 :           ),
+     293           18 :           Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))),
+     294            5 :         )
+     295              :       },
+     296            6 :     } satisfies Route<Body, Prepared>
+     297           15 :     return route
+     298              :   }
+     299              : 
+     300           90 :   return build({ ...input, defaults: mergeRouteDefaults(undefined, input.defaults ?? {}) })
+     301              : }
+     302              : 
+     303              : export function make<Body, Prepared, Frame, Event, State>(
+     304              :   input: MakeTransportInput<Body, Prepared, Frame, Event, State>,
+     305              : ): Route<Body, Prepared>
+     306              : /**
+     307              :  * Build a `Route` by composing the four orthogonal pieces of a deployment:
+     308              :  *
+     309              :  * - `Protocol` — what is the API I'm speaking?
+     310              :  * - `Endpoint` — where do I send the request?
+     311              :  * - `Auth` — how do I authenticate it?
+     312              :  * - `Framing` — how do I cut the response stream into protocol frames?
+     313              :  *
+     314              :  * Plus optional `headers` for cross-cutting deployment concerns (provider
+     315              :  * version pins, per-deployment quirks).
+     316              :  *
+     317              :  * This is the canonical route constructor. If a new route does not fit
+     318              :  * this four-axis model, add a purpose-built constructor rather than widening
+     319              :  * the public surface preemptively.
+     320              :  */
+     321              : export function make<Body, Frame, Event, State>(
+     322              :   input: MakeInput<Body, Frame, Event, State>,
+     323              : ): Route<Body, HttpTransport.HttpPrepared<Frame>>
+     324            6 : export function make<Body, Prepared, Frame, Event, State>(
+     325            7 :   input: MakeInput<Body, Frame, Event, State> | MakeTransportInput<Body, Prepared, Frame, Event, State>,
+     326            3 : ): Route<Body, Prepared> | Route<Body, HttpTransport.HttpPrepared<Frame>> {
+     327           61 :   if ("transport" in input) return makeFromTransport(input)
+     328           34 :   const protocol = input.protocol
+     329           30 :   return makeFromTransport({
+     330           17 :     id: input.id,
+     331           29 :     provider: input.provider,
+     332           13 :     protocol,
+     333           29 :     endpoint: input.endpoint,
+     334           21 :     auth: input.auth,
+     335           27 :     headers: input.headers,
+     336           66 :     transport: HttpTransport.httpJson({ framing: input.framing }),
+     337           26 :     defaults: input.defaults,
+     338            3 :   })
+     339              : }
+     340              : 
+     341              : // `compile` is the important boundary: it turns a common `LLMRequest` into a
+     342              : // validated provider body plus transport-private prepared data, but does not
+     343              : // execute transport.
+     344           51 : const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) {
+     345           68 :   const resolved = applyCachePolicy(resolveRequestOptions(request))
+     346           37 :   const route = resolved.model.route
+     347              : 
+     348           31 :   const body = yield* route.body
+     349           15 :     .from(resolved)
+     350           99 :     .pipe(Effect.flatMap(ProviderShared.validateWith(Schema.decodeUnknownEffect(route.body.schema))))
+     351           65 :   const prepared = yield* route.prepareTransport(body, resolved)
+     352              : 
+     353           12 :   return {
+     354           22 :     request: resolved,
+     355           10 :     route,
+     356            9 :     body,
+     357           10 :     prepared,
+     358            1 :   }
+     359            3 : })
+     360              : 
+     361           61 : const prepareWith = Effect.fn("LLMClient.prepare")(function* (request: LLMRequest) {
+     362           43 :   const compiled = yield* compile(request)
+     363              : 
+     364           32 :   return new PreparedRequest({
+     365           41 :     id: compiled.request.id ?? "request",
+     366           29 :     route: compiled.route.id,
+     367           38 :     protocol: compiled.route.protocol,
+     368           34 :     model: compiled.request.model,
+     369           24 :     body: compiled.body,
+     370           54 :     metadata: { transport: compiled.route.transport.id },
+     371            2 :   })
+     372            3 : })
+     373              : 
+     374           49 : const streamRequestWith = (runtime: TransportRuntime) => (request: LLMRequest) =>
+     375           14 :   Stream.unwrap(
+     376           15 :     Effect.gen(function* () {
+     377           43 :       const compiled = yield* compile(request)
+     378           82 :       return compiled.route.streamPrepared(compiled.prepared, compiled.request, runtime)
+     379            1 :     }),
+     380            3 :   )
+     381              : 
+     382           31 : const generateWith = (stream: Interface["stream"]) =>
+     383            0 :   Effect.fn("LLM.generate")(function* (request: LLMRequest) {
+     384            0 :     const state = yield* stream(request).pipe(Stream.runFold(LLMResponse.empty, LLMResponse.reduce))
+     385            0 :     const response = LLMResponse.complete(state)
+     386            0 :     if (response) return response
+     387            0 :     return yield* ProviderShared.eventError(
+     388            0 :       `${request.model.provider}/${request.model.route.id}`,
+     389            0 :       "Provider stream ended without a terminal finish event",
+     390              :     )
+     391            2 :   })
+     392              : 
+     393           34 : export const prepare = <Body = unknown>(request: LLMRequest) =>
+     394           21 :   prepareWith(request) as Effect.Effect<PreparedRequestOf<Body>, LLMError>
+     395              : 
+     396            0 : export function stream(request: LLMRequest): Stream.Stream<LLMEvent, LLMError> {
+     397            0 :   return Stream.unwrap(
+     398            0 :     Effect.gen(function* () {
+     399            0 :       return (yield* Service).stream(request)
+     400            0 :     }),
+     401            1 :   ) as Stream.Stream<LLMEvent, LLMError>
+     402              : }
+     403              : 
+     404            0 : export function generate(request: LLMRequest): Effect.Effect<LLMResponse, LLMError> {
+     405            0 :   return Effect.gen(function* () {
+     406            0 :     return yield* (yield* Service).generate(request)
+     407            1 :   }) as Effect.Effect<LLMResponse, LLMError>
+     408              : }
+     409              : 
+     410            0 : export const streamRequest = (request: LLMRequest) =>
+     411            0 :   Stream.unwrap(
+     412            0 :     Effect.gen(function* () {
+     413            0 :       return (yield* Service).stream(request)
+     414              :     }),
+     415            2 :   )
+     416              : 
+     417           33 : export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer.effect(
+     418            9 :   Service,
+     419           15 :   Effect.gen(function* () {
+     420           38 :     const stream = streamRequestWith({
+     421           41 :       http: yield* RequestExecutor.Service,
+     422           90 :       webSocket: Option.getOrUndefined(yield* Effect.serviceOption(WebSocketExecutor.Service)),
+     423            5 :     })
+     424           83 :     return Service.of({ prepare: prepareWith as Interface["prepare"], stream, generate: generateWith(stream) })
+     425            1 :   }),
+     426            3 : )
+     427              : 
+     428           30 : export const Route = { make } as const
+     429              : 
+     430           27 : export const LLMClient = {
+     431           10 :   Service,
+     432            8 :   layer,
+     433           10 :   prepare,
+     434            9 :   stream,
+     435            9 :   generate,
+     436            1 : } as const
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/endpoint.ts.gcov.html b/packages/core/llm/src/route/endpoint.ts.gcov.html new file mode 100644 index 00000000..ddc29d7b --- /dev/null +++ b/packages/core/llm/src/route/endpoint.ts.gcov.html @@ -0,0 +1,129 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route/endpoint.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/route - endpoint.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %1919
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import type { LLMRequest } from "../schema"
+       2           54 : import * as ProviderShared from "../protocols/shared"
+       3              : 
+       4              : export interface EndpointInput<Body> {
+       5              :   readonly request: LLMRequest
+       6              :   readonly body: Body
+       7              : }
+       8              : 
+       9              : export type EndpointPart<Body> = string | ((input: EndpointInput<Body>) => string)
+      10              : 
+      11              : /**
+      12              :  * Declarative URL construction for one route.
+      13              :  *
+      14              :  * `Endpoint` carries URL construction for one route. Routes with a canonical
+      15              :  * host put `baseURL` here; provider helpers can override it by configuring the
+      16              :  * route before selecting a model.
+      17              :  *
+      18              :  * `path` may be a string or a function of `EndpointInput`, for routes whose
+      19              :  * URL embeds the model id, region, or another body field (e.g. Bedrock,
+      20              :  * Gemini).
+      21              :  */
+      22              : export interface Endpoint<Body> {
+      23              :   readonly baseURL?: string
+      24              :   readonly path: EndpointPart<Body>
+      25              :   readonly query?: Record<string, string>
+      26              : }
+      27              : 
+      28              : export type EndpointPatch<Body> = Partial<Endpoint<Body>>
+      29              : 
+      30              : /** Construct an `Endpoint` from a path string or path function. */
+      31           50 : export const path = <Body>(value: EndpointPart<Body>, options: Omit<Endpoint<Body>, "path"> = {}): Endpoint<Body> => ({
+      32           10 :   ...options,
+      33           12 :   path: value,
+      34            2 : })
+      35              : 
+      36           43 : export const merge = <Body>(base: Endpoint<Body>, patch: EndpointPatch<Body>): Endpoint<Body> => ({
+      37           10 :   ...base,
+      38            8 :   ...patch,
+      39           41 :   baseURL: patch.baseURL ?? base.baseURL,
+      40           32 :   path: patch.path ?? base.path,
+      41           44 :   query: patch.query === undefined ? base.query : { ...base.query, ...patch.query },
+      42            2 : })
+      43              : 
+      44           34 : const renderPart = <Body>(part: EndpointPart<Body>, input: EndpointInput<Body>) =>
+      45           36 :   typeof part === "function" ? part(input) : part
+      46              : 
+      47           44 : export const render = <Body>(endpoint: Endpoint<Body>, input: EndpointInput<Body>) => {
+      48          114 :   const url = new URL(`${ProviderShared.trimBaseUrl(endpoint.baseURL ?? "")}${renderPart(endpoint.path, input)}`)
+      49           69 :   for (const [key, value] of Object.entries(endpoint.query ?? {})) url.searchParams.set(key, value)
+      50           12 :   return url
+      51              : }
+      52              : 
+      53           38 : export * as Endpoint from "./endpoint"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/executor.ts.gcov.html b/packages/core/llm/src/route/executor.ts.gcov.html new file mode 100644 index 00000000..e49ad64d --- /dev/null +++ b/packages/core/llm/src/route/executor.ts.gcov.html @@ -0,0 +1,461 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route/executor.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/route - executor.tsCoverageTotalHit
Test:opencode-lcov.infoLines:21.9 %30166
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           25 : import { Cause, Context, Effect, Layer, Random } from "effect"
+       2           96 : import {
+       3              :   FetchHttpClient,
+       4              :   Headers,
+       5              :   HttpClient,
+       6              :   HttpClientError,
+       7              :   HttpClientRequest,
+       8              :   HttpClientResponse,
+       9              : } from "effect/unstable/http"
+      10          292 : import {
+      11              :   AuthenticationReason,
+      12              :   ContentPolicyReason,
+      13              :   HttpContext,
+      14              :   HttpRateLimitDetails,
+      15              :   HttpRequestDetails,
+      16              :   HttpResponseDetails,
+      17              :   InvalidRequestReason,
+      18              :   LLMError,
+      19              :   ProviderInternalReason,
+      20              :   QuotaExceededReason,
+      21              :   RateLimitReason,
+      22              :   TransportReason,
+      23              :   UnknownProviderReason,
+      24              : } from "../schema"
+      25           54 : import { isContextOverflow } from "../provider-error"
+      26              : 
+      27              : export interface Interface {
+      28              :   readonly execute: (
+      29              :     request: HttpClientRequest.HttpClientRequest,
+      30              :   ) => Effect.Effect<HttpClientResponse.HttpClientResponse, LLMError>
+      31              : }
+      32              : 
+      33           82 : export class Service extends Context.Service<Service, Interface>()("@opencode/LLM/RequestExecutor") {}
+      34              : 
+      35           25 : const BODY_LIMIT = 16_384
+      36           22 : const MAX_RETRIES = 2
+      37           26 : const BASE_DELAY_MS = 500
+      38           25 : const MAX_DELAY_MS = 10_000
+      39           30 : const REDACTED = "<redacted>"
+      40              : 
+      41              : // One source of truth for what counts as a sensitive name across headers,
+      42              : // URL query keys, and field names embedded inside request/response bodies.
+      43              : //
+      44              : // `SENSITIVE_NAME` is used as both a substring matcher (for free-form header
+      45              : // names like `Authorization` / `X-API-Key`) and as the body-field alternation
+      46              : // list. `SHORT_QUERY_NAME` covers anchored short keys like `?key=…` / `?sig=…`
+      47              : // that are too generic to redact substring-style without false positives.
+      48           29 : const SENSITIVE_NAME_SOURCE =
+      49          127 :   "authorization|api[-_]?key|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|credential|signature|x-amz-signature"
+      50           62 : const SENSITIVE_NAME = new RegExp(SENSITIVE_NAME_SOURCE, "i")
+      51           40 : const SHORT_QUERY_NAME = /^(key|sig)$/i
+      52           81 : const SENSITIVE_BODY_FIELD = new RegExp(`(?:${SENSITIVE_NAME_SOURCE}|key)`, "i")
+      53          101 : const REDACT_JSON_FIELD = new RegExp(`("(?:${SENSITIVE_BODY_FIELD.source})"\\s*:\\s*)"[^"]*"`, "gi")
+      54           94 : const REDACT_QUERY_FIELD = new RegExp(`((?:${SENSITIVE_BODY_FIELD.source})=)[^&\\s"]+`, "gi")
+      55              : 
+      56           30 : const isSensitiveHeaderName = (name: string) => SENSITIVE_NAME.test(name)
+      57              : 
+      58           29 : const isSensitiveQueryName = (name: string) => isSensitiveHeaderName(name) || SHORT_QUERY_NAME.test(name)
+      59              : 
+      60            0 : const redactHeaders = (headers: Headers.Headers, redactedNames: ReadonlyArray<string | RegExp>) =>
+      61            0 :   Object.fromEntries(
+      62            0 :     Object.entries(Headers.redact(headers, [...redactedNames, SENSITIVE_NAME])).map(([name, value]) => [
+      63            0 :       name,
+      64            0 :       String(value),
+      65              :     ]),
+      66            2 :   )
+      67              : 
+      68            0 : const redactUrl = (value: string) => {
+      69            0 :   if (!URL.canParse(value)) return REDACTED
+      70            0 :   const url = new URL(value)
+      71            0 :   url.searchParams.forEach((_, key) => {
+      72            0 :     if (isSensitiveQueryName(key)) url.searchParams.set(key, REDACTED)
+      73            0 :   })
+      74            2 :   return url.toString()
+      75              : }
+      76              : 
+      77            0 : const normalizedHeaders = (headers: Headers.Headers) =>
+      78            2 :   Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]))
+      79              : 
+      80            0 : const requestId = (headers: Record<string, string>) => {
+      81            0 :   return (
+      82            0 :     headers["x-request-id"] ??
+      83            0 :     headers["request-id"] ??
+      84            0 :     headers["x-amzn-requestid"] ??
+      85            0 :     headers["x-amz-request-id"] ??
+      86            0 :     headers["x-goog-request-id"] ??
+      87            2 :     headers["cf-ray"]
+      88              :   )
+      89              : }
+      90              : 
+      91           24 : const retryableStatus = (status: number) => status === 429 || status === 503 || status === 504 || status === 529
+      92              : 
+      93            0 : const retryAfterMs = (headers: Record<string, string>) => {
+      94            0 :   const millis = Number(headers["retry-after-ms"])
+      95            0 :   if (Number.isFinite(millis)) return Math.max(0, millis)
+      96            0 : 
+      97            0 :   const value = headers["retry-after"]
+      98            0 :   if (!value) return undefined
+      99            0 : 
+     100            0 :   const seconds = Number(value)
+     101            0 :   if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000)
+     102            0 : 
+     103            0 :   const date = Date.parse(value)
+     104            0 :   if (!Number.isNaN(date)) return Math.max(0, date - Date.now())
+     105            2 :   return undefined
+     106              : }
+     107              : 
+     108            0 : const addRateLimitValue = (target: Record<string, string>, key: string, value: string) => {
+     109            2 :   if (key.length > 0) target[key] = value
+     110              : }
+     111              : 
+     112            0 : const rateLimitDetails = (headers: Record<string, string>, retryAfter: number | undefined) => {
+     113            0 :   const limit: Record<string, string> = {}
+     114            0 :   const remaining: Record<string, string> = {}
+     115            0 :   const reset: Record<string, string> = {}
+     116            0 : 
+     117            0 :   Object.entries(headers).forEach(([name, value]) => {
+     118            0 :     const openaiLimit = /^x-ratelimit-limit-(.+)$/.exec(name)?.[1]
+     119            0 :     if (openaiLimit) return addRateLimitValue(limit, openaiLimit, value)
+     120            0 : 
+     121            0 :     const openaiRemaining = /^x-ratelimit-remaining-(.+)$/.exec(name)?.[1]
+     122            0 :     if (openaiRemaining) return addRateLimitValue(remaining, openaiRemaining, value)
+     123            0 : 
+     124            0 :     const openaiReset = /^x-ratelimit-reset-(.+)$/.exec(name)?.[1]
+     125            0 :     if (openaiReset) return addRateLimitValue(reset, openaiReset, value)
+     126            0 : 
+     127            0 :     const anthropic = /^anthropic-ratelimit-(.+)-(limit|remaining|reset)$/.exec(name)
+     128            0 :     if (!anthropic) return
+     129            0 :     if (anthropic[2] === "limit") return addRateLimitValue(limit, anthropic[1], value)
+     130            0 :     if (anthropic[2] === "remaining") return addRateLimitValue(remaining, anthropic[1], value)
+     131            0 :     return addRateLimitValue(reset, anthropic[1], value)
+     132            0 :   })
+     133            0 : 
+     134            0 :   if (
+     135            0 :     retryAfter === undefined &&
+     136            0 :     Object.keys(limit).length === 0 &&
+     137            0 :     Object.keys(remaining).length === 0 &&
+     138            0 :     Object.keys(reset).length === 0
+     139            0 :   )
+     140            0 :     return undefined
+     141            0 : 
+     142            0 :   return new HttpRateLimitDetails({
+     143            0 :     retryAfterMs: retryAfter,
+     144            0 :     limit: Object.keys(limit).length === 0 ? undefined : limit,
+     145            0 :     remaining: Object.keys(remaining).length === 0 ? undefined : remaining,
+     146            0 :     reset: Object.keys(reset).length === 0 ? undefined : reset,
+     147            2 :   })
+     148              : }
+     149              : 
+     150            0 : const requestDetails = (request: HttpClientRequest.HttpClientRequest, redactedNames: ReadonlyArray<string | RegExp>) =>
+     151            0 :   new HttpRequestDetails({
+     152            0 :     method: request.method,
+     153            0 :     url: redactUrl(request.url),
+     154              :     headers: redactHeaders(request.headers, redactedNames),
+     155            2 :   })
+     156              : 
+     157            0 : const responseDetails = (
+     158            0 :   response: HttpClientResponse.HttpClientResponse,
+     159            0 :   redactedNames: ReadonlyArray<string | RegExp>,
+     160            0 : ) =>
+     161            0 :   new HttpResponseDetails({
+     162            0 :     status: response.status,
+     163              :     headers: redactHeaders(response.headers, redactedNames),
+     164            2 :   })
+     165              : 
+     166            0 : const secretValues = (request: HttpClientRequest.HttpClientRequest) => {
+     167            0 :   const values = new Set<string>()
+     168            0 :   const add = (value: string) => {
+     169            0 :     if (value.length < 4) return
+     170            0 :     values.add(value)
+     171            0 :     values.add(encodeURIComponent(value))
+     172            0 :   }
+     173            0 : 
+     174            0 :   Object.entries(request.headers).forEach(([name, value]) => {
+     175            0 :     if (!isSensitiveHeaderName(name)) return
+     176            0 :     add(value)
+     177            0 :     const bearer = /^Bearer\s+(.+)$/i.exec(value)?.[1]
+     178            0 :     if (bearer) add(bearer)
+     179            0 :   })
+     180            0 : 
+     181            0 :   if (!URL.canParse(request.url)) return values
+     182            0 :   new URL(request.url).searchParams.forEach((value, key) => {
+     183            0 :     if (isSensitiveQueryName(key)) add(value)
+     184            0 :   })
+     185            2 :   return values
+     186              : }
+     187              : 
+     188              : // Two passes: structural (redact `"name": "value"` and `name=value` patterns
+     189              : // for any field name that looks sensitive) plus literal (replace any actual
+     190              : // secret values we sent in the request, in case the response echoes one back).
+     191            0 : const redactBody = (body: string, request: HttpClientRequest.HttpClientRequest) =>
+     192            0 :   Array.from(secretValues(request)).reduce(
+     193            0 :     (text, secret) => text.split(secret).join(REDACTED),
+     194              :     body.replace(REDACT_JSON_FIELD, `$1"${REDACTED}"`).replace(REDACT_QUERY_FIELD, `$1${REDACTED}`),
+     195            2 :   )
+     196              : 
+     197            0 : const responseBody = (body: string | void, request: HttpClientRequest.HttpClientRequest) => {
+     198            0 :   if (body === undefined) return {}
+     199            0 :   const redacted = redactBody(body, request)
+     200            0 :   if (redacted.length <= BODY_LIMIT) return { body: redacted }
+     201            2 :   return { body: redacted.slice(0, BODY_LIMIT), bodyTruncated: true }
+     202              : }
+     203              : 
+     204            0 : const providerMessage = (status: number, body: { readonly body?: string }) => {
+     205            0 :   if (body.body && body.body.length <= 500) return `Provider request failed with HTTP ${status}: ${body.body}`
+     206            2 :   return `Provider request failed with HTTP ${status}`
+     207              : }
+     208              : 
+     209            0 : const responseHttp = (input: {
+     210            0 :   readonly request: HttpClientRequest.HttpClientRequest
+     211            0 :   readonly response: HttpClientResponse.HttpClientResponse
+     212            0 :   readonly redactedNames: ReadonlyArray<string | RegExp>
+     213            0 :   readonly body: ReturnType<typeof responseBody>
+     214            0 :   readonly requestId?: string | undefined
+     215            0 :   readonly rateLimit?: HttpRateLimitDetails | undefined
+     216            0 : }) =>
+     217            0 :   new HttpContext({
+     218            0 :     request: requestDetails(input.request, input.redactedNames),
+     219            0 :     response: responseDetails(input.response, input.redactedNames),
+     220            0 :     ...input.body,
+     221            0 :     requestId: input.requestId,
+     222              :     rateLimit: input.rateLimit,
+     223            2 :   })
+     224              : 
+     225            0 : const statusReason = (input: {
+     226            0 :   readonly status: number
+     227            0 :   readonly message: string
+     228            0 :   readonly retryAfterMs?: number | undefined
+     229            0 :   readonly rateLimit?: HttpRateLimitDetails | undefined
+     230            0 :   readonly http: HttpContext
+     231            0 : }) => {
+     232            0 :   const body = input.http.body ?? ""
+     233            0 :   if (/content[-_\s]?policy|content_filter|safety/i.test(body)) {
+     234            0 :     return new ContentPolicyReason({ message: input.message, http: input.http })
+     235            0 :   }
+     236            0 :   if (input.status === 401) {
+     237            0 :     return new AuthenticationReason({ message: input.message, kind: "invalid", http: input.http })
+     238            0 :   }
+     239            0 :   if (input.status === 403) {
+     240            0 :     return new AuthenticationReason({ message: input.message, kind: "insufficient-permissions", http: input.http })
+     241            0 :   }
+     242            0 :   if (input.status === 429) {
+     243            0 :     if (/insufficient[-_\s]?quota|quota[-_\s]?exceeded/i.test(body)) {
+     244            0 :       return new QuotaExceededReason({ message: input.message, http: input.http })
+     245            0 :     }
+     246            0 :     return new RateLimitReason({
+     247            0 :       message: input.message,
+     248            0 :       retryAfterMs: input.retryAfterMs,
+     249            0 :       rateLimit: input.rateLimit,
+     250            0 :       http: input.http,
+     251            0 :     })
+     252            0 :   }
+     253            0 :   if (
+     254            0 :     input.status === 400 ||
+     255            0 :     input.status === 404 ||
+     256            0 :     input.status === 409 ||
+     257            0 :     input.status === 413 ||
+     258            0 :     input.status === 422
+     259            0 :   ) {
+     260            0 :     return new InvalidRequestReason({
+     261            0 :       message: input.message,
+     262            0 :       classification: isContextOverflow(body) ? "context-overflow" : undefined,
+     263            0 :       http: input.http,
+     264            0 :     })
+     265            0 :   }
+     266            0 :   if (input.status >= 500 || retryableStatus(input.status)) {
+     267            0 :     return new ProviderInternalReason({
+     268            0 :       message: input.message,
+     269            0 :       status: input.status,
+     270            0 :       retryAfterMs: input.retryAfterMs,
+     271            0 :       http: input.http,
+     272            0 :     })
+     273            0 :   }
+     274            2 :   return new UnknownProviderReason({ message: input.message, status: input.status, http: input.http })
+     275              : }
+     276              : 
+     277           18 : const statusError =
+     278           27 :   (request: HttpClientRequest.HttpClientRequest, redactedNames: ReadonlyArray<string | RegExp>) =>
+     279           14 :   (response: HttpClientResponse.HttpClientResponse) =>
+     280           15 :     Effect.gen(function* () {
+     281           44 :       if (response.status < 400) return response
+     282            0 :       const body = yield* response.text.pipe(Effect.catch(() => Effect.void))
+     283            0 :       const headers = normalizedHeaders(response.headers)
+     284            0 :       const retryAfter = retryAfterMs(headers)
+     285            0 :       const rateLimit = rateLimitDetails(headers, retryAfter)
+     286            0 :       const details = responseBody(body, request)
+     287            0 :       return yield* new LLMError({
+     288            0 :         module: "RequestExecutor",
+     289            0 :         method: "execute",
+     290            0 :         reason: statusReason({
+     291            0 :           status: response.status,
+     292            0 :           message: providerMessage(response.status, details),
+     293            0 :           retryAfterMs: retryAfter,
+     294            0 :           rateLimit,
+     295            0 :           http: responseHttp({
+     296            0 :             request,
+     297            0 :             response,
+     298            0 :             redactedNames,
+     299            0 :             body: details,
+     300            0 :             requestId: requestId(headers),
+     301            0 :             rateLimit,
+     302            0 :           }),
+     303            0 :         }),
+     304            0 :       })
+     305            3 :     })
+     306              : 
+     307            0 : const toHttpError = (redactedNames: ReadonlyArray<string | RegExp>) => (error: unknown) => {
+     308            0 :   const transportError = (input: {
+     309            0 :     readonly message: string
+     310            0 :     readonly kind?: string | undefined
+     311            0 :     readonly request?: HttpClientRequest.HttpClientRequest | undefined
+     312            0 :   }) =>
+     313            0 :     new LLMError({
+     314            0 :       module: "RequestExecutor",
+     315            0 :       method: "execute",
+     316            0 :       reason: new TransportReason({
+     317            0 :         message: input.message,
+     318            0 :         kind: input.kind,
+     319            0 :         url: input.request ? redactUrl(input.request.url) : undefined,
+     320            0 :         http: input.request ? new HttpContext({ request: requestDetails(input.request, redactedNames) }) : undefined,
+     321            0 :       }),
+     322            0 :     })
+     323            0 : 
+     324            0 :   if (Cause.isTimeoutError(error)) {
+     325            0 :     return transportError({ message: error.message, kind: "Timeout" })
+     326            0 :   }
+     327            0 :   if (!HttpClientError.isHttpClientError(error)) {
+     328            0 :     return transportError({ message: "HTTP transport failed" })
+     329            0 :   }
+     330            0 :   const request = "request" in error ? error.request : undefined
+     331            0 :   if (error.reason._tag === "TransportError") {
+     332            0 :     return transportError({
+     333            0 :       message: error.reason.description ?? "HTTP transport failed",
+     334            0 :       kind: error.reason._tag,
+     335            0 :       request,
+     336            0 :     })
+     337            0 :   }
+     338            0 :   return transportError({
+     339            0 :     message: `HTTP transport failed: ${error.reason._tag}`,
+     340            0 :     kind: error.reason._tag,
+     341            0 :     request,
+     342            2 :   })
+     343              : }
+     344              : 
+     345            0 : const retryDelay = (error: LLMError, attempt: number) => {
+     346            0 :   if (error.retryAfterMs !== undefined) return Effect.succeed(Math.min(error.retryAfterMs, MAX_DELAY_MS))
+     347            0 :   return Random.nextBetween(
+     348            0 :     Math.min(BASE_DELAY_MS * 2 ** attempt * 0.8, MAX_DELAY_MS),
+     349            0 :     Math.min(BASE_DELAY_MS * 2 ** attempt * 1.2, MAX_DELAY_MS),
+     350            2 :   ).pipe(Effect.map((delay) => Math.round(delay)))
+     351              : }
+     352              : 
+     353           27 : const retryStatusFailures = <A, R>(
+     354            8 :   effect: Effect.Effect<A, LLMError, R>,
+     355           23 :   retries = MAX_RETRIES,
+     356           16 :   attempt = 0,
+     357              : ): Effect.Effect<A, LLMError, R> =>
+     358            0 :   Effect.catchTag(effect, "LLM.Error", (error): Effect.Effect<A, LLMError, R> => {
+     359            0 :     if (!error.retryable || retries <= 0) return Effect.fail(error)
+     360            0 :     return retryDelay(error, attempt).pipe(
+     361            0 :       Effect.flatMap((delay) => Effect.sleep(delay)),
+     362            0 :       Effect.flatMap(() => retryStatusFailures(effect, retries - 1, attempt + 1)),
+     363              :     )
+     364            2 :   })
+     365              : 
+     366           33 : export const layer: Layer.Layer<Service, never, HttpClient.HttpClient> = Layer.effect(
+     367            9 :   Service,
+     368           15 :   Effect.gen(function* () {
+     369           44 :     const http = yield* HttpClient.HttpClient
+     370           32 :     const executeOnce = (request: HttpClientRequest.HttpClientRequest) =>
+     371           17 :       Effect.gen(function* () {
+     372           62 :         const redactedNames = yield* Headers.CurrentRedactedNames
+     373           19 :         return yield* http
+     374           17 :           .execute(request)
+     375          103 :           .pipe(Effect.mapError(toHttpError(redactedNames)), Effect.flatMap(statusError(request, redactedNames)))
+     376            3 :       })
+     377           23 :     return Service.of({
+     378           63 :       execute: (request) => retryStatusFailures(executeOnce(request)),
+     379            2 :     })
+     380            1 :   }),
+     381            3 : )
+     382              : 
+     383           75 : export const fetchLayer = layer.pipe(Layer.provide(FetchHttpClient.layer))
+     384              : 
+     385           45 : export * as RequestExecutor from "./executor"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/framing.ts.gcov.html b/packages/core/llm/src/route/framing.ts.gcov.html new file mode 100644 index 00000000..fdeff186 --- /dev/null +++ b/packages/core/llm/src/route/framing.ts.gcov.html @@ -0,0 +1,103 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route/framing.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/route - framing.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %33
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import type { Stream } from "effect"
+       2           54 : import * as ProviderShared from "../protocols/shared"
+       3              : import type { LLMError } from "../schema"
+       4              : 
+       5              : /**
+       6              :  * Decode a streaming HTTP response body into provider-protocol frames.
+       7              :  *
+       8              :  * `Framing` is the byte-stream-shaped seam between transport and protocol:
+       9              :  *
+      10              :  * - SSE (`Framing.sse`) — UTF-8 decode the body, run the SSE channel decoder,
+      11              :  *   drop empty / `[DONE]` keep-alives. Each emitted frame is the JSON `data:`
+      12              :  *   payload of one event.
+      13              :  * - AWS event stream — length-prefixed binary frames with CRC checksums.
+      14              :  *   Each emitted frame is one parsed binary event record.
+      15              :  *
+      16              :  * The frame type is opaque to this layer; the protocol's `decode` step turns
+      17              :  * a frame into a typed chunk.
+      18              :  */
+      19              : export interface Framing<Frame> {
+      20              :   readonly id: string
+      21              :   readonly frame: (bytes: Stream.Stream<Uint8Array, LLMError>) => Stream.Stream<Frame, LLMError>
+      22              : }
+      23              : 
+      24              : /** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
+      25           67 : export const sse: Framing<string> = { id: "sse", frame: ProviderShared.sseFraming }
+      26              : 
+      27           36 : export * as Framing from "./framing"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/index-sort-f.html b/packages/core/llm/src/route/index-sort-f.html new file mode 100644 index 00000000..fbecd2bc --- /dev/null +++ b/packages/core/llm/src/route/index-sort-f.html @@ -0,0 +1,161 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/routeCoverageTotalHit
Test:opencode-lcov.infoLines:55.6 %656365
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
auth-options.ts +
30.0%30.0%
+
30.0 %103
auth.ts +
73.3%73.3%
+
73.3 %8663
client.ts +
88.3%88.3%
+
88.3 %222196
endpoint.ts +
100.0%
+
100.0 %1919
executor.ts +
21.9%21.9%
+
21.9 %30166
framing.ts +
100.0%
+
100.0 %33
index.ts +
100.0%
+
100.0 %99
protocol.ts +
100.0%
+
100.0 %66
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/index-sort-l.html b/packages/core/llm/src/route/index-sort-l.html new file mode 100644 index 00000000..929bc297 --- /dev/null +++ b/packages/core/llm/src/route/index-sort-l.html @@ -0,0 +1,161 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/routeCoverageTotalHit
Test:opencode-lcov.infoLines:55.6 %656365
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
executor.ts +
21.9%21.9%
+
21.9 %30166
auth-options.ts +
30.0%30.0%
+
30.0 %103
auth.ts +
73.3%73.3%
+
73.3 %8663
client.ts +
88.3%88.3%
+
88.3 %222196
framing.ts +
100.0%
+
100.0 %33
protocol.ts +
100.0%
+
100.0 %66
index.ts +
100.0%
+
100.0 %99
endpoint.ts +
100.0%
+
100.0 %1919
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/index.html b/packages/core/llm/src/route/index.html new file mode 100644 index 00000000..18c74c64 --- /dev/null +++ b/packages/core/llm/src/route/index.html @@ -0,0 +1,161 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/routeCoverageTotalHit
Test:opencode-lcov.infoLines:55.6 %656365
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
auth-options.ts +
30.0%30.0%
+
30.0 %103
auth.ts +
73.3%73.3%
+
73.3 %8663
client.ts +
88.3%88.3%
+
88.3 %222196
endpoint.ts +
100.0%
+
100.0 %1919
executor.ts +
21.9%21.9%
+
21.9 %30166
framing.ts +
100.0%
+
100.0 %33
index.ts +
100.0%
+
100.0 %99
protocol.ts +
100.0%
+
100.0 %66
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/index.ts.gcov.html b/packages/core/llm/src/route/index.ts.gcov.html new file mode 100644 index 00000000..86e9d940 --- /dev/null +++ b/packages/core/llm/src/route/index.ts.gcov.html @@ -0,0 +1,101 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route/index.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/route - index.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %99
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           44 : export { Route, LLMClient } from "./client"
+       2              : export type {
+       3              :   Route as RouteShape,
+       4              :   RouteModelInput,
+       5              :   RouteRoutedModelInput,
+       6              :   RouteDefaults,
+       7              :   RouteDefaultsInput,
+       8              :   AnyRoute,
+       9              :   Interface as LLMClientShape,
+      10              :   Service as LLMClientService,
+      11              : } from "./client"
+      12           27 : export * from "./executor"
+      13           30 : export { Auth } from "./auth"
+      14           45 : export { AuthOptions } from "./auth-options"
+      15           38 : export { Endpoint } from "./endpoint"
+      16           36 : export { Framing } from "./framing"
+      17           38 : export { Protocol } from "./protocol"
+      18           83 : export { HttpTransport, WebSocketExecutor, WebSocketTransport } from "./transport"
+      19           40 : export * as Transport from "./transport"
+      20              : export type { Auth as AuthShape, AuthInput, Credential, CredentialError } from "./auth"
+      21              : export type { ApiKeyMode, AuthOverride, ProviderAuthOption } from "./auth-options"
+      22              : export type { Endpoint as EndpointFn, EndpointInput } from "./endpoint"
+      23              : export type { Framing as FramingDef } from "./framing"
+      24              : export type { Protocol as ProtocolDef } from "./protocol"
+      25              : export type { Transport as TransportDef, TransportRuntime } from "./transport"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/protocol.ts.gcov.html b/packages/core/llm/src/route/protocol.ts.gcov.html new file mode 100644 index 00000000..36a6330b --- /dev/null +++ b/packages/core/llm/src/route/protocol.ts.gcov.html @@ -0,0 +1,160 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route/protocol.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/route - protocol.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %66
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Schema, type Effect } from "effect"
+       2              : import type { LLMError, LLMEvent, LLMRequest, ProtocolID } from "../schema"
+       3              : 
+       4              : /**
+       5              :  * The semantic API contract of one model server family.
+       6              :  *
+       7              :  * A `Protocol` owns the parts of a route that are intrinsic to "what does
+       8              :  * this API look like": how a common `LLMRequest` becomes a provider-native
+       9              :  * body, what schema that body must satisfy before it is JSON-encoded, and
+      10              :  * how the streaming response decodes back into common `LLMEvent`s.
+      11              :  *
+      12              :  * Examples:
+      13              :  *
+      14              :  * - `OpenAIChat.protocol` — chat completions style
+      15              :  * - `OpenAIResponses.protocol` — responses API
+      16              :  * - `AnthropicMessages.protocol` — messages API with content blocks
+      17              :  * - `Gemini.protocol` — generateContent
+      18              :  * - `BedrockConverse.protocol` — Converse with binary event-stream framing
+      19              :  *
+      20              :  * A `Protocol` is **not** a deployment. It does not know which URL, which
+      21              :  * headers, or which auth scheme to use. Those are deployment concerns owned
+      22              :  * by `Route.make(...)` along with the chosen `Endpoint`, `Auth`,
+      23              :  * and `Framing`. This separation is what lets DeepSeek, TogetherAI, Cerebras,
+      24              :  * etc. all reuse `OpenAIChat.protocol` without forking 300 lines per provider.
+      25              :  *
+      26              :  * The four type parameters reflect the pipeline:
+      27              :  *
+      28              :  * - `Body` — provider-native request body candidate. `Route.make(...)`
+      29              :  *   validates and JSON-encodes it with `body.schema`.
+      30              :  * - `Frame` — one unit of the framed response stream. SSE: a JSON data
+      31              :  *   string. AWS event stream: a parsed binary frame.
+      32              :  * - `Event` — schema-decoded provider event produced from one frame.
+      33              :  * - `State` — accumulator threaded through `stream.step` to translate event
+      34              :  *   sequences into `LLMEvent` sequences.
+      35              :  */
+      36              : export interface Protocol<Body, Frame, Event, State> {
+      37              :   /** Stable id for the wire protocol implementation. */
+      38              :   readonly id: ProtocolID
+      39              :   /** Request side: schema for the provider-native body and how to build it. */
+      40              :   readonly body: ProtocolBody<Body>
+      41              :   /** Response side: streaming state machine. */
+      42              :   readonly stream: ProtocolStream<Frame, Event, State>
+      43              : }
+      44              : 
+      45              : export interface ProtocolBody<Body> {
+      46              :   /** Schema for the validated provider-native body sent as the JSON request. */
+      47              :   readonly schema: Schema.Codec<Body, unknown>
+      48              :   /** Build the provider-native body from a common `LLMRequest`. */
+      49              :   readonly from: (request: LLMRequest) => Effect.Effect<Body, LLMError>
+      50              : }
+      51              : 
+      52              : export interface ProtocolStream<Frame, Event, State> {
+      53              :   /** Schema for one decoded streaming event, decoded from a transport frame. */
+      54              :   readonly event: Schema.Codec<Event, Frame>
+      55              :   /** Initial parser state. Called once per response with the resolved request. */
+      56              :   readonly initial: (request: LLMRequest) => State
+      57              :   /** Translate one event into emitted `LLMEvent`s plus the next state. */
+      58              :   readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], LLMError>
+      59              :   /** Optional request-completion signal for transports that do not end naturally. */
+      60              :   readonly terminal?: (event: Event) => boolean
+      61              :   /** Optional flush emitted when the framed stream ends. */
+      62              :   readonly onHalt?: (state: State) => ReadonlyArray<LLMEvent>
+      63              : }
+      64              : 
+      65              : /**
+      66              :  * Construct a `Protocol` from its body and stream pieces:
+      67              :  *
+      68              :  * - `body.schema` infers the provider-native request body shape.
+      69              :  * - `body.from` ties the common `LLMRequest` to the provider body.
+      70              :  * - `stream.event` infers the decoded streaming event and the wire frame.
+      71              :  * - `stream.initial`, `stream.step`, and `stream.onHalt` infer the parser state.
+      72              :  *
+      73              :  * Provider implementations should usually call `Protocol.make({ ... })`
+      74              :  * without explicit type arguments; the schemas and parser functions are the
+      75              :  * source of truth. The constructor remains as the public seam for future
+      76              :  * cross-cutting concerns such as tracing or instrumentation.
+      77              :  */
+      78           19 : export const make = <Body, Frame, Event, State>(
+      79           10 :   input: Protocol<Body, Frame, Event, State>,
+      80            6 : ): Protocol<Body, Frame, Event, State> => input
+      81              : 
+      82           65 : export const jsonEvent = <const S extends Schema.Top>(schema: S) => Schema.fromJsonString(schema)
+      83              : 
+      84           38 : export * as Protocol from "./protocol"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/transport/http.ts.gcov.html b/packages/core/llm/src/route/transport/http.ts.gcov.html new file mode 100644 index 00000000..4728e714 --- /dev/null +++ b/packages/core/llm/src/route/transport/http.ts.gcov.html @@ -0,0 +1,231 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route/transport/http.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/route/transport - http.tsCoverageTotalHit
Test:opencode-lcov.infoLines:91.6 %119109
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           40 : import { Effect, Stream } from "effect"
+       2           47 : import { Headers, HttpClientRequest } from "effect/unstable/http"
+       3           31 : import { Auth } from "../auth"
+       4           55 : import { render as renderEndpoint } from "../endpoint"
+       5           37 : import { Framing, type Framing as FramingDef } from "../framing"
+       6              : import type { Transport, TransportPrepareInput } from "./index"
+       7           57 : import * as ProviderShared from "../../protocols/shared"
+       8           48 : import { mergeJsonRecords, type LLMRequest } from "../../schema"
+       9              : 
+      10              : export type JsonRequestInput<Body> = TransportPrepareInput<Body>
+      11              : 
+      12              : export interface JsonRequestParts<Body = unknown> {
+      13              :   readonly url: string
+      14              :   readonly jsonBody: Body | Record<string, unknown>
+      15              :   readonly bodyText: string
+      16              :   readonly headers: Headers.Headers
+      17              : }
+      18              : 
+      19              : export interface HttpPrepared<Frame> {
+      20              :   readonly request: HttpClientRequest.HttpClientRequest
+      21              :   readonly framing: FramingDef<Frame>
+      22              : }
+      23              : 
+      24           36 : const applyQuery = (url: string, query: Record<string, string> | undefined) => {
+      25           24 :   if (!query) return url
+      26            0 :   const next = new URL(url)
+      27            0 :   Object.entries(query).forEach(([key, value]) => next.searchParams.set(key, value))
+      28            2 :   return next.toString()
+      29              : }
+      30              : 
+      31           49 : const PROTOCOL_BODY_OVERLAY_DENYLIST = new Set([
+      32           12 :   "content",
+      33           13 :   "contents",
+      34           21 :   "frequencyPenalty",
+      35           22 :   "frequency_penalty",
+      36           21 :   "generationConfig",
+      37           20 :   "inferenceConfig",
+      38           10 :   "input",
+      39           14 :   "maxTokens",
+      40           15 :   "max_tokens",
+      41           13 :   "messages",
+      42           10 :   "model",
+      43           20 :   "presencePenalty",
+      44           21 :   "presence_penalty",
+      45           19 :   "responseFormat",
+      46           20 :   "response_format",
+      47            9 :   "seed",
+      48            9 :   "stop",
+      49           18 :   "stopSequences",
+      50           19 :   "stop_sequences",
+      51           11 :   "stream",
+      52           18 :   "streamOptions",
+      53           19 :   "stream_options",
+      54           11 :   "system",
+      55           22 :   "systemInstruction",
+      56           23 :   "system_instruction",
+      57           16 :   "temperature",
+      58           13 :   "thinking",
+      59           15 :   "toolChoice",
+      60           15 :   "toolConfig",
+      61           16 :   "tool_choice",
+      62           16 :   "tool_config",
+      63           10 :   "tools",
+      64            9 :   "topK",
+      65            9 :   "topP",
+      66           10 :   "top_k",
+      67            8 :   "top_p",
+      68            3 : ])
+      69              : 
+      70           41 : const forbiddenBodyOverlayKeys = (body: Record<string, unknown>) =>
+      71           73 :   Object.keys(body).filter((key) => PROTOCOL_BODY_OVERLAY_DENYLIST.has(key))
+      72              : 
+      73           53 : const bodyWithOverlay = <Body>(body: Body, request: LLMRequest, encodeBody: (body: Body) => string) =>
+      74           15 :   Effect.gen(function* () {
+      75           95 :     if (request.http?.body === undefined) return { jsonBody: body, bodyText: encodeBody(body) }
+      76           68 :     const forbiddenKeys = forbiddenBodyOverlayKeys(request.http.body)
+      77           32 :     if (forbiddenKeys.length > 0)
+      78            0 :       return yield* ProviderShared.invalidRequest(
+      79            0 :         `http.body cannot overlay protocol-owned field(s): ${forbiddenKeys.join(", ")}`,
+      80            2 :       )
+      81           39 :     if (ProviderShared.isRecord(body)) {
+      82           69 :       const overlaid = mergeJsonRecords(body, request.http.body) ?? {}
+      83           76 :       return { jsonBody: overlaid, bodyText: ProviderShared.encodeJson(overlaid) }
+      84            0 :     }
+      85            0 :     return yield* ProviderShared.invalidRequest("http.body can only overlay JSON object request bodies")
+      86            2 :   })
+      87              : 
+      88           41 : export const jsonRequestParts = <Body>(input: JsonRequestInput<Body>) =>
+      89           15 :   Effect.gen(function* () {
+      90           23 :     const url = applyQuery(
+      91           89 :       renderEndpoint(input.endpoint, { request: input.request, body: input.body }).toString(),
+      92           25 :       input.request.http?.query,
+      93            4 :     )
+      94           83 :     const body = yield* bodyWithOverlay(input.body, input.request, input.encodeBody)
+      95           54 :     const headers = yield* Auth.toEffect(input.auth)({
+      96           27 :       request: input.request,
+      97           19 :       method: "POST",
+      98            8 :       url,
+      99           24 :       body: body.bodyText,
+     100           37 :       headers: Headers.fromInput({
+     101           53 :         ...input.headers?.({ request: input.request }),
+     102           31 :         ...input.request.http?.headers,
+     103            4 :       }),
+     104            5 :     })
+     105           73 :     return { url, jsonBody: body.jsonBody, bodyText: body.bodyText, headers }
+     106            2 :   })
+     107              : 
+     108              : export interface HttpJsonInput<_Body, Frame> {
+     109              :   readonly framing: FramingDef<Frame>
+     110              : }
+     111              : 
+     112              : export type HttpJsonPatch<Body, Frame> = Partial<HttpJsonInput<Body, Frame>>
+     113              : 
+     114              : export interface HttpJsonTransport<Body, Frame> extends Transport<Body, HttpPrepared<Frame>, Frame> {
+     115              :   readonly with: (patch: HttpJsonPatch<Body, Frame>) => HttpJsonTransport<Body, Frame>
+     116              : }
+     117              : 
+     118           37 : export const httpJson = <Body, Frame>(input: HttpJsonInput<Body, Frame>): HttpJsonTransport<Body, Frame> => ({
+     119           18 :   id: "http-json",
+     120            8 :   with: (patch) => httpJson({ ...input, ...patch }),
+     121           26 :   prepare: (prepareInput) =>
+     122           25 :     jsonRequestParts({
+     123           14 :       ...prepareInput,
+     124            8 :     }).pipe(
+     125           27 :       Effect.map((parts) => ({
+     126          103 :         request: ProviderShared.jsonPost({ url: parts.url, body: parts.bodyText, headers: parts.headers }),
+     127           24 :         framing: input.framing,
+     128            2 :       })),
+     129            3 :     ),
+     130           39 :   frames: (prepared, request, runtime) =>
+     131           14 :     Stream.unwrap(
+     132           13 :       runtime.http
+     133           26 :         .execute(prepared.request)
+     134            5 :         .pipe(
+     135           24 :           Effect.map((response) =>
+     136           23 :             prepared.framing.frame(
+     137           21 :               response.stream.pipe(
+     138            0 :                 Stream.mapError((error) =>
+     139            0 :                   ProviderShared.eventError(
+     140            0 :                     `${request.model.provider}/${request.model.route.id}`,
+     141            0 :                     `Failed to read ${request.model.provider}/${request.model.route.id} stream`,
+     142              :                     ProviderShared.errorText(error),
+     143              :                   ),
+     144            1 :                 ),
+     145            1 :               ),
+     146              :             ),
+     147            1 :           ),
+     148            1 :         ),
+     149            1 :     ),
+     150            2 : })
+     151              : 
+     152           25 : export const sseJson = {
+     153           22 :   id: "http-json/sse",
+     154           45 :   with: <Body>() => httpJson<Body, string>({ framing: Framing.sse }),
+     155            1 : } as const
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/transport/index-sort-f.html b/packages/core/llm/src/route/transport/index-sort-f.html new file mode 100644 index 00000000..5abd2f35 --- /dev/null +++ b/packages/core/llm/src/route/transport/index-sort-f.html @@ -0,0 +1,116 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route/transport + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/route/transportCoverageTotalHit
Test:opencode-lcov.infoLines:42.6 %340145
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
http.ts +
91.6%91.6%
+
91.6 %119109
index.ts +
100.0%
+
100.0 %22
websocket.ts +
15.5%15.5%
+
15.5 %21934
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/transport/index-sort-l.html b/packages/core/llm/src/route/transport/index-sort-l.html new file mode 100644 index 00000000..8b897b11 --- /dev/null +++ b/packages/core/llm/src/route/transport/index-sort-l.html @@ -0,0 +1,116 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route/transport + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/route/transportCoverageTotalHit
Test:opencode-lcov.infoLines:42.6 %340145
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
websocket.ts +
15.5%15.5%
+
15.5 %21934
http.ts +
91.6%91.6%
+
91.6 %119109
index.ts +
100.0%
+
100.0 %22
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/transport/index.html b/packages/core/llm/src/route/transport/index.html new file mode 100644 index 00000000..53525a05 --- /dev/null +++ b/packages/core/llm/src/route/transport/index.html @@ -0,0 +1,116 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route/transport + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/route/transportCoverageTotalHit
Test:opencode-lcov.infoLines:42.6 %340145
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
http.ts +
91.6%91.6%
+
91.6 %119109
index.ts +
100.0%
+
100.0 %22
websocket.ts +
15.5%15.5%
+
15.5 %21934
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/transport/index.ts.gcov.html b/packages/core/llm/src/route/transport/index.ts.gcov.html new file mode 100644 index 00000000..aea5063a --- /dev/null +++ b/packages/core/llm/src/route/transport/index.ts.gcov.html @@ -0,0 +1,109 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route/transport/index.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/route/transport - index.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %22
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import type { Effect, Stream } from "effect"
+       2              : import type { Endpoint } from "../endpoint"
+       3              : import type { Auth } from "../auth"
+       4              : import type { Interface as RequestExecutorInterface } from "../executor"
+       5              : import type { Interface as WebSocketExecutorInterface } from "./websocket"
+       6              : import type { LLMError, LLMRequest } from "../../schema"
+       7              : 
+       8              : export interface TransportRuntime {
+       9              :   readonly http: RequestExecutorInterface
+      10              :   readonly webSocket?: WebSocketExecutorInterface
+      11              : }
+      12              : 
+      13              : export interface Transport<Body, Prepared, Frame> {
+      14              :   readonly id: string
+      15              :   readonly prepare: (input: TransportPrepareInput<Body>) => Effect.Effect<Prepared, LLMError>
+      16              :   readonly frames: (
+      17              :     prepared: Prepared,
+      18              :     request: LLMRequest,
+      19              :     runtime: TransportRuntime,
+      20              :   ) => Stream.Stream<Frame, LLMError>
+      21              : }
+      22              : 
+      23              : export interface TransportPrepareInput<Body> {
+      24              :   readonly body: Body
+      25              :   readonly request: LLMRequest
+      26              :   readonly endpoint: Endpoint<Body>
+      27              :   readonly auth: Auth
+      28              :   readonly encodeBody: (body: Body) => string
+      29              :   readonly headers?: (input: { readonly request: LLMRequest }) => Record<string, string>
+      30              : }
+      31              : 
+      32           40 : export * as HttpTransport from "./http"
+      33           67 : export { WebSocketExecutor, WebSocketTransport } from "./websocket"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/route/transport/websocket.ts.gcov.html b/packages/core/llm/src/route/transport/websocket.ts.gcov.html new file mode 100644 index 00000000..0e96ef1d --- /dev/null +++ b/packages/core/llm/src/route/transport/websocket.ts.gcov.html @@ -0,0 +1,356 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/route/transport/websocket.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/route/transport - websocket.tsCoverageTotalHit
Test:opencode-lcov.infoLines:15.5 %21934
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Cause, Context, Effect, Layer, Queue, Stream } from "effect"
+       2              : import { Headers } from "effect/unstable/http"
+       3           57 : import { LLMError, TransportReason } from "../../schema"
+       4           40 : import * as HttpTransport from "./http"
+       5              : import type { Transport } from "./index"
+       6              : 
+       7              : export interface WebSocketRequest {
+       8              :   readonly url: string
+       9              :   readonly headers: Headers.Headers
+      10              : }
+      11              : 
+      12              : export interface WebSocketConnection {
+      13              :   readonly sendText: (message: string) => Effect.Effect<void, LLMError>
+      14              :   readonly messages: Stream.Stream<string | Uint8Array, LLMError>
+      15              :   readonly close: Effect.Effect<void, never>
+      16              : }
+      17              : 
+      18              : export interface Interface {
+      19              :   readonly open: (input: WebSocketRequest) => Effect.Effect<WebSocketConnection, LLMError>
+      20              : }
+      21              : 
+      22              : type WebSocketConstructorWithHeaders = new (
+      23              :   url: string,
+      24              :   options?: { readonly headers?: Headers.Headers },
+      25              : ) => globalThis.WebSocket
+      26              : 
+      27           84 : export class Service extends Context.Service<Service, Interface>()("@opencode/LLM/WebSocketExecutor") {}
+      28              : 
+      29            0 : const transportError = (
+      30            0 :   method: string,
+      31            0 :   message: string,
+      32            0 :   input: { readonly url?: string; readonly kind?: string } = {},
+      33            0 : ) =>
+      34            0 :   new LLMError({
+      35            0 :     module: "WebSocketExecutor",
+      36            0 :     method,
+      37              :     reason: new TransportReason({ message, url: input.url, kind: input.kind }),
+      38            2 :   })
+      39              : 
+      40            0 : const eventMessage = (event: Event) => {
+      41            0 :   if ("message" in event && typeof event.message === "string") return event.message
+      42            2 :   return event.type
+      43              : }
+      44              : 
+      45            0 : const binaryMessage = (data: unknown) => {
+      46            0 :   if (data instanceof Uint8Array) return data
+      47            0 :   if (data instanceof ArrayBuffer) return new Uint8Array(data)
+      48            0 :   if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
+      49            2 :   return undefined
+      50              : }
+      51              : 
+      52            0 : const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => {
+      53            0 :   if (ws.readyState === globalThis.WebSocket.OPEN) return Effect.void
+      54            0 :   if (ws.readyState === globalThis.WebSocket.CLOSING || ws.readyState === globalThis.WebSocket.CLOSED) {
+      55            0 :     return Effect.fail(
+      56            0 :       transportError("open", `WebSocket closed before opening (state ${ws.readyState})`, {
+      57            0 :         url: input.url,
+      58            0 :         kind: "open",
+      59            0 :       }),
+      60            0 :     )
+      61            0 :   }
+      62            0 :   return Effect.callback<void, LLMError>((resume, signal) => {
+      63            0 :     const cleanup = () => {
+      64            0 :       ws.removeEventListener("open", onOpen)
+      65            0 :       ws.removeEventListener("error", onError)
+      66            0 :       ws.removeEventListener("close", onClose)
+      67            0 :       signal.removeEventListener("abort", onAbort)
+      68            0 :     }
+      69            0 :     const onAbort = () => {
+      70            0 :       cleanup()
+      71            0 :       if (ws.readyState !== globalThis.WebSocket.CLOSED && ws.readyState !== globalThis.WebSocket.CLOSING)
+      72            0 :         ws.close(1000)
+      73            0 :     }
+      74            0 :     const onOpen = () => {
+      75            0 :       cleanup()
+      76            0 :       resume(Effect.void)
+      77            0 :     }
+      78            0 :     const onError = (event: Event) => {
+      79            0 :       cleanup()
+      80            0 :       resume(
+      81            0 :         Effect.fail(
+      82            0 :           transportError("open", `Failed to open WebSocket: ${eventMessage(event)}`, { url: input.url, kind: "open" }),
+      83            0 :         ),
+      84            0 :       )
+      85            0 :     }
+      86            0 :     const onClose = (event: CloseEvent) => {
+      87            0 :       cleanup()
+      88            0 :       resume(
+      89            0 :         Effect.fail(
+      90            0 :           transportError("open", `WebSocket closed before opening with code ${event.code}`, {
+      91            0 :             url: input.url,
+      92            0 :             kind: "open",
+      93            0 :           }),
+      94            0 :         ),
+      95            0 :       )
+      96            0 :     }
+      97            0 :     ws.addEventListener("open", onOpen, { once: true })
+      98            0 :     ws.addEventListener("error", onError, { once: true })
+      99            0 :     ws.addEventListener("close", onClose, { once: true })
+     100            0 :     signal.addEventListener("abort", onAbort, { once: true })
+     101            2 :   })
+     102              : }
+     103              : 
+     104            0 : const webSocketUrl = (value: string) =>
+     105            0 :   Effect.try({
+     106            0 :     try: () => {
+     107            0 :       const url = new URL(value)
+     108            0 :       if (url.protocol === "https:") {
+     109            0 :         url.protocol = "wss:"
+     110            0 :         return url.toString()
+     111            0 :       }
+     112            0 :       if (url.protocol === "http:") {
+     113            0 :         url.protocol = "ws:"
+     114            0 :         return url.toString()
+     115            0 :       }
+     116            0 :       throw new Error(`Unsupported WebSocket URL protocol ${url.protocol}`)
+     117            0 :     },
+     118            0 :     catch: (error) =>
+     119            0 :       transportError("prepare", error instanceof Error ? error.message : "Invalid WebSocket URL", {
+     120            0 :         url: value,
+     121            0 :         kind: "websocket",
+     122              :       }),
+     123            2 :   })
+     124              : 
+     125            0 : export const open = (input: WebSocketRequest) =>
+     126            0 :   Effect.try({
+     127            0 :     try: () =>
+     128            0 :       new (globalThis.WebSocket as unknown as WebSocketConstructorWithHeaders)(input.url, { headers: input.headers }),
+     129            0 :     catch: (error) =>
+     130            0 :       transportError("open", error instanceof Error ? error.message : "Failed to construct WebSocket", {
+     131            0 :         url: input.url,
+     132            0 :         kind: "open",
+     133            0 :       }),
+     134            2 :   }).pipe(Effect.flatMap((ws) => fromWebSocket(ws, input)))
+     135              : 
+     136           66 : export const layer: Layer.Layer<Service> = Layer.succeed(Service, Service.of({ open }))
+     137              : 
+     138            0 : export const fromWebSocket = (
+     139            0 :   ws: globalThis.WebSocket,
+     140            0 :   input: WebSocketRequest,
+     141            0 : ): Effect.Effect<WebSocketConnection, LLMError> =>
+     142            0 :   Effect.gen(function* () {
+     143            0 :     yield* waitOpen(ws, input)
+     144            0 :     const messages = yield* Queue.bounded<string | Uint8Array, LLMError | Cause.Done<void>>(128)
+     145            0 : 
+     146            0 :     const onMessage = (event: MessageEvent) => {
+     147            0 :       if (typeof event.data === "string") return Queue.offerUnsafe(messages, event.data)
+     148            0 :       const binary = binaryMessage(event.data)
+     149            0 :       if (binary) return Queue.offerUnsafe(messages, binary)
+     150            0 :       Queue.failCauseUnsafe(
+     151            0 :         messages,
+     152            0 :         Cause.fail(
+     153            0 :           transportError("message", "Unsupported WebSocket message payload", { url: input.url, kind: "message" }),
+     154            0 :         ),
+     155            0 :       )
+     156            0 :     }
+     157            0 :     const onError = (event: Event) => {
+     158            0 :       Queue.failCauseUnsafe(
+     159            0 :         messages,
+     160            0 :         Cause.fail(
+     161            0 :           transportError("message", `WebSocket error: ${eventMessage(event)}`, { url: input.url, kind: "message" }),
+     162            0 :         ),
+     163            0 :       )
+     164            0 :     }
+     165            0 :     const onClose = (event: CloseEvent) => {
+     166            0 :       if (event.code === 1000 || event.code === 1005) return Queue.endUnsafe(messages)
+     167            0 :       Queue.failCauseUnsafe(
+     168            0 :         messages,
+     169            0 :         Cause.fail(
+     170            0 :           transportError("message", `WebSocket closed with code ${event.code}`, { url: input.url, kind: "close" }),
+     171            0 :         ),
+     172            0 :       )
+     173            0 :     }
+     174            0 :     const cleanup = Effect.sync(() => {
+     175            0 :       ws.removeEventListener("message", onMessage)
+     176            0 :       ws.removeEventListener("error", onError)
+     177            0 :       ws.removeEventListener("close", onClose)
+     178            0 :     }).pipe(Effect.andThen(Queue.shutdown(messages)))
+     179            0 : 
+     180            0 :     ws.addEventListener("message", onMessage)
+     181            0 :     ws.addEventListener("error", onError)
+     182            0 :     ws.addEventListener("close", onClose)
+     183            0 : 
+     184            0 :     return {
+     185            0 :       sendText: (message) =>
+     186            0 :         Effect.try({
+     187            0 :           try: () => ws.send(message),
+     188            0 :           catch: (error) =>
+     189            0 :             transportError("sendText", error instanceof Error ? error.message : "Failed to send WebSocket message", {
+     190            0 :               url: input.url,
+     191            0 :               kind: "write",
+     192            0 :             }),
+     193            0 :         }),
+     194            0 :       messages: Stream.fromQueue(messages),
+     195            0 :       close: cleanup.pipe(
+     196            0 :         Effect.andThen(
+     197            0 :           Effect.sync(() => {
+     198            0 :             if (ws.readyState === globalThis.WebSocket.CLOSED || ws.readyState === globalThis.WebSocket.CLOSING) return
+     199            0 :             ws.close(1000)
+     200            0 :           }),
+     201            0 :         ),
+     202            0 :       ),
+     203              :     }
+     204            2 :   })
+     205              : 
+     206            0 : export const messageText = (message: string | Uint8Array, decoder: TextDecoder) =>
+     207            2 :   typeof message === "string" ? message : decoder.decode(message)
+     208              : 
+     209              : export interface JsonPrepared {
+     210              :   readonly url: string
+     211              :   readonly headers: Headers.Headers
+     212              :   readonly message: string
+     213              : }
+     214              : 
+     215              : export interface JsonInput<Body, Message> {
+     216              :   readonly toMessage: (body: Body | Record<string, unknown>) => Effect.Effect<Message, LLMError>
+     217              :   readonly encodeMessage: (message: Message) => string
+     218              : }
+     219              : 
+     220              : export type JsonPatch<Body, Message> = Partial<JsonInput<Body, Message>>
+     221              : 
+     222              : export interface JsonTransport<Body, Message> extends Transport<Body, JsonPrepared, string> {
+     223              :   readonly with: (patch: JsonPatch<Body, Message>) => JsonTransport<Body, Message>
+     224              : }
+     225              : 
+     226           33 : export const json = <Body, Message>(input: JsonInput<Body, Message>): JsonTransport<Body, Message> => ({
+     227           23 :   id: "websocket-json",
+     228            8 :   with: (patch) => json({ ...input, ...patch }),
+     229            0 :   prepare: (prepareInput) =>
+     230            0 :     Effect.gen(function* () {
+     231            0 :       const parts = yield* HttpTransport.jsonRequestParts({
+     232            0 :         ...prepareInput,
+     233            0 :       })
+     234            0 :       return {
+     235            0 :         url: yield* webSocketUrl(parts.url),
+     236            0 :         headers: parts.headers,
+     237            0 :         message: input.encodeMessage(yield* input.toMessage(parts.jsonBody)),
+     238              :       }
+     239            3 :     }),
+     240            0 :   frames: (prepared, _request, runtime) => {
+     241            0 :     const webSocket = runtime.webSocket
+     242            0 :     if (!webSocket) {
+     243            0 :       return Stream.fail(
+     244            0 :         transportError("json", "WebSocket JSON transport requires WebSocketExecutor.Service", {
+     245            0 :           url: prepared.url,
+     246            0 :           kind: "websocket",
+     247            0 :         }),
+     248            0 :       )
+     249            0 :     }
+     250            0 :     const decoder = new TextDecoder()
+     251            0 :     return Stream.unwrap(
+     252            0 :       Effect.gen(function* () {
+     253            0 :         const connection = yield* Effect.acquireRelease(
+     254            0 :           webSocket.open({ url: prepared.url, headers: prepared.headers }),
+     255            0 :           (connection) => connection.close,
+     256            0 :         )
+     257            0 :         yield* connection.sendText(prepared.message)
+     258            0 :         return connection.messages.pipe(Stream.map((message) => messageText(message, decoder)))
+     259            0 :       }),
+     260            1 :     )
+     261              :   },
+     262            2 : })
+     263              : 
+     264           31 : export const jsonTransport = {
+     265           23 :   id: "websocket-json",
+     266           11 :   with: json,
+     267            2 : } as const
+     268              : 
+     269           35 : export const WebSocketExecutor = {
+     270           10 :   Service,
+     271            8 :   layer,
+     272            7 :   open,
+     273           16 :   fromWebSocket,
+     274           12 :   messageText,
+     275            2 : } as const
+     276              : 
+     277           36 : export const WebSocketTransport = {
+     278            7 :   json,
+     279           14 :   jsonTransport,
+     280            1 : } as const
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/schema/errors.ts.gcov.html b/packages/core/llm/src/schema/errors.ts.gcov.html new file mode 100644 index 00000000..775d7c3d --- /dev/null +++ b/packages/core/llm/src/schema/errors.ts.gcov.html @@ -0,0 +1,283 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/schema/errors.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/schema - errors.tsCoverageTotalHit
Test:opencode-lcov.infoLines:78.4 %167131
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1            0 : import { Schema } from "effect"
+       2          137 : import { ModelID, ProviderID, ProviderMetadata, RouteID } from "./ids"
+       3              : 
+       4          160 : export const ProviderFailureClassification = Schema.Literal("context-overflow")
+       5              : export type ProviderFailureClassification = typeof ProviderFailureClassification.Type
+       6              : 
+       7          162 : export class HttpRequestDetails extends Schema.Class<HttpRequestDetails>("LLM.HttpRequestDetails")({
+       8           48 :   method: Schema.String,
+       9           42 :   url: Schema.String,
+      10          106 :   headers: Schema.Record(Schema.String, Schema.String),
+      11           10 : }) {}
+      12              : 
+      13          166 : export class HttpResponseDetails extends Schema.Class<HttpResponseDetails>("LLM.HttpResponseDetails")({
+      14           48 :   status: Schema.Number,
+      15          106 :   headers: Schema.Record(Schema.String, Schema.String),
+      16           10 : }) {}
+      17              : 
+      18          170 : export class HttpRateLimitDetails extends Schema.Class<HttpRateLimitDetails>("LLM.HttpRateLimitDetails")({
+      19           94 :   retryAfterMs: Schema.optional(Schema.Number),
+      20          140 :   limit: Schema.optional(Schema.Record(Schema.String, Schema.String)),
+      21          148 :   remaining: Schema.optional(Schema.Record(Schema.String, Schema.String)),
+      22          136 :   reset: Schema.optional(Schema.Record(Schema.String, Schema.String)),
+      23           10 : }) {}
+      24              : 
+      25          134 : export class HttpContext extends Schema.Class<HttpContext>("LLM.HttpContext")({
+      26           60 :   request: HttpRequestDetails,
+      27           98 :   response: Schema.optional(HttpResponseDetails),
+      28           78 :   body: Schema.optional(Schema.String),
+      29           98 :   bodyTruncated: Schema.optional(Schema.Boolean),
+      30           88 :   requestId: Schema.optional(Schema.String),
+      31           98 :   rateLimit: Schema.optional(HttpRateLimitDetails),
+      32           10 : }) {}
+      33              : 
+      34          170 : export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("LLM.Error.InvalidRequest")({
+      35           74 :   _tag: Schema.tag("InvalidRequest"),
+      36           50 :   message: Schema.String,
+      37           88 :   parameter: Schema.optional(Schema.String),
+      38          130 :   classification: Schema.optional(ProviderFailureClassification),
+      39          108 :   providerMetadata: Schema.optional(ProviderMetadata),
+      40           70 :   http: Schema.optional(HttpContext),
+      41            0 : }) {
+      42            0 :   get retryable() {
+      43           18 :     return false
+      44              :   }
+      45            2 : }
+      46              : 
+      47          142 : export class NoRouteReason extends Schema.Class<NoRouteReason>("LLM.Error.NoRoute")({
+      48           60 :   _tag: Schema.tag("NoRoute"),
+      49           34 :   route: RouteID,
+      50           46 :   provider: ProviderID,
+      51           30 :   model: ModelID,
+      52            0 : }) {
+      53            0 :   get retryable() {
+      54            0 :     return false
+      55            0 :   }
+      56            0 : 
+      57            0 :   get message() {
+      58           82 :     return `No LLM route for ${this.provider}/${this.model} using ${this.route}`
+      59              :   }
+      60            2 : }
+      61              : 
+      62          170 : export class AuthenticationReason extends Schema.Class<AuthenticationReason>("LLM.Error.Authentication")({
+      63           74 :   _tag: Schema.tag("Authentication"),
+      64           50 :   message: Schema.String,
+      65          196 :   kind: Schema.Literals(["missing", "invalid", "expired", "insufficient-permissions", "unknown"]),
+      66          108 :   providerMetadata: Schema.optional(ProviderMetadata),
+      67           70 :   http: Schema.optional(HttpContext),
+      68            0 : }) {
+      69            0 :   get retryable() {
+      70           18 :     return false
+      71              :   }
+      72            2 : }
+      73              : 
+      74          150 : export class RateLimitReason extends Schema.Class<RateLimitReason>("LLM.Error.RateLimit")({
+      75           64 :   _tag: Schema.tag("RateLimit"),
+      76           50 :   message: Schema.String,
+      77           94 :   retryAfterMs: Schema.optional(Schema.Number),
+      78          102 :   rateLimit: Schema.optional(HttpRateLimitDetails),
+      79          108 :   providerMetadata: Schema.optional(ProviderMetadata),
+      80           70 :   http: Schema.optional(HttpContext),
+      81            0 : }) {
+      82            0 :   get retryable() {
+      83           17 :     return true
+      84              :   }
+      85            2 : }
+      86              : 
+      87          166 : export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("LLM.Error.QuotaExceeded")({
+      88           72 :   _tag: Schema.tag("QuotaExceeded"),
+      89           50 :   message: Schema.String,
+      90          108 :   providerMetadata: Schema.optional(ProviderMetadata),
+      91           70 :   http: Schema.optional(HttpContext),
+      92            0 : }) {
+      93            0 :   get retryable() {
+      94           18 :     return false
+      95              :   }
+      96            2 : }
+      97              : 
+      98          166 : export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("LLM.Error.ContentPolicy")({
+      99           72 :   _tag: Schema.tag("ContentPolicy"),
+     100           50 :   message: Schema.String,
+     101          108 :   providerMetadata: Schema.optional(ProviderMetadata),
+     102           70 :   http: Schema.optional(HttpContext),
+     103            0 : }) {
+     104            0 :   get retryable() {
+     105           18 :     return false
+     106              :   }
+     107            2 : }
+     108              : 
+     109          178 : export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>("LLM.Error.ProviderInternal")({
+     110           78 :   _tag: Schema.tag("ProviderInternal"),
+     111           50 :   message: Schema.String,
+     112           48 :   status: Schema.Number,
+     113           94 :   retryAfterMs: Schema.optional(Schema.Number),
+     114          108 :   providerMetadata: Schema.optional(ProviderMetadata),
+     115           70 :   http: Schema.optional(HttpContext),
+     116            0 : }) {
+     117            0 :   get retryable() {
+     118           17 :     return true
+     119              :   }
+     120            2 : }
+     121              : 
+     122          150 : export class TransportReason extends Schema.Class<TransportReason>("LLM.Error.Transport")({
+     123           64 :   _tag: Schema.tag("Transport"),
+     124           50 :   message: Schema.String,
+     125           78 :   kind: Schema.optional(Schema.String),
+     126           76 :   url: Schema.optional(Schema.String),
+     127           70 :   http: Schema.optional(HttpContext),
+     128            0 : }) {
+     129            0 :   get retryable() {
+     130           18 :     return false
+     131              :   }
+     132            2 : }
+     133              : 
+     134          122 : export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOutputReason>(
+     135           66 :   "LLM.Error.InvalidProviderOutput",
+     136           10 : )({
+     137           88 :   _tag: Schema.tag("InvalidProviderOutput"),
+     138           50 :   message: Schema.String,
+     139           80 :   route: Schema.optional(Schema.String),
+     140           76 :   raw: Schema.optional(Schema.String),
+     141          104 :   providerMetadata: Schema.optional(ProviderMetadata),
+     142            0 : }) {
+     143            0 :   get retryable() {
+     144           18 :     return false
+     145              :   }
+     146            2 : }
+     147              : 
+     148          174 : export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("LLM.Error.UnknownProvider")({
+     149           76 :   _tag: Schema.tag("UnknownProvider"),
+     150           50 :   message: Schema.String,
+     151           82 :   status: Schema.optional(Schema.Number),
+     152          108 :   providerMetadata: Schema.optional(ProviderMetadata),
+     153           70 :   http: Schema.optional(HttpContext),
+     154            0 : }) {
+     155            0 :   get retryable() {
+     156           18 :     return false
+     157              :   }
+     158            2 : }
+     159              : 
+     160           90 : export const LLMErrorReason = Schema.Union([
+     161           46 :   InvalidRequestReason,
+     162           32 :   NoRouteReason,
+     163           46 :   AuthenticationReason,
+     164           36 :   RateLimitReason,
+     165           44 :   QuotaExceededReason,
+     166           44 :   ContentPolicyReason,
+     167           50 :   ProviderInternalReason,
+     168           36 :   TransportReason,
+     169           60 :   InvalidProviderOutputReason,
+     170           44 :   UnknownProviderReason,
+     171           76 : ]).pipe(Schema.toTaggedUnion("_tag"))
+     172              : export type LLMErrorReason = Schema.Schema.Type<typeof LLMErrorReason>
+     173              : 
+     174          142 : export class LLMError extends Schema.TaggedErrorClass<LLMError>()("LLM.Error", {
+     175           48 :   module: Schema.String,
+     176           48 :   method: Schema.String,
+     177           46 :   reason: LLMErrorReason,
+     178           10 : }) {
+     179            0 :   override readonly cause = this.reason
+     180            0 : 
+     181            0 :   get retryable() {
+     182            0 :     return this.reason.retryable
+     183            0 :   }
+     184            0 : 
+     185            0 :   get retryAfterMs() {
+     186            0 :     return "retryAfterMs" in this.reason ? this.reason.retryAfterMs : undefined
+     187            0 :   }
+     188            0 : 
+     189            0 :   override get message() {
+     190           68 :     return `${this.module}.${this.method}: ${this.reason.message}`
+     191              :   }
+     192            2 : }
+     193              : 
+     194              : /**
+     195              :  * Failure type for tool execute handlers. Handlers must map their internal
+     196              :  * errors to this shape; the runtime catches `ToolFailure`s and surfaces them
+     197              :  * as `tool-error` events plus a `tool-result` of `type: "error"` so the model
+     198              :  * can self-correct.
+     199              :  *
+     200              :  * Anything thrown or yielded by a handler that is not a `ToolFailure` is
+     201              :  * treated as a defect and fails the stream.
+     202              :  */
+     203          160 : export class ToolFailure extends Schema.TaggedErrorClass<ToolFailure>()("LLM.ToolFailure", {
+     204           50 :   message: Schema.String,
+     205           84 :   error: Schema.optional(Schema.Defect()),
+     206          144 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+     207            9 : }) {}
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/schema/events.ts.gcov.html b/packages/core/llm/src/schema/events.ts.gcov.html new file mode 100644 index 00000000..27a64bed --- /dev/null +++ b/packages/core/llm/src/schema/events.ts.gcov.html @@ -0,0 +1,694 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/schema/events.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/schema - events.tsCoverageTotalHit
Test:opencode-lcov.infoLines:57.6 %415239
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1            0 : import { Schema } from "effect"
+       2           99 : import { ContentBlockID, FinishReason, ProtocolID, ProviderMetadata, RouteID, ToolCallID } from "./ids"
+       3           40 : import { ModelSchema } from "./options"
+       4           96 : import { Message, ToolCallPart, ToolOutput, ToolResultPart, ToolResultValue, type ContentPart } from "./messages"
+       5           57 : import { ProviderFailureClassification } from "./errors"
+       6              : 
+       7              : /**
+       8              :  * Token usage reported by an LLM provider.
+       9              :  *
+      10              :  * **Inclusive totals** (match AI SDK / OpenAI / LangChain convention — a
+      11              :  * reader from any of those ecosystems sees the number they expect):
+      12              :  *
+      13              :  * - `inputTokens` — total prompt tokens, *including* cached reads/writes.
+      14              :  * - `outputTokens` — total output tokens, *including* reasoning.
+      15              :  * - `totalTokens` — provider-supplied total, or `inputTokens + outputTokens`.
+      16              :  *
+      17              :  * **Non-overlapping breakdown** (every field is independently meaningful;
+      18              :  * consumers never have to subtract):
+      19              :  *
+      20              :  * - `nonCachedInputTokens` — the "fresh" portion of the prompt.
+      21              :  * - `cacheReadInputTokens` — input tokens served from cache.
+      22              :  * - `cacheWriteInputTokens` — input tokens written to cache.
+      23              :  * - `reasoningTokens` — subset of `outputTokens` spent on hidden reasoning.
+      24              :  *
+      25              :  * **Invariant**: `nonCachedInputTokens + cacheReadInputTokens +
+      26              :  * cacheWriteInputTokens = inputTokens`, and `reasoningTokens ≤ outputTokens`.
+      27              :  * Each protocol mapper computes whichever side it doesn't get natively,
+      28              :  * with `Math.max(0, …)` clamping for defense against provider bugs. Because
+      29              :  * every breakdown field is stored independently, downstream consumers can
+      30              :  * read whatever they need (cost-by-category, context-pressure, AI-SDK-style
+      31              :  * inclusive total) without ever subtracting — eliminating the underflow
+      32              :  * class of bug where a clamped difference would silently store the wrong
+      33              :  * value.
+      34              :  *
+      35              :  * **Semantics by provider**:
+      36              :  *
+      37              :  * - OpenAI Chat / Responses / Gemini / Bedrock: provider reports inclusive
+      38              :  *   `inputTokens` and an inclusive `outputTokens`; mapper subtracts to
+      39              :  *   derive the breakdown.
+      40              :  * - Anthropic: provider reports the breakdown natively (`input_tokens` is
+      41              :  *   non-cached only); mapper sums to derive the inclusive `inputTokens`.
+      42              :  *   Anthropic does *not* break extended-thinking out of `output_tokens`, so
+      43              :  *   `reasoningTokens` is `undefined` and `outputTokens` carries the
+      44              :  *   combined total — a documented limitation of the Anthropic API.
+      45              :  *
+      46              :  * `providerMetadata` always carries the provider's raw usage payload —
+      47              :  * keyed by provider name (`{ openai: ... }`, `{ anthropic: ... }`, etc.)
+      48              :  * — for fields we don't normalize and for billing-level audit trails.
+      49              :  * Matches the same escape-hatch field on `LLMEvent`.
+      50              :  */
+      51           55 : export class Usage extends Schema.Class<Usage>("LLM.Usage")({
+      52           46 :   inputTokens: Schema.optional(Schema.Number),
+      53           47 :   outputTokens: Schema.optional(Schema.Number),
+      54           55 :   nonCachedInputTokens: Schema.optional(Schema.Number),
+      55           55 :   cacheReadInputTokens: Schema.optional(Schema.Number),
+      56           56 :   cacheWriteInputTokens: Schema.optional(Schema.Number),
+      57           50 :   reasoningTokens: Schema.optional(Schema.Number),
+      58           46 :   totalTokens: Schema.optional(Schema.Number),
+      59           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+      60            4 : }) {
+      61              :   /**
+      62              :    * Visible output tokens — `outputTokens` minus `reasoningTokens`, clamped
+      63              :    * to zero. The one place subtraction happens in this contract; the clamp
+      64              :    * means a provider reporting `reasoningTokens > outputTokens` produces a
+      65              :    * harmless zero rather than a negative that crashes downstream schemas.
+      66              :    */
+      67            8 :   get visibleOutputTokens() {
+      68           82 :     return Math.max(0, (this.outputTokens ?? 0) - (this.reasoningTokens ?? 0))
+      69              :   }
+      70              : 
+      71           13 :   static from(input: UsageInput) {
+      72           55 :     return input instanceof Usage ? input : new Usage(input)
+      73              :   }
+      74            1 : }
+      75              : 
+      76              : export type UsageInput = Usage | ConstructorParameters<typeof Usage>[0]
+      77              : 
+      78           41 : export const StepStart = Schema.Struct({
+      79           33 :   type: Schema.tag("step-start"),
+      80           21 :   index: Schema.Number,
+      81           51 : }).annotate({ identifier: "LLM.Event.StepStart" })
+      82              : export type StepStart = Schema.Schema.Type<typeof StepStart>
+      83              : 
+      84           41 : export const TextStart = Schema.Struct({
+      85           33 :   type: Schema.tag("text-start"),
+      86           21 :   id: ContentBlockID,
+      87           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+      88           51 : }).annotate({ identifier: "LLM.Event.TextStart" })
+      89              : export type TextStart = Schema.Schema.Type<typeof TextStart>
+      90              : 
+      91           41 : export const TextDelta = Schema.Struct({
+      92           33 :   type: Schema.tag("text-delta"),
+      93           21 :   id: ContentBlockID,
+      94           22 :   text: Schema.String,
+      95           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+      96           51 : }).annotate({ identifier: "LLM.Event.TextDelta" })
+      97              : export type TextDelta = Schema.Schema.Type<typeof TextDelta>
+      98              : 
+      99           39 : export const TextEnd = Schema.Struct({
+     100           31 :   type: Schema.tag("text-end"),
+     101           21 :   id: ContentBlockID,
+     102           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+     103           49 : }).annotate({ identifier: "LLM.Event.TextEnd" })
+     104              : export type TextEnd = Schema.Schema.Type<typeof TextEnd>
+     105              : 
+     106           46 : export const ReasoningStart = Schema.Struct({
+     107           38 :   type: Schema.tag("reasoning-start"),
+     108           21 :   id: ContentBlockID,
+     109           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+     110           56 : }).annotate({ identifier: "LLM.Event.ReasoningStart" })
+     111              : export type ReasoningStart = Schema.Schema.Type<typeof ReasoningStart>
+     112              : 
+     113           46 : export const ReasoningDelta = Schema.Struct({
+     114           38 :   type: Schema.tag("reasoning-delta"),
+     115           21 :   id: ContentBlockID,
+     116           22 :   text: Schema.String,
+     117           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+     118           56 : }).annotate({ identifier: "LLM.Event.ReasoningDelta" })
+     119              : export type ReasoningDelta = Schema.Schema.Type<typeof ReasoningDelta>
+     120              : 
+     121           44 : export const ReasoningEnd = Schema.Struct({
+     122           36 :   type: Schema.tag("reasoning-end"),
+     123           21 :   id: ContentBlockID,
+     124           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+     125           54 : }).annotate({ identifier: "LLM.Event.ReasoningEnd" })
+     126              : export type ReasoningEnd = Schema.Schema.Type<typeof ReasoningEnd>
+     127              : 
+     128           46 : export const ToolInputStart = Schema.Struct({
+     129           39 :   type: Schema.tag("tool-input-start"),
+     130           17 :   id: ToolCallID,
+     131           22 :   name: Schema.String,
+     132           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+     133           56 : }).annotate({ identifier: "LLM.Event.ToolInputStart" })
+     134              : export type ToolInputStart = Schema.Schema.Type<typeof ToolInputStart>
+     135              : 
+     136           46 : export const ToolInputDelta = Schema.Struct({
+     137           39 :   type: Schema.tag("tool-input-delta"),
+     138           17 :   id: ToolCallID,
+     139           22 :   name: Schema.String,
+     140           20 :   text: Schema.String,
+     141           56 : }).annotate({ identifier: "LLM.Event.ToolInputDelta" })
+     142              : export type ToolInputDelta = Schema.Schema.Type<typeof ToolInputDelta>
+     143              : 
+     144           44 : export const ToolInputEnd = Schema.Struct({
+     145           37 :   type: Schema.tag("tool-input-end"),
+     146           17 :   id: ToolCallID,
+     147           22 :   name: Schema.String,
+     148           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+     149           54 : }).annotate({ identifier: "LLM.Event.ToolInputEnd" })
+     150              : export type ToolInputEnd = Schema.Schema.Type<typeof ToolInputEnd>
+     151              : 
+     152           40 : export const ToolCall = Schema.Struct({
+     153           32 :   type: Schema.tag("tool-call"),
+     154           17 :   id: ToolCallID,
+     155           22 :   name: Schema.String,
+     156           24 :   input: Schema.Unknown,
+     157           52 :   providerExecuted: Schema.optional(Schema.Boolean),
+     158           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+     159           50 : }).annotate({ identifier: "LLM.Event.ToolCall" })
+     160              : export type ToolCall = Schema.Schema.Type<typeof ToolCall>
+     161              : 
+     162           42 : export const ToolResult = Schema.Struct({
+     163           34 :   type: Schema.tag("tool-result"),
+     164           17 :   id: ToolCallID,
+     165           22 :   name: Schema.String,
+     166           26 :   result: ToolResultValue,
+     167           38 :   output: Schema.optional(ToolOutput),
+     168           52 :   providerExecuted: Schema.optional(Schema.Boolean),
+     169           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+     170           52 : }).annotate({ identifier: "LLM.Event.ToolResult" })
+     171              : export type ToolResult = Schema.Schema.Type<typeof ToolResult>
+     172              : 
+     173           41 : export const ToolError = Schema.Struct({
+     174           33 :   type: Schema.tag("tool-error"),
+     175           17 :   id: ToolCallID,
+     176           22 :   name: Schema.String,
+     177           25 :   message: Schema.String,
+     178           42 :   error: Schema.optional(Schema.Defect()),
+     179           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+     180           51 : }).annotate({ identifier: "LLM.Event.ToolError" })
+     181              : export type ToolError = Schema.Schema.Type<typeof ToolError>
+     182              : 
+     183           42 : export const StepFinish = Schema.Struct({
+     184           34 :   type: Schema.tag("step-finish"),
+     185           23 :   index: Schema.Number,
+     186           23 :   reason: FinishReason,
+     187           32 :   usage: Schema.optional(Usage),
+     188           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+     189           52 : }).annotate({ identifier: "LLM.Event.StepFinish" })
+     190              : export type StepFinish = Schema.Schema.Type<typeof StepFinish>
+     191              : 
+     192           38 : export const Finish = Schema.Struct({
+     193           29 :   type: Schema.tag("finish"),
+     194           23 :   reason: FinishReason,
+     195           32 :   usage: Schema.optional(Usage),
+     196           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+     197           48 : }).annotate({ identifier: "LLM.Event.Finish" })
+     198              : export type Finish = Schema.Schema.Type<typeof Finish>
+     199              : 
+     200           50 : export const ProviderErrorEvent = Schema.Struct({
+     201           37 :   type: Schema.tag("provider-error"),
+     202           25 :   message: Schema.String,
+     203           65 :   classification: Schema.optional(ProviderFailureClassification),
+     204           45 :   retryable: Schema.optional(Schema.Boolean),
+     205           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+     206           55 : }).annotate({ identifier: "LLM.Event.ProviderError" })
+     207              : export type ProviderErrorEvent = Schema.Schema.Type<typeof ProviderErrorEvent>
+     208              : 
+     209           38 : const llmEventTagged = Schema.Union([
+     210           12 :   StepStart,
+     211           12 :   TextStart,
+     212           12 :   TextDelta,
+     213           10 :   TextEnd,
+     214           17 :   ReasoningStart,
+     215           17 :   ReasoningDelta,
+     216           15 :   ReasoningEnd,
+     217           17 :   ToolInputStart,
+     218           17 :   ToolInputDelta,
+     219           15 :   ToolInputEnd,
+     220           11 :   ToolCall,
+     221           13 :   ToolResult,
+     222           12 :   ToolError,
+     223           13 :   StepFinish,
+     224            9 :   Finish,
+     225           19 :   ProviderErrorEvent,
+     226           38 : ]).pipe(Schema.toTaggedUnion("type"))
+     227              : 
+     228              : type WithID<Event extends { readonly id: unknown }, ID> = Omit<Event, "type" | "id"> & { readonly id: ID | string }
+     229              : type WithUsage<Event extends { readonly usage?: Usage }> = Omit<Event, "type" | "usage"> & {
+     230              :   readonly usage?: UsageInput
+     231              : }
+     232              : 
+     233           59 : const contentBlockID = (value: ContentBlockID | string) => ContentBlockID.make(value)
+     234           51 : const toolCallID = (value: ToolCallID | string) => ToolCallID.make(value)
+     235              : 
+     236              : /**
+     237              :  * camelCase aliases for `LLMEvent.guards` (provided by `Schema.toTaggedUnion`).
+     238              :  * Lets consumers write `events.filter(LLMEvent.is.toolCall)` instead of
+     239              :  * `events.filter(LLMEvent.guards["tool-call"])`.
+     240              :  */
+     241           56 : export const LLMEvent = Object.assign(llmEventTagged, {
+     242           28 :   stepStart: StepStart.make,
+     243           81 :   textStart: (input: WithID<TextStart, ContentBlockID>) => TextStart.make({ ...input, id: contentBlockID(input.id) }),
+     244           81 :   textDelta: (input: WithID<TextDelta, ContentBlockID>) => TextDelta.make({ ...input, id: contentBlockID(input.id) }),
+     245           77 :   textEnd: (input: WithID<TextEnd, ContentBlockID>) => TextEnd.make({ ...input, id: contentBlockID(input.id) }),
+     246           26 :   reasoningStart: (input: WithID<ReasoningStart, ContentBlockID>) =>
+     247           65 :     ReasoningStart.make({ ...input, id: contentBlockID(input.id) }),
+     248           26 :   reasoningDelta: (input: WithID<ReasoningDelta, ContentBlockID>) =>
+     249           65 :     ReasoningDelta.make({ ...input, id: contentBlockID(input.id) }),
+     250           24 :   reasoningEnd: (input: WithID<ReasoningEnd, ContentBlockID>) =>
+     251           63 :     ReasoningEnd.make({ ...input, id: contentBlockID(input.id) }),
+     252           26 :   toolInputStart: (input: WithID<ToolInputStart, ToolCallID>) =>
+     253           61 :     ToolInputStart.make({ ...input, id: toolCallID(input.id) }),
+     254           26 :   toolInputDelta: (input: WithID<ToolInputDelta, ToolCallID>) =>
+     255           61 :     ToolInputDelta.make({ ...input, id: toolCallID(input.id) }),
+     256           83 :   toolInputEnd: (input: WithID<ToolInputEnd, ToolCallID>) => ToolInputEnd.make({ ...input, id: toolCallID(input.id) }),
+     257           75 :   toolCall: (input: WithID<ToolCall, ToolCallID>) => ToolCall.make({ ...input, id: toolCallID(input.id) }),
+     258           22 :   toolResult: (input: WithID<ToolResult, ToolCallID>) =>
+     259           24 :     ToolResult.make({
+     260           10 :       ...input,
+     261           29 :       id: toolCallID(input.id),
+     262          110 :       output: input.output === undefined ? undefined : ToolOutput.make(input.output.structured, input.output.content),
+     263            4 :     }),
+     264           77 :   toolError: (input: WithID<ToolError, ToolCallID>) => ToolError.make({ ...input, id: toolCallID(input.id) }),
+     265           22 :   stepFinish: (input: WithUsage<StepFinish>) =>
+     266           24 :     StepFinish.make({
+     267           10 :       ...input,
+     268           69 :       usage: input.usage === undefined ? undefined : Usage.from(input.usage),
+     269            4 :     }),
+     270           18 :   finish: (input: WithUsage<Finish>) =>
+     271           20 :     Finish.make({
+     272           10 :       ...input,
+     273           69 :       usage: input.usage === undefined ? undefined : Usage.from(input.usage),
+     274            4 :     }),
+     275           41 :   providerError: ProviderErrorEvent.make,
+     276            9 :   is: {
+     277           51 :     stepStart: llmEventTagged.guards["step-start"],
+     278           51 :     textStart: llmEventTagged.guards["text-start"],
+     279           51 :     textDelta: llmEventTagged.guards["text-delta"],
+     280           47 :     textEnd: llmEventTagged.guards["text-end"],
+     281           61 :     reasoningStart: llmEventTagged.guards["reasoning-start"],
+     282           61 :     reasoningDelta: llmEventTagged.guards["reasoning-delta"],
+     283           57 :     reasoningEnd: llmEventTagged.guards["reasoning-end"],
+     284           62 :     toolInputStart: llmEventTagged.guards["tool-input-start"],
+     285           62 :     toolInputDelta: llmEventTagged.guards["tool-input-delta"],
+     286           58 :     toolInputEnd: llmEventTagged.guards["tool-input-end"],
+     287           49 :     toolCall: llmEventTagged.guards["tool-call"],
+     288           53 :     toolResult: llmEventTagged.guards["tool-result"],
+     289           51 :     toolError: llmEventTagged.guards["tool-error"],
+     290           53 :     stepFinish: llmEventTagged.guards["step-finish"],
+     291           41 :     finish: llmEventTagged.guards.finish,
+     292           56 :     providerError: llmEventTagged.guards["provider-error"],
+     293            2 :   },
+     294            3 : })
+     295              : export type LLMEvent = Schema.Schema.Type<typeof llmEventTagged>
+     296              : 
+     297           75 : export class PreparedRequest extends Schema.Class<PreparedRequest>("LLM.PreparedRequest")({
+     298           20 :   id: Schema.String,
+     299           17 :   route: RouteID,
+     300           23 :   protocol: ProtocolID,
+     301           21 :   model: ModelSchema,
+     302           23 :   body: Schema.Unknown,
+     303           72 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+     304            5 : }) {}
+     305              : 
+     306              : /**
+     307              :  * A `PreparedRequest` whose `body` is typed as `Body`. Use with the generic
+     308              :  * on `LLMClient.prepare<Body>(...)` when the caller knows which route their
+     309              :  * request will resolve to and wants its native shape statically exposed
+     310              :  * (debug UIs, request previews, plan rendering).
+     311              :  *
+     312              :  * The runtime body is identical — the route still emits `body: unknown` — so
+     313              :  * this is a type-level assertion the caller makes about what they expect to
+     314              :  * find. The prepare runtime does not validate the assertion.
+     315              :  */
+     316              : export type PreparedRequestOf<Body> = Omit<PreparedRequest, "body"> & {
+     317              :   readonly body: Body
+     318              : }
+     319              : 
+     320            0 : const responseText = (events: ReadonlyArray<LLMEvent>) =>
+     321            0 :   events
+     322            0 :     .filter(LLMEvent.is.textDelta)
+     323            0 :     .map((event) => event.text)
+     324            2 :     .join("")
+     325              : 
+     326            0 : const responseReasoning = (events: ReadonlyArray<LLMEvent>) =>
+     327            0 :   events
+     328            0 :     .filter(LLMEvent.is.reasoningDelta)
+     329            0 :     .map((event) => event.text)
+     330            2 :     .join("")
+     331              : 
+     332            0 : const responseUsage = (events: ReadonlyArray<LLMEvent>) =>
+     333            0 :   events.reduce<Usage | undefined>(
+     334            0 :     (usage, event) => ("usage" in event && event.usage !== undefined ? event.usage : usage),
+     335              :     undefined,
+     336            2 :   )
+     337              : 
+     338              : interface ContentAssembly {
+     339              :   readonly contentIndex: number
+     340              :   readonly text: string
+     341              :   readonly providerMetadata?: ProviderMetadata
+     342              : }
+     343              : 
+     344              : interface ToolInputAssembly {
+     345              :   readonly name: string
+     346              :   readonly text: string
+     347              :   readonly providerMetadata?: ProviderMetadata
+     348              : }
+     349              : 
+     350              : interface ResponseState {
+     351              :   readonly events: ReadonlyArray<LLMEvent>
+     352              :   readonly message: Message
+     353              :   readonly usage?: Usage
+     354              :   readonly finishReason?: FinishReason
+     355              :   readonly textParts: Readonly<Record<string, ContentAssembly>>
+     356              :   readonly reasoningParts: Readonly<Record<string, ContentAssembly>>
+     357              :   readonly toolInputs: Readonly<Record<string, ToolInputAssembly>>
+     358              : }
+     359              : 
+     360            0 : const emptyResponseState = (): ResponseState => ({
+     361            0 :   events: [],
+     362            0 :   message: Message.assistant([]),
+     363            0 :   textParts: {},
+     364            0 :   reasoningParts: {},
+     365              :   toolInputs: {},
+     366            2 : })
+     367              : 
+     368            0 : const appendEvent = (state: ResponseState, event: LLMEvent): ResponseState => {
+     369            0 :   const events = [...state.events, event]
+     370            0 :   if (LLMEvent.is.finish(event)) {
+     371            0 :     return {
+     372            0 :       ...state,
+     373            0 :       events,
+     374            0 :       usage: event.usage ?? state.usage,
+     375            0 :       finishReason: event.reason,
+     376            0 :     }
+     377            0 :   }
+     378            0 :   if (LLMEvent.is.providerError(event)) {
+     379            0 :     return {
+     380            0 :       ...state,
+     381            0 :       events,
+     382            0 :       finishReason: state.finishReason ?? "error",
+     383            0 :     }
+     384            0 :   }
+     385            0 :   return {
+     386            0 :     ...state,
+     387            0 :     events,
+     388            0 :     usage: "usage" in event && event.usage !== undefined ? event.usage : state.usage,
+     389            2 :   }
+     390              : }
+     391              : 
+     392            0 : const textContent = (text: string, providerMetadata: ProviderMetadata | undefined): ContentPart =>
+     393            2 :   providerMetadata === undefined ? { type: "text", text } : { type: "text", text, providerMetadata }
+     394              : 
+     395            0 : const reasoningContent = (text: string, providerMetadata: ProviderMetadata | undefined): ContentPart =>
+     396            2 :   providerMetadata === undefined ? { type: "reasoning", text } : { type: "reasoning", text, providerMetadata }
+     397              : 
+     398            0 : const contentWith = (state: ResponseState, content: ReadonlyArray<ContentPart>): ResponseState => ({
+     399            0 :   ...state,
+     400              :   message: Message.assistant(content),
+     401            2 : })
+     402              : 
+     403           22 : const appendContent = (state: ResponseState, part: ContentPart) => contentWith(state, [...state.message.content, part])
+     404              : 
+     405            0 : const replaceContent = (state: ResponseState, index: number, part: ContentPart) =>
+     406            0 :   contentWith(
+     407            0 :     state,
+     408              :     state.message.content.map((item, itemIndex) => (itemIndex === index ? part : item)),
+     409            2 :   )
+     410              : 
+     411            0 : const ensureText = (state: ResponseState, id: string, providerMetadata?: ProviderMetadata): ResponseState => {
+     412            0 :   if (state.textParts[id]) return state
+     413            0 :   return {
+     414            0 :     ...appendContent(state, textContent("", providerMetadata)),
+     415            0 :     textParts: {
+     416            0 :       ...state.textParts,
+     417            0 :       [id]: { contentIndex: state.message.content.length, text: "", providerMetadata },
+     418            0 :     },
+     419            2 :   }
+     420              : }
+     421              : 
+     422            0 : const reduceTextDelta = (state: ResponseState, event: TextDelta): ResponseState => {
+     423            0 :   const started = ensureText(state, event.id, event.providerMetadata)
+     424            0 :   const current = started.textParts[event.id]
+     425            0 :   if (!current) return started
+     426            0 :   const text = current.text + event.text
+     427            0 :   const providerMetadata = event.providerMetadata ?? current.providerMetadata
+     428            0 :   return {
+     429            0 :     ...replaceContent(started, current.contentIndex, textContent(text, providerMetadata)),
+     430            0 :     textParts: { ...started.textParts, [event.id]: { ...current, text, providerMetadata } },
+     431            2 :   }
+     432              : }
+     433              : 
+     434            0 : const reduceTextEnd = (state: ResponseState, event: TextEnd): ResponseState => {
+     435            0 :   const current = state.textParts[event.id]
+     436            0 :   if (!current) return state
+     437            0 :   const providerMetadata = event.providerMetadata ?? current.providerMetadata
+     438            0 :   return {
+     439            0 :     ...replaceContent(state, current.contentIndex, textContent(current.text, providerMetadata)),
+     440            0 :     textParts: { ...state.textParts, [event.id]: { ...current, providerMetadata } },
+     441            2 :   }
+     442              : }
+     443              : 
+     444            0 : const ensureReasoning = (state: ResponseState, id: string, providerMetadata?: ProviderMetadata): ResponseState => {
+     445            0 :   if (state.reasoningParts[id]) return state
+     446            0 :   return {
+     447            0 :     ...appendContent(state, reasoningContent("", providerMetadata)),
+     448            0 :     reasoningParts: {
+     449            0 :       ...state.reasoningParts,
+     450            0 :       [id]: { contentIndex: state.message.content.length, text: "", providerMetadata },
+     451            0 :     },
+     452            2 :   }
+     453              : }
+     454              : 
+     455            0 : const reduceReasoningDelta = (state: ResponseState, event: ReasoningDelta): ResponseState => {
+     456            0 :   const started = ensureReasoning(state, event.id, event.providerMetadata)
+     457            0 :   const current = started.reasoningParts[event.id]
+     458            0 :   if (!current) return started
+     459            0 :   const text = current.text + event.text
+     460            0 :   const providerMetadata = event.providerMetadata ?? current.providerMetadata
+     461            0 :   return {
+     462            0 :     ...replaceContent(started, current.contentIndex, reasoningContent(text, providerMetadata)),
+     463            0 :     reasoningParts: { ...started.reasoningParts, [event.id]: { ...current, text, providerMetadata } },
+     464            2 :   }
+     465              : }
+     466              : 
+     467            0 : const reduceReasoningEnd = (state: ResponseState, event: ReasoningEnd): ResponseState => {
+     468            0 :   const current = state.reasoningParts[event.id]
+     469            0 :   if (!current) return state
+     470            0 :   const providerMetadata = event.providerMetadata ?? current.providerMetadata
+     471            0 :   return {
+     472            0 :     ...replaceContent(state, current.contentIndex, reasoningContent(current.text, providerMetadata)),
+     473            0 :     reasoningParts: { ...state.reasoningParts, [event.id]: { ...current, providerMetadata } },
+     474            2 :   }
+     475              : }
+     476              : 
+     477            0 : const reduceToolInputStart = (state: ResponseState, event: ToolInputStart): ResponseState => ({
+     478            0 :   ...state,
+     479            0 :   toolInputs: {
+     480            0 :     ...state.toolInputs,
+     481            0 :     [event.id]: { name: event.name, text: "", providerMetadata: event.providerMetadata },
+     482              :   },
+     483            2 : })
+     484              : 
+     485            0 : const reduceToolInputDelta = (state: ResponseState, event: ToolInputDelta): ResponseState => {
+     486            0 :   const current = state.toolInputs[event.id] ?? { name: event.name, text: "" }
+     487            0 :   return {
+     488            0 :     ...state,
+     489            0 :     toolInputs: { ...state.toolInputs, [event.id]: { ...current, text: current.text + event.text } },
+     490            2 :   }
+     491              : }
+     492              : 
+     493            0 : const reduceToolInputEnd = (state: ResponseState, event: ToolInputEnd): ResponseState => {
+     494            0 :   const current = state.toolInputs[event.id] ?? { name: event.name, text: "" }
+     495            0 :   return {
+     496            0 :     ...state,
+     497            0 :     toolInputs: {
+     498            0 :       ...state.toolInputs,
+     499            0 :       [event.id]: {
+     500            0 :         ...current,
+     501            0 :         name: event.name,
+     502            0 :         providerMetadata: event.providerMetadata ?? current.providerMetadata,
+     503            0 :       },
+     504            0 :     },
+     505            2 :   }
+     506              : }
+     507              : 
+     508            0 : const toolCallContent = (event: ToolCall): ContentPart =>
+     509            0 :   ToolCallPart.make({
+     510            0 :     id: event.id,
+     511            0 :     name: event.name,
+     512            0 :     input: event.input,
+     513            0 :     ...(event.providerExecuted === undefined ? {} : { providerExecuted: event.providerExecuted }),
+     514              :     ...(event.providerMetadata === undefined ? {} : { providerMetadata: event.providerMetadata }),
+     515            2 :   })
+     516              : 
+     517            0 : const toolResultContent = (event: ToolResult): ContentPart =>
+     518            0 :   ToolResultPart.make({
+     519            0 :     id: event.id,
+     520            0 :     name: event.name,
+     521            0 :     result: event.result,
+     522            0 :     ...(event.providerExecuted === undefined ? {} : { providerExecuted: event.providerExecuted }),
+     523              :     ...(event.providerMetadata === undefined ? {} : { providerMetadata: event.providerMetadata }),
+     524            2 :   })
+     525              : 
+     526            0 : const reduceToolCall = (state: ResponseState, event: ToolCall): ResponseState => {
+     527            0 :   const { [event.id]: _finished, ...toolInputs } = state.toolInputs
+     528            2 :   return { ...appendContent(state, toolCallContent(event)), toolInputs }
+     529              : }
+     530              : 
+     531            0 : const reduceResponseState = (state: ResponseState, event: LLMEvent): ResponseState => {
+     532            0 :   const next = appendEvent(state, event)
+     533            0 :   switch (event.type) {
+     534            0 :     case "text-start":
+     535            0 :       return ensureText(next, event.id, event.providerMetadata)
+     536            0 :     case "text-delta":
+     537            0 :       return reduceTextDelta(next, event)
+     538            0 :     case "text-end":
+     539            0 :       return reduceTextEnd(next, event)
+     540            0 :     case "reasoning-start":
+     541            0 :       return ensureReasoning(next, event.id, event.providerMetadata)
+     542            0 :     case "reasoning-delta":
+     543            0 :       return reduceReasoningDelta(next, event)
+     544            0 :     case "reasoning-end":
+     545            0 :       return reduceReasoningEnd(next, event)
+     546            0 :     case "tool-input-start":
+     547            0 :       return reduceToolInputStart(next, event)
+     548            0 :     case "tool-input-delta":
+     549            0 :       return reduceToolInputDelta(next, event)
+     550            0 :     case "tool-input-end":
+     551            0 :       return reduceToolInputEnd(next, event)
+     552            0 :     case "tool-call":
+     553            0 :       return reduceToolCall(next, event)
+     554            0 :     case "tool-result":
+     555            0 :       return appendContent(next, toolResultContent(event))
+     556            0 :     default:
+     557            2 :       return next
+     558              :   }
+     559              : }
+     560              : 
+     561           64 : export class LLMResponse extends Schema.Class<LLMResponse>("LLM.Response")({
+     562           19 :   message: Message,
+     563           33 :   events: Schema.Array(LLMEvent),
+     564           32 :   usage: Schema.optional(Usage),
+     565           27 :   finishReason: FinishReason,
+     566            0 : }) {
+     567            0 :   /** Concatenated assistant text assembled from streamed `text-delta` events. */
+     568            0 :   get text() {
+     569            0 :     return responseText(this.events)
+     570            0 :   }
+     571            0 : 
+     572            0 :   /** Concatenated reasoning text assembled from streamed `reasoning-delta` events. */
+     573            0 :   get reasoning() {
+     574            0 :     return responseReasoning(this.events)
+     575            0 :   }
+     576            0 : 
+     577            0 :   /** Completed tool calls emitted by the provider. */
+     578            0 :   get toolCalls() {
+     579            1 :     return this.events.filter(LLMEvent.is.toolCall)
+     580              :   }
+     581            1 : }
+     582              : 
+     583           40 : export namespace LLMResponse {
+     584              :   export type State = ResponseState
+     585              :   export type Output = LLMResponse | { readonly events: ReadonlyArray<LLMEvent>; readonly usage?: Usage }
+     586              : 
+     587              :   /** Initial reducer state for assembling one provider attempt. */
+     588           41 :   export const empty = emptyResponseState
+     589              : 
+     590              :   /** Purely fold one provider-neutral event into the attempt assembly state. */
+     591           43 :   export const reduce = reduceResponseState
+     592              : 
+     593              :   /** Return a completed response only after a terminal finish or provider error. */
+     594            0 :   export const complete = (state: State): LLMResponse | undefined =>
+     595            0 :     state.finishReason === undefined
+     596            0 :       ? undefined
+     597            0 :       : new LLMResponse({
+     598            0 :           message: state.message,
+     599            0 :           events: [...state.events],
+     600            0 :           usage: state.usage,
+     601            0 :           finishReason: state.finishReason,
+     602            3 :         })
+     603              : 
+     604              :   /** Convenience reducer for callers that already have a collected event list. */
+     605           27 :   export const fromEvents = (events: ReadonlyArray<LLMEvent>) => complete(events.reduce(reduce, empty()))
+     606              : 
+     607              :   /** Concatenate assistant text from a response or collected event list. */
+     608           21 :   export const text = (response: Output) => responseText(response.events)
+     609              : 
+     610              :   /** Return response usage, falling back to the latest usage-bearing event. */
+     611           22 :   export const usage = (response: Output) => response.usage ?? responseUsage(response.events)
+     612              : 
+     613              :   /** Return completed tool calls from a response or collected event list. */
+     614           26 :   export const toolCalls = (response: Output) => response.events.filter(LLMEvent.is.toolCall)
+     615              : 
+     616              :   /** Concatenate reasoning text from a response or collected event list. */
+     617           27 :   export const reasoning = (response: Output) => responseReasoning(response.events)
+     618              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/schema/ids.ts.gcov.html b/packages/core/llm/src/schema/ids.ts.gcov.html new file mode 100644 index 00000000..0d6fea8b --- /dev/null +++ b/packages/core/llm/src/schema/ids.ts.gcov.html @@ -0,0 +1,119 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/schema/ids.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/schema - ids.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %1616
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Schema } from "effect"
+       2           59 : import { ProviderMetadata } from "@opencode-ai/schema/llm"
+       3              : 
+       4           28 : export { ProviderMetadata }
+       5              : 
+       6              : /** Stable string identifier for a protocol implementation. */
+       7           40 : export const ProtocolID = Schema.String
+       8              : export type ProtocolID = Schema.Schema.Type<typeof ProtocolID>
+       9              : 
+      10              : /** Stable string identifier for the runnable route. */
+      11           37 : export const RouteID = Schema.String
+      12              : export type RouteID = Schema.Schema.Type<typeof RouteID>
+      13              : 
+      14           71 : export const ModelID = Schema.String.pipe(Schema.brand("LLM.ModelID"))
+      15              : export type ModelID = typeof ModelID.Type
+      16              : 
+      17           77 : export const ProviderID = Schema.String.pipe(Schema.brand("LLM.ProviderID"))
+      18              : export type ProviderID = typeof ProviderID.Type
+      19              : 
+      20           40 : export const ResponseID = Schema.String
+      21              : export type ResponseID = Schema.Schema.Type<typeof ResponseID>
+      22              : 
+      23           44 : export const ContentBlockID = Schema.String
+      24              : export type ContentBlockID = Schema.Schema.Type<typeof ContentBlockID>
+      25              : 
+      26           40 : export const ToolCallID = Schema.String
+      27              : export type ToolCallID = Schema.Schema.Type<typeof ToolCallID>
+      28              : 
+      29           93 : export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const
+      30           65 : export const ReasoningEffort = Schema.Literals(ReasoningEfforts)
+      31              : export type ReasoningEffort = Schema.Schema.Type<typeof ReasoningEffort>
+      32              : 
+      33           72 : export const TextVerbosity = Schema.Literals(["low", "medium", "high"])
+      34              : export type TextVerbosity = Schema.Schema.Type<typeof TextVerbosity>
+      35              : 
+      36           84 : export const MessageRole = Schema.Literals(["system", "user", "assistant", "tool"])
+      37              : export type MessageRole = Schema.Schema.Type<typeof MessageRole>
+      38              : 
+      39          116 : export const FinishReason = Schema.Literals(["stop", "length", "tool-calls", "content-filter", "error", "unknown"])
+      40              : export type FinishReason = Schema.Schema.Type<typeof FinishReason>
+      41              : 
+      42           70 : export const JsonSchema = Schema.Record(Schema.String, Schema.Unknown)
+      43              : export type JsonSchema = Schema.Schema.Type<typeof JsonSchema>
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/schema/index-sort-f.html b/packages/core/llm/src/schema/index-sort-f.html new file mode 100644 index 00000000..7d2b8eb6 --- /dev/null +++ b/packages/core/llm/src/schema/index-sort-f.html @@ -0,0 +1,143 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/schema + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/schemaCoverageTotalHit
Test:opencode-lcov.infoLines:75.7 %990749
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
errors.ts +
78.4%78.4%
+
78.4 %167131
events.ts +
57.6%57.6%
+
57.6 %415239
ids.ts +
100.0%
+
100.0 %1616
index.ts +
100.0%
+
100.0 %55
messages.ts +
96.4%96.4%
+
96.4 %223215
options.ts +
87.2%87.2%
+
87.2 %164143
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/schema/index-sort-l.html b/packages/core/llm/src/schema/index-sort-l.html new file mode 100644 index 00000000..0da9090d --- /dev/null +++ b/packages/core/llm/src/schema/index-sort-l.html @@ -0,0 +1,143 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/schema + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/schemaCoverageTotalHit
Test:opencode-lcov.infoLines:75.7 %990749
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
events.ts +
57.6%57.6%
+
57.6 %415239
errors.ts +
78.4%78.4%
+
78.4 %167131
options.ts +
87.2%87.2%
+
87.2 %164143
messages.ts +
96.4%96.4%
+
96.4 %223215
index.ts +
100.0%
+
100.0 %55
ids.ts +
100.0%
+
100.0 %1616
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/schema/index.html b/packages/core/llm/src/schema/index.html new file mode 100644 index 00000000..40a0a8d7 --- /dev/null +++ b/packages/core/llm/src/schema/index.html @@ -0,0 +1,143 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/schema + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/schemaCoverageTotalHit
Test:opencode-lcov.infoLines:75.7 %990749
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
errors.ts +
78.4%78.4%
+
78.4 %167131
events.ts +
57.6%57.6%
+
57.6 %415239
ids.ts +
100.0%
+
100.0 %1616
index.ts +
100.0%
+
100.0 %55
messages.ts +
96.4%96.4%
+
96.4 %223215
options.ts +
87.2%87.2%
+
87.2 %164143
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/schema/index.ts.gcov.html b/packages/core/llm/src/schema/index.ts.gcov.html new file mode 100644 index 00000000..3aa13a32 --- /dev/null +++ b/packages/core/llm/src/schema/index.ts.gcov.html @@ -0,0 +1,81 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/schema/index.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/schema - index.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %55
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           22 : export * from "./ids"
+       2           26 : export * from "./options"
+       3           27 : export * from "./messages"
+       4           25 : export * from "./events"
+       5           24 : export * from "./errors"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/schema/messages.ts.gcov.html b/packages/core/llm/src/schema/messages.ts.gcov.html new file mode 100644 index 00000000..79ff9be4 --- /dev/null +++ b/packages/core/llm/src/schema/messages.ts.gcov.html @@ -0,0 +1,388 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/schema/messages.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/schema - messages.tsCoverageTotalHit
Test:opencode-lcov.infoLines:96.4 %223215
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1            0 : import { Schema } from "effect"
+       2           83 : import { ToolContent, ToolFileContent, ToolTextContent } from "@opencode-ai/schema/llm"
+       3           66 : import { JsonSchema, MessageRole, ProviderMetadata } from "./ids"
+       4          113 : import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options"
+       5           43 : import { isRecord } from "../utils/record"
+       6              : 
+       7           41 : const systemPartSchema = Schema.Struct({
+       8           31 :   type: Schema.Literal("text"),
+       9           22 :   text: Schema.String,
+      10           36 :   cache: Schema.optional(CacheHint),
+      11           72 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+      12           46 : }).annotate({ identifier: "LLM.SystemPart" })
+      13              : export type SystemPart = Schema.Schema.Type<typeof systemPartSchema>
+      14              : 
+      15           56 : const makeSystemPart = (text: string): SystemPart => ({ type: "text", text })
+      16              : 
+      17           60 : export const SystemPart = Object.assign(systemPartSchema, {
+      18           23 :   make: makeSystemPart,
+      19           24 :   content: (input?: string | SystemPart | ReadonlyArray<SystemPart>) => {
+      20           42 :     if (input === undefined) return []
+      21           69 :     return typeof input === "string" ? [makeSystemPart(input)] : Array.isArray(input) ? [...input] : [input]
+      22              :   },
+      23            3 : })
+      24              : 
+      25           40 : export const TextPart = Schema.Struct({
+      26           31 :   type: Schema.Literal("text"),
+      27           22 :   text: Schema.String,
+      28           36 :   cache: Schema.optional(CacheHint),
+      29           74 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+      30           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+      31           48 : }).annotate({ identifier: "LLM.Content.Text" })
+      32              : export type TextPart = Schema.Schema.Type<typeof TextPart>
+      33              : 
+      34           41 : export const MediaPart = Schema.Struct({
+      35           32 :   type: Schema.Literal("media"),
+      36           27 :   mediaType: Schema.String,
+      37           57 :   data: Schema.Union([Schema.String, Schema.Uint8Array]),
+      38           43 :   filename: Schema.optional(Schema.String),
+      39           72 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+      40           49 : }).annotate({ identifier: "LLM.Content.Media" })
+      41              : export type MediaPart = Schema.Schema.Type<typeof MediaPart>
+      42              : 
+      43           57 : export { ToolContent, ToolFileContent, ToolTextContent }
+      44              : 
+      45           35 : const isToolResultValue = (value: unknown): value is ToolResultValue =>
+      46           20 :   isRecord(value) &&
+      47          106 :   (value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
+      48           18 :   "value" in value
+      49              : 
+      50           44 : export const ToolResultValue = Object.assign(
+      51           16 :   Schema.Union([
+      52           19 :     Schema.Struct({
+      53           33 :       type: Schema.Literal("json"),
+      54           23 :       value: Schema.Unknown,
+      55            5 :     }),
+      56           19 :     Schema.Struct({
+      57           33 :       type: Schema.Literal("text"),
+      58           23 :       value: Schema.Unknown,
+      59            5 :     }),
+      60           19 :     Schema.Struct({
+      61           34 :       type: Schema.Literal("error"),
+      62           23 :       value: Schema.Unknown,
+      63            5 :     }),
+      64           19 :     Schema.Struct({
+      65           36 :       type: Schema.Literal("content"),
+      66           34 :       value: Schema.Array(ToolContent),
+      67            3 :     }),
+      68           46 :   ]).annotate({ identifier: "LLM.ToolResult" }),
+      69            3 :   {
+      70           24 :     is: isToolResultValue,
+      71           36 :     make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
+      72           50 :       if (isToolResultValue(value)) return value
+      73           32 :       if (type === "content") return { type, value: Array.isArray(value) ? value : [] }
+      74           23 :       return { type, value }
+      75              :     },
+      76              :   },
+      77            3 : )
+      78              : export type ToolResultValue = Schema.Schema.Type<typeof ToolResultValue>
+      79              : 
+      80              : export interface ToolOutput {
+      81              :   readonly structured: unknown
+      82              :   readonly content: ReadonlyArray<ToolContent>
+      83              : }
+      84              : 
+      85           39 : export const ToolOutput = Object.assign(
+      86           17 :   Schema.Struct({
+      87           29 :     structured: Schema.Unknown,
+      88           35 :     content: Schema.Array(ToolContent),
+      89           46 :   }).annotate({ identifier: "LLM.ToolOutput" }),
+      90            3 :   {
+      91           62 :     make: (structured: unknown, content: ReadonlyArray<ToolContent> = []): ToolOutput => ({ structured, content }),
+      92           33 :     fromResultValue: (result: ToolResultValue): ToolOutput | undefined => {
+      93           32 :       switch (result.type) {
+      94           15 :         case "json":
+      95           48 :           return { structured: result.value, content: [] }
+      96            0 :         case "text":
+      97            5 :           return { structured: {}, content: [{ type: "text", text: toolResultText(result.value) }] }
+      98           18 :         case "content":
+      99           48 :           return { structured: {}, content: result.value }
+     100            0 :         case "error":
+     101            3 :           return undefined
+     102              :       }
+     103              :     },
+     104           31 :     toResultValue: (output: ToolOutput): ToolResultValue => {
+     105           90 :       if (output.content.length === 0) return { type: "json", value: output.structured }
+     106           75 :       if (output.content.length === 1 && output.content[0]?.type === "text")
+     107           58 :         return { type: "text", value: output.content[0].text }
+     108           50 :       return { type: "content", value: output.content }
+     109              :     },
+     110              :   },
+     111            3 : )
+     112              : 
+     113            0 : const toolResultText = (value: unknown) => {
+     114            0 :   if (typeof value === "string") return value
+     115            0 :   try {
+     116            0 :     return JSON.stringify(value) ?? String(value)
+     117            0 :   } catch {
+     118            2 :     return String(value)
+     119              :   }
+     120              : }
+     121              : 
+     122           41 : export const ToolCallPart = Object.assign(
+     123           17 :   Schema.Struct({
+     124           36 :     type: Schema.Literal("tool-call"),
+     125           20 :     id: Schema.String,
+     126           22 :     name: Schema.String,
+     127           24 :     input: Schema.Unknown,
+     128           52 :     providerExecuted: Schema.optional(Schema.Boolean),
+     129           74 :     metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+     130           52 :     providerMetadata: Schema.optional(ProviderMetadata),
+     131           52 :   }).annotate({ identifier: "LLM.Content.ToolCall" }),
+     132            3 :   {
+     133           49 :     make: (input: Omit<ToolCallPart, "type">): ToolCallPart => ({ type: "tool-call", ...input }),
+     134              :   },
+     135            3 : )
+     136              : export type ToolCallPart = Schema.Schema.Type<typeof ToolCallPart>
+     137              : 
+     138           43 : export const ToolResultPart = Object.assign(
+     139           17 :   Schema.Struct({
+     140           38 :     type: Schema.Literal("tool-result"),
+     141           20 :     id: Schema.String,
+     142           22 :     name: Schema.String,
+     143           26 :     result: ToolResultValue,
+     144           52 :     providerExecuted: Schema.optional(Schema.Boolean),
+     145           36 :     cache: Schema.optional(CacheHint),
+     146           74 :     metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+     147           52 :     providerMetadata: Schema.optional(ProviderMetadata),
+     148           54 :   }).annotate({ identifier: "LLM.Content.ToolResult" }),
+     149            3 :   {
+     150            6 :     make: (
+     151           11 :       input: Omit<ToolResultPart, "type" | "result"> & {
+     152              :         readonly result: unknown
+     153              :         readonly resultType?: ToolResultValue["type"]
+     154              :       },
+     155            5 :     ): ToolResultPart => ({
+     156           24 :       type: "tool-result",
+     157           17 :       id: input.id,
+     158           21 :       name: input.name,
+     159           65 :       result: ToolResultValue.make(input.result, input.resultType),
+     160           45 :       providerExecuted: input.providerExecuted,
+     161           23 :       cache: input.cache,
+     162           29 :       metadata: input.metadata,
+     163           42 :       providerMetadata: input.providerMetadata,
+     164            2 :     }),
+     165              :   },
+     166            3 : )
+     167              : export type ToolResultPart = Schema.Schema.Type<typeof ToolResultPart>
+     168              : 
+     169           45 : export const ReasoningPart = Schema.Struct({
+     170           36 :   type: Schema.Literal("reasoning"),
+     171           22 :   text: Schema.String,
+     172           44 :   encrypted: Schema.optional(Schema.String),
+     173           74 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+     174           52 :   providerMetadata: Schema.optional(ProviderMetadata),
+     175           53 : }).annotate({ identifier: "LLM.Content.Reasoning" })
+     176              : export type ReasoningPart = Schema.Schema.Type<typeof ReasoningPart>
+     177              : 
+     178          112 : export const ContentPart = Schema.Union([TextPart, MediaPart, ToolCallPart, ToolResultPart, ReasoningPart]).pipe(
+     179           28 :   Schema.toTaggedUnion("type"),
+     180            3 : )
+     181              : export type ContentPart = Schema.Schema.Type<typeof ContentPart>
+     182              : 
+     183           59 : export class Message extends Schema.Class<Message>("LLM.Message")({
+     184           37 :   id: Schema.optional(Schema.String),
+     185           20 :   role: MessageRole,
+     186           37 :   content: Schema.Array(ContentPart),
+     187           74 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+     188           70 :   native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+     189            5 : }) {}
+     190              : 
+     191           33 : export namespace Message {
+     192              :   export type ContentInput = string | ContentPart | ReadonlyArray<ContentPart>
+     193              :   export type SystemContentInput = string | TextPart | ReadonlyArray<TextPart>
+     194              :   export type Input = Omit<ConstructorParameters<typeof Message>[0], "content"> & {
+     195              :     readonly content: ContentInput
+     196              :   }
+     197              : 
+     198           58 :   export const text = (value: string): ContentPart => ({ type: "text", text: value })
+     199              : 
+     200           28 :   export const content = (input: ContentInput) =>
+     201           85 :     typeof input === "string" ? [text(input)] : Array.isArray(input) ? [...input] : [input]
+     202              : 
+     203           30 :   export const make = (input: Message | Input) => {
+     204           50 :     if (input instanceof Message) return input
+     205           76 :     return new Message({ ...input, content: content(input.content) })
+     206              :   }
+     207              : 
+     208           68 :   export const user = (content: ContentInput) => make({ role: "user", content })
+     209              : 
+     210           78 :   export const assistant = (content: ContentInput) => make({ role: "assistant", content })
+     211              : 
+     212              :   /**
+     213              :    * Add an operator-authored instruction at this chronological point in the
+     214              :    * conversation. This is distinct from the initial `LLMRequest.system`
+     215              :    * prompt. Keep raw retrieved, tool, and web content out of privileged system
+     216              :    * updates; pass that untrusted content through ordinary user/tool channels.
+     217              :    */
+     218           72 :   export const system = (content: SystemContentInput) => make({ role: "system", content })
+     219              : 
+     220           26 :   export const tool = (result: ToolResultPart | Parameters<typeof ToolResultPart.make>[0]) =>
+     221           69 :     make({ role: "tool", content: ["type" in result ? result : ToolResultPart.make(result)] })
+     222              : }
+     223              : 
+     224           73 : export class ToolDefinition extends Schema.Class<ToolDefinition>("LLM.ToolDefinition")({
+     225           22 :   name: Schema.String,
+     226           29 :   description: Schema.String,
+     227           26 :   inputSchema: JsonSchema,
+     228           44 :   outputSchema: Schema.optional(JsonSchema),
+     229           36 :   cache: Schema.optional(CacheHint),
+     230           74 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+     231           70 :   native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+     232            5 : }) {}
+     233              : 
+     234           47 : export namespace ToolDefinition {
+     235              :   export type Input = ToolDefinition | ConstructorParameters<typeof ToolDefinition>[0]
+     236              : 
+     237              :   /** Normalize tool definition input into the canonical `ToolDefinition` class. */
+     238           74 :   export const make = (input: Input) => (input instanceof ToolDefinition ? input : new ToolDefinition(input))
+     239              : }
+     240              : 
+     241           65 : export class ToolChoice extends Schema.Class<ToolChoice>("LLM.ToolChoice")({
+     242           62 :   type: Schema.Literals(["auto", "none", "required", "tool"]),
+     243           37 :   name: Schema.optional(Schema.String),
+     244            5 : }) {}
+     245              : 
+     246           39 : export namespace ToolChoice {
+     247              :   export type Mode = Exclude<ToolChoice["type"], "tool">
+     248              :   export type Input = ToolChoice | ConstructorParameters<typeof ToolChoice>[0] | ToolDefinition | string
+     249              : 
+     250           87 :   const isMode = (value: string): value is Mode => value === "auto" || value === "none" || value === "required"
+     251              : 
+     252              :   /** Select a specific named tool. */
+     253           21 :   export const named = (value: string) => new ToolChoice({ type: "tool", name: value })
+     254              : 
+     255              :   /** Normalize ergonomic tool-choice inputs into the canonical `ToolChoice` class. */
+     256           33 :   export const make = (input: Input) => {
+     257           41 :     if (input instanceof ToolChoice) return input
+     258           45 :     if (input instanceof ToolDefinition) return named(input.name)
+     259           88 :     if (typeof input === "string") return isMode(input) ? new ToolChoice({ type: input }) : named(input)
+     260            4 :     return new ToolChoice(input)
+     261              :   }
+     262              : }
+     263              : 
+     264           45 : export const ResponseFormat = Schema.Union([
+     265           50 :   Schema.Struct({ type: Schema.Literal("text") }),
+     266           70 :   Schema.Struct({ type: Schema.Literal("json"), schema: JsonSchema }),
+     267           70 :   Schema.Struct({ type: Schema.Literal("tool"), tool: ToolDefinition }),
+     268           38 : ]).pipe(Schema.toTaggedUnion("type"))
+     269              : export type ResponseFormat = Schema.Schema.Type<typeof ResponseFormat>
+     270              : 
+     271           62 : export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
+     272           37 :   id: Schema.optional(Schema.String),
+     273           21 :   model: ModelSchema,
+     274           35 :   system: Schema.Array(SystemPart),
+     275           34 :   messages: Schema.Array(Message),
+     276           38 :   tools: Schema.Array(ToolDefinition),
+     277           42 :   toolChoice: Schema.optional(ToolChoice),
+     278           49 :   generation: Schema.optional(GenerationOptions),
+     279           52 :   providerOptions: Schema.optional(ProviderOptions),
+     280           37 :   http: Schema.optional(HttpOptions),
+     281           50 :   responseFormat: Schema.optional(ResponseFormat),
+     282           38 :   cache: Schema.optional(CachePolicy),
+     283           72 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+     284            5 : }) {}
+     285              : 
+     286           38 : export namespace LLMRequest {
+     287              :   export type Input = ConstructorParameters<typeof LLMRequest>[0]
+     288              : 
+     289           37 :   export const input = (request: LLMRequest): Input => ({
+     290           19 :     id: request.id,
+     291           25 :     model: request.model,
+     292           27 :     system: request.system,
+     293           31 :     messages: request.messages,
+     294           25 :     tools: request.tools,
+     295           35 :     toolChoice: request.toolChoice,
+     296           35 :     generation: request.generation,
+     297           45 :     providerOptions: request.providerOptions,
+     298           23 :     http: request.http,
+     299           43 :     responseFormat: request.responseFormat,
+     300           25 :     cache: request.cache,
+     301           28 :     metadata: request.metadata,
+     302            4 :   })
+     303              : 
+     304           44 :   export const update = (request: LLMRequest, patch: Partial<Input>) => {
+     305           45 :     if (Object.keys(patch).length === 0) return request
+     306           32 :     return new LLMRequest({
+     307           35 :       ...input(request),
+     308           12 :       ...patch,
+     309           39 :       model: patch.model ?? request.model,
+     310            6 :     })
+     311              :   }
+     312              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/schema/options.ts.gcov.html b/packages/core/llm/src/schema/options.ts.gcov.html new file mode 100644 index 00000000..dac8bc6a --- /dev/null +++ b/packages/core/llm/src/schema/options.ts.gcov.html @@ -0,0 +1,352 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/schema/options.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/schema - options.tsCoverageTotalHit
Test:opencode-lcov.infoLines:87.2 %164143
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           64 : import { Schema } from "effect"
+       2          112 : import { JsonSchema, ModelID, ProviderID } from "./ids"
+       3              : import type { AnyRoute } from "../route/client"
+       4           86 : import { isRecord } from "../utils/record"
+       5              : 
+       6           69 : export const mergeJsonRecords = (
+       7           20 :   ...items: ReadonlyArray<Record<string, unknown> | undefined>
+       8            6 : ): Record<string, unknown> | undefined => {
+       9          120 :   const defined = items.filter((item): item is Record<string, unknown> => item !== undefined)
+      10           74 :   if (defined.length === 0) return undefined
+      11          228 :   if (defined.length === 1 && Object.values(defined[0]).every((value) => value !== undefined)) return defined[0]
+      12           40 :   const result: Record<string, unknown> = {}
+      13           65 :   for (const item of defined) {
+      14          111 :     for (const [key, value] of Object.entries(item)) {
+      15           92 :       if (value === undefined) continue
+      16          173 :       result[key] = isRecord(result[key]) && isRecord(value) ? mergeJsonRecords(result[key], value) : value
+      17            5 :     }
+      18            5 :   }
+      19          115 :   return Object.keys(result).length === 0 ? undefined : result
+      20              : }
+      21              : 
+      22           59 : const mergeStringRecords = (
+      23           20 :   ...items: ReadonlyArray<Record<string, string> | undefined>
+      24            6 : ): Record<string, string> | undefined => {
+      25          120 :   const defined = items.filter((item): item is Record<string, string> => item !== undefined)
+      26           74 :   if (defined.length === 0) return undefined
+      27           96 :   if (defined.length === 1) return defined[0]
+      28           68 :   const result = Object.fromEntries(
+      29           51 :     defined.flatMap((item) =>
+      30          121 :       Object.entries(item).filter((entry): entry is [string, string] => entry[1] !== undefined),
+      31            2 :     ),
+      32            8 :   )
+      33          115 :   return Object.keys(result).length === 0 ? undefined : result
+      34              : }
+      35              : 
+      36          212 : export const ProviderOptions = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown))
+      37              : export type ProviderOptions = Schema.Schema.Type<typeof ProviderOptions>
+      38              : 
+      39           77 : export const mergeProviderOptions = (
+      40           20 :   ...items: ReadonlyArray<ProviderOptions | undefined>
+      41            6 : ): ProviderOptions | undefined => {
+      42           40 :   const result: Record<string, Record<string, unknown>> = {}
+      43           61 :   for (const item of items) {
+      44           56 :     if (!item) continue
+      45          125 :     for (const [provider, options] of Object.entries(item)) {
+      46          130 :       const merged = mergeJsonRecords(result[provider], options)
+      47           96 :       if (merged) result[provider] = merged
+      48            5 :     }
+      49            5 :   }
+      50          124 :   return Object.keys(result).length === 0 ? undefined : result
+      51              : }
+      52              : 
+      53          134 : export class HttpOptions extends Schema.Class<HttpOptions>("LLM.HttpOptions")({
+      54           72 :   body: Schema.optional(JsonSchema),
+      55          144 :   headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
+      56          136 :   query: Schema.optional(Schema.Record(Schema.String, Schema.String)),
+      57           10 : }) {}
+      58              : 
+      59           82 : export namespace HttpOptions {
+      60              :   export type Input = HttpOptions | ConstructorParameters<typeof HttpOptions>[0]
+      61              : 
+      62              :   /** Normalize HTTP option input into the canonical `HttpOptions` class. */
+      63          182 :   export const make = (input: Input) => (input instanceof HttpOptions ? input : new HttpOptions(input))
+      64              : }
+      65              : 
+      66           95 : export const mergeHttpOptions = (...items: ReadonlyArray<HttpOptions | undefined>): HttpOptions | undefined => {
+      67          134 :   const body = mergeJsonRecords(...items.map((item) => item?.body))
+      68          150 :   const headers = mergeStringRecords(...items.map((item) => item?.headers))
+      69          142 :   const query = mergeStringRecords(...items.map((item) => item?.query))
+      70           88 :   if (!body && !headers && !query) return undefined
+      71          102 :   return new HttpOptions({ body, headers, query })
+      72              : }
+      73              : 
+      74          158 : export class GenerationOptions extends Schema.Class<GenerationOptions>("LLM.GenerationOptions")({
+      75           88 :   maxTokens: Schema.optional(Schema.Number),
+      76           92 :   temperature: Schema.optional(Schema.Number),
+      77           78 :   topP: Schema.optional(Schema.Number),
+      78           78 :   topK: Schema.optional(Schema.Number),
+      79          102 :   frequencyPenalty: Schema.optional(Schema.Number),
+      80          100 :   presencePenalty: Schema.optional(Schema.Number),
+      81           78 :   seed: Schema.optional(Schema.Number),
+      82          102 :   stop: Schema.optional(Schema.Array(Schema.String)),
+      83           10 : }) {}
+      84              : 
+      85          106 : export namespace GenerationOptions {
+      86              :   export type Input = GenerationOptions | ConstructorParameters<typeof GenerationOptions>[0]
+      87              : 
+      88              :   /** Normalize generation option input into the canonical `GenerationOptions` class. */
+      89          228 :   export const make = (input: Input = {}) => (input instanceof GenerationOptions ? input : new GenerationOptions(input))
+      90              : }
+      91              : 
+      92              : export type GenerationOptionsFields = {
+      93              :   readonly maxTokens?: number
+      94              :   readonly temperature?: number
+      95              :   readonly topP?: number
+      96              :   readonly topK?: number
+      97              :   readonly frequencyPenalty?: number
+      98              :   readonly presencePenalty?: number
+      99              :   readonly seed?: number
+     100              :   readonly stop?: ReadonlyArray<string>
+     101              : }
+     102              : 
+     103              : export type GenerationOptionsInput = GenerationOptions | GenerationOptionsFields
+     104              : 
+     105           49 : const latestGeneration = <Key extends keyof GenerationOptionsFields>(
+     106           14 :   items: ReadonlyArray<GenerationOptionsInput | undefined>,
+     107           16 :   key: Key,
+     108          117 : ) => items.findLast((item) => item?.[key] !== undefined)?.[key]
+     109              : 
+     110          107 : export const mergeGenerationOptions = (...items: ReadonlyArray<GenerationOptionsInput | undefined>) => {
+     111           84 :   const result = new GenerationOptions({
+     112          104 :     maxTokens: latestGeneration(items, "maxTokens"),
+     113          112 :     temperature: latestGeneration(items, "temperature"),
+     114           84 :     topP: latestGeneration(items, "topP"),
+     115           84 :     topK: latestGeneration(items, "topK"),
+     116          132 :     frequencyPenalty: latestGeneration(items, "frequencyPenalty"),
+     117          128 :     presencePenalty: latestGeneration(items, "presencePenalty"),
+     118           84 :     seed: latestGeneration(items, "seed"),
+     119           78 :     stop: latestGeneration(items, "stop"),
+     120           10 :   })
+     121          174 :   return Object.values(result).some((value) => value !== undefined) ? result : undefined
+     122              : }
+     123              : 
+     124          134 : export class ModelLimits extends Schema.Class<ModelLimits>("LLM.ModelLimits")({
+     125           84 :   context: Schema.optional(Schema.Number),
+     126           78 :   output: Schema.optional(Schema.Number),
+     127           10 : }) {}
+     128              : 
+     129           82 : export namespace ModelLimits {
+     130              :   export type Input = ModelLimits | ConstructorParameters<typeof ModelLimits>[0]
+     131              : 
+     132              :   /** Normalize model limit input into the canonical `ModelLimits` class. */
+     133           59 :   export const make = (input: Input | undefined) =>
+     134          135 :     input instanceof ModelLimits ? input : new ModelLimits(input ?? {})
+     135              : }
+     136              : 
+     137          142 : export class ModelDefaults extends Schema.Class<ModelDefaults>("LLM.ModelDefaults")({
+     138           78 :   limits: Schema.optional(ModelLimits),
+     139           98 :   generation: Schema.optional(GenerationOptions),
+     140          104 :   providerOptions: Schema.optional(ProviderOptions),
+     141           70 :   http: Schema.optional(HttpOptions),
+     142           10 : }) {}
+     143              : 
+     144           90 : export namespace ModelDefaults {
+     145              :   export type Input =
+     146              :     | ModelDefaults
+     147              :     | {
+     148              :         readonly limits?: ModelLimits.Input
+     149              :         readonly generation?: GenerationOptions.Input
+     150              :         readonly providerOptions?: ProviderOptions
+     151              :         readonly http?: HttpOptions.Input
+     152              :       }
+     153              : 
+     154              :   /** Normalize selected-model request defaults without applying precedence. */
+     155            0 :   export const make = (input: Input) => {
+     156            0 :     if (input instanceof ModelDefaults) return input
+     157            0 :     return new ModelDefaults({
+     158            0 :       limits: input.limits === undefined ? undefined : ModelLimits.make(input.limits),
+     159            0 :       generation: input.generation === undefined ? undefined : GenerationOptions.make(input.generation),
+     160            0 :       providerOptions: input.providerOptions,
+     161            0 :       http: input.http === undefined ? undefined : HttpOptions.make(input.http),
+     162           14 :     })
+     163              :   }
+     164              : }
+     165              : 
+     166          168 : export const ModelToolSchemaCompatibility = Schema.Literals(["gemini", "moonshot"])
+     167              : export type ModelToolSchemaCompatibility = Schema.Schema.Type<typeof ModelToolSchemaCompatibility>
+     168              : 
+     169          162 : export class ModelCompatibility extends Schema.Class<ModelCompatibility>("LLM.ModelCompatibility")({
+     170          116 :   toolSchema: Schema.optional(ModelToolSchemaCompatibility),
+     171           10 : }) {}
+     172              : 
+     173          110 : export namespace ModelCompatibility {
+     174              :   export type Input = ModelCompatibility | ConstructorParameters<typeof ModelCompatibility>[0]
+     175              : 
+     176              :   /** Normalize model/upstream compatibility metadata without projecting requests. */
+     177          145 :   export const make = (input: Input) => (input instanceof ModelCompatibility ? input : new ModelCompatibility(input))
+     178              : }
+     179              : 
+     180            0 : export class Model {
+     181            0 :   readonly id: ModelID
+     182           12 :   readonly provider: ProviderID
+     183           16 :   readonly route: AnyRoute
+     184           22 :   readonly defaults?: ModelDefaults
+     185           31 :   readonly compatibility?: ModelCompatibility
+     186              : 
+     187           37 :   constructor(input: Model.ConstructorInput) {
+     188           46 :     this.id = input.id
+     189           70 :     this.provider = input.provider
+     190           58 :     this.route = input.route
+     191           70 :     this.defaults = input.defaults
+     192          104 :     this.compatibility = input.compatibility
+     193              :   }
+     194              : 
+     195           30 :   static make(input: Model.Input) {
+     196           48 :     return new Model({
+     197           66 :       id: ModelID.make(input.id),
+     198           96 :       provider: ProviderID.make(input.provider),
+     199           50 :       route: input.route,
+     200          149 :       defaults: input.defaults === undefined ? undefined : ModelDefaults.make(input.defaults),
+     201          173 :       compatibility: input.compatibility === undefined ? undefined : ModelCompatibility.make(input.compatibility),
+     202           25 :     })
+     203              :   }
+     204              : 
+     205            0 :   static input(model: Model): Model.ConstructorInput {
+     206            0 :     return {
+     207            0 :       id: model.id,
+     208            0 :       provider: model.provider,
+     209            0 :       route: model.route,
+     210            0 :       defaults: model.defaults,
+     211            0 :       compatibility: model.compatibility,
+     212           22 :     }
+     213              :   }
+     214              : 
+     215            0 :   static update(model: Model, patch: Partial<Model.Input>) {
+     216            0 :     if (Object.keys(patch).length === 0) return model
+     217            0 :     return Model.make({
+     218            0 :       ...Model.input(model),
+     219            0 :       ...patch,
+     220            8 :     })
+     221              :   }
+     222            2 : }
+     223              : 
+     224              : export namespace Model {
+     225              :   export type ConstructorInput = {
+     226              :     readonly id: ModelID
+     227              :     readonly provider: ProviderID
+     228              :     readonly route: AnyRoute
+     229              :     readonly defaults?: ModelDefaults
+     230              :     readonly compatibility?: ModelCompatibility
+     231              :   }
+     232              : 
+     233              :   export type Input = Omit<ConstructorInput, "id" | "provider" | "defaults" | "compatibility"> & {
+     234              :     readonly id: string | ModelID
+     235              :     readonly provider: string | ProviderID
+     236              :     readonly defaults?: ModelDefaults.Input
+     237              :     readonly compatibility?: ModelCompatibility.Input
+     238              :   }
+     239              : }
+     240              : 
+     241              : export type ModelInput = Model.Input
+     242              : 
+     243          206 : export const ModelSchema = Schema.declare((value): value is Model => value instanceof Model, { expected: "LLM.Model" })
+     244              : 
+     245          126 : export class CacheHint extends Schema.Class<CacheHint>("LLM.CacheHint")({
+     246          106 :   type: Schema.Literals(["ephemeral", "persistent"]),
+     247           86 :   ttlSeconds: Schema.optional(Schema.Number),
+     248           10 : }) {}
+     249              : 
+     250              : // Auto-placement policy for prompt caching. The protocol-neutral lowering step
+     251              : // reads this and injects `CacheHint`s at the configured boundaries; the
+     252              : // per-protocol body builders then translate those hints into wire markers as
+     253              : // usual. `"auto"` is the recommended default for agent loops — it places one
+     254              : // breakpoint at the last tool definition, one at the last system part, and one
+     255              : // at the latest user message. The combination of provider invalidation
+     256              : // hierarchy (tools → system → messages) and Anthropic/Bedrock's 20-block
+     257              : // lookback means three trailing breakpoints reliably cover the static prefix.
+     258              : //
+     259              : // Pass `"none"` to opt out entirely (the legacy behavior). Pass the granular
+     260              : // object form to override individual choices.
+     261           98 : export const CachePolicyObject = Schema.Struct({
+     262           82 :   tools: Schema.optional(Schema.Boolean),
+     263           84 :   system: Schema.optional(Schema.Boolean),
+     264           52 :   messages: Schema.optional(
+     265           36 :     Schema.Union([
+     266           84 :       Schema.Literal("latest-user-message"),
+     267           78 :       Schema.Literal("latest-assistant"),
+     268           80 :       Schema.Struct({ tail: Schema.Number }),
+     269            4 :     ]),
+     270            8 :   ),
+     271           86 :   ttlSeconds: Schema.optional(Schema.Number),
+     272            6 : })
+     273              : export type CachePolicyObject = Schema.Schema.Type<typeof CachePolicyObject>
+     274              : 
+     275          217 : export const CachePolicy = Schema.Union([Schema.Literal("auto"), Schema.Literal("none"), CachePolicyObject])
+     276              : export type CachePolicy = Schema.Schema.Type<typeof CachePolicy>
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/tool-runtime.ts.gcov.html b/packages/core/llm/src/tool-runtime.ts.gcov.html new file mode 100644 index 00000000..1a4599d8 --- /dev/null +++ b/packages/core/llm/src/tool-runtime.ts.gcov.html @@ -0,0 +1,154 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/tool-runtime.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src - tool-runtime.tsCoverageTotalHit
Test:opencode-lcov.infoLines:11.5 %526
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Effect } from "effect"
+       2           81 : import {
+       3              :   LLMEvent,
+       4              :   type ToolCallPart,
+       5              :   ToolFailure,
+       6              :   ToolOutput,
+       7              :   ToolResultValue,
+       8              :   type ToolOutput as ToolOutputType,
+       9              :   type ToolResultValue as ToolResultValueType,
+      10              : } from "./schema"
+      11              : import { type AnyTool, type Tools } from "./tool"
+      12              : 
+      13              : export interface ToolSettlement {
+      14              :   readonly result: ToolResultValueType
+      15              :   readonly output?: ToolOutputType
+      16              : }
+      17              : 
+      18              : export interface DispatchResult extends ToolSettlement {
+      19              :   readonly events: ReadonlyArray<LLMEvent>
+      20              : }
+      21              : 
+      22              : /** Execute one canonical tool call without owning provider IO or continuation. */
+      23            0 : export const dispatch = (tools: Tools, call: ToolCallPart): Effect.Effect<DispatchResult> => {
+      24            0 :   const tool = tools[call.name]
+      25            0 :   if (!tool) return Effect.succeed(result(call, { type: "error", value: `Unknown tool: ${call.name}` }))
+      26            0 :   if (!tool.execute)
+      27            0 :     return Effect.succeed(result(call, { type: "error", value: `Tool has no execute handler: ${call.name}` }))
+      28            0 : 
+      29            0 :   return decodeAndExecute(tool, call).pipe(
+      30            0 :     Effect.map((value) => result(call, value)),
+      31            0 :     Effect.catchTag("LLM.ToolFailure", (failure) =>
+      32            0 :       Effect.succeed(result(call, { type: "error", value: failure.message }, failure.error)),
+      33            0 :     ),
+      34            2 :   )
+      35              : }
+      36              : 
+      37            0 : const decodeAndExecute = (tool: AnyTool, call: ToolCallPart): Effect.Effect<ToolSettlement, ToolFailure> =>
+      38            0 :   tool._decode(call.input).pipe(
+      39            0 :     Effect.mapError((error) => new ToolFailure({ message: `Invalid tool input: ${error.message}` })),
+      40            0 :     Effect.flatMap((decoded) =>
+      41            0 :       tool.execute!(decoded, { id: call.id, name: call.name }).pipe(
+      42            0 :         Effect.flatMap((value) =>
+      43            0 :           tool._encode(value).pipe(
+      44            0 :             Effect.mapError(
+      45            0 :               (error) =>
+      46            0 :                 new ToolFailure({
+      47            0 :                   message: `Tool returned an invalid value for its success schema: ${error.message}`,
+      48            0 :                 }),
+      49            0 :             ),
+      50            0 :           ),
+      51            0 :         ),
+      52            0 :         Effect.map((encoded) => {
+      53            0 :           if (tool._legacyResult && ToolResultValue.is(encoded))
+      54            0 :             return { result: encoded, output: ToolOutput.fromResultValue(encoded) }
+      55            0 :           const output = tool._project(decoded, call.id, encoded)
+      56            0 :           const result = ToolOutput.toResultValue(output)
+      57            0 :           return result.type === "error" ? { result } : { result, output }
+      58            0 :         }),
+      59            0 :       ),
+      60              :     ),
+      61            2 :   )
+      62              : 
+      63            0 : const result = (call: ToolCallPart, value: ToolResultValueType | ToolSettlement, error?: unknown): DispatchResult => {
+      64            0 :   const settlement = ToolResultValue.is(value) ? { result: value } : value
+      65            0 :   return {
+      66            0 :     result: settlement.result,
+      67            0 :     output: settlement.output,
+      68            0 :     events:
+      69            0 :       settlement.result.type === "error"
+      70            0 :         ? [
+      71            0 :             LLMEvent.toolError({ id: call.id, name: call.name, message: String(settlement.result.value), error }),
+      72            0 :             LLMEvent.toolResult({ id: call.id, name: call.name, result: settlement.result }),
+      73            0 :           ]
+      74            0 :         : [LLMEvent.toolResult({ id: call.id, name: call.name, result: settlement.result, output: settlement.output })],
+      75            2 :   }
+      76              : }
+      77              : 
+      78           39 : export const ToolRuntime = { dispatch } as const
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/tool.ts.gcov.html b/packages/core/llm/src/tool.ts.gcov.html new file mode 100644 index 00000000..2ce2fd37 --- /dev/null +++ b/packages/core/llm/src/tool.ts.gcov.html @@ -0,0 +1,329 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/tool.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src - tool.tsCoverageTotalHit
Test:opencode-lcov.infoLines:11.6 %698
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           40 : import { Effect, JsonSchema, Schema } from "effect"
+       2              : import type {
+       3              :   ToolCallPart,
+       4              :   ToolContent,
+       5              :   ToolDefinition as ToolDefinitionClass,
+       6              :   ToolOutput as ToolOutputType,
+       7              : } from "./schema"
+       8           67 : import { ToolDefinition, ToolFailure, ToolOutput } from "./schema"
+       9              : 
+      10              : /**
+      11              :  * Schema constraint for tool parameters / success values: no decoding or
+      12              :  * encoding services are allowed. Tools should be self-contained — anything
+      13              :  * beyond pure data conversion belongs in the handler closure.
+      14              :  */
+      15              : export type ToolSchema<T> = Schema.Codec<T, any, never, never>
+      16              : export interface ToolExecuteContext {
+      17              :   readonly id: ToolCallPart["id"]
+      18              :   readonly name: ToolCallPart["name"]
+      19              : }
+      20              : 
+      21              : export type ToolExecute<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = (
+      22              :   params: Schema.Schema.Type<Parameters>,
+      23              :   context?: ToolExecuteContext,
+      24              : ) => Effect.Effect<Schema.Schema.Type<Success>, ToolFailure>
+      25              : 
+      26              : export interface ToolModelOutputInput<Parameters, Output> {
+      27              :   readonly callID: ToolCallPart["id"]
+      28              :   readonly parameters: Parameters
+      29              :   readonly output: Output
+      30              : }
+      31              : 
+      32              : export type ToolToModelOutput<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = (
+      33              :   input: ToolModelOutputInput<Schema.Schema.Type<Parameters>, Success["Encoded"]>,
+      34              : ) => ReadonlyArray<ToolContent>
+      35              : 
+      36              : /**
+      37              :  * A type-safe LLM tool. Each tool bundles its own description, parameter
+      38              :  * Schema and success Schema. The execute handler is optional: omit it when you
+      39              :  * only want to expose a tool schema to the model and handle tool calls outside
+      40              :  * this package.
+      41              :  *
+      42              :  * Errors must be expressed as `ToolFailure`. Unmapped errors and defects fail
+      43              :  * the stream.
+      44              :  *
+      45              :  * Internally each tool also carries memoized codecs and a precomputed
+      46              :  * `ToolDefinition` so callers do not rebuild them per invocation.
+      47              :  */
+      48              : export interface Tool<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> {
+      49              :   readonly description: string
+      50              :   readonly parameters: Parameters
+      51              :   readonly success: Success
+      52              :   readonly execute?: ToolExecute<Parameters, Success>
+      53              :   readonly toModelOutput?: ToolToModelOutput<Parameters, Success>
+      54              :   readonly toStructuredOutput?: (output: Success["Encoded"]) => unknown
+      55              :   /** @internal */
+      56              :   readonly _decode: (input: unknown) => Effect.Effect<Schema.Schema.Type<Parameters>, Schema.SchemaError>
+      57              :   /** @internal */
+      58              :   readonly _encode: (value: Schema.Schema.Type<Success>) => Effect.Effect<unknown, Schema.SchemaError>
+      59              :   /** @internal */
+      60              :   readonly _project: (
+      61              :     parameters: Schema.Schema.Type<Parameters>,
+      62              :     callID: ToolCallPart["id"],
+      63              :     output: unknown,
+      64              :   ) => ToolOutputType
+      65              :   /** @internal */
+      66              :   readonly _legacyResult: boolean
+      67              :   /** @internal */
+      68              :   readonly _definition: ToolDefinitionClass
+      69              : }
+      70              : 
+      71              : export type AnyTool = Tool<any, any>
+      72              : 
+      73              : export type ExecutableTool<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = Tool<
+      74              :   Parameters,
+      75              :   Success
+      76              : > & {
+      77              :   readonly execute: ToolExecute<Parameters, Success>
+      78              : }
+      79              : 
+      80              : export type AnyExecutableTool = ExecutableTool<any, any>
+      81              : 
+      82              : export type ExecutableTools = Record<string, AnyExecutableTool>
+      83              : 
+      84              : type TypedToolConfig = {
+      85              :   readonly description: string
+      86              :   readonly parameters: ToolSchema<any>
+      87              :   readonly success: ToolSchema<any>
+      88              :   readonly execute?: ToolExecute<ToolSchema<any>, ToolSchema<any>>
+      89              :   readonly toModelOutput?: ToolToModelOutput<ToolSchema<any>, ToolSchema<any>>
+      90              :   readonly toStructuredOutput?: (output: unknown) => unknown
+      91              : }
+      92              : 
+      93              : type DynamicToolConfig = {
+      94              :   readonly description: string
+      95              :   readonly jsonSchema: JsonSchema.JsonSchema
+      96              :   readonly outputSchema?: JsonSchema.JsonSchema
+      97              :   readonly execute?: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure>
+      98              :   readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent>
+      99              :   readonly toStructuredOutput?: (output: unknown) => unknown
+     100              : }
+     101              : 
+     102              : /**
+     103              :  * Constructs a tool. Two input modes:
+     104              :  *
+     105              :  * 1. **Typed** — pass Effect `parameters` and `success` Schemas; inputs and
+     106              :  *    outputs are statically typed and decoded/encoded automatically.
+     107              :  *
+     108              :  *    ```ts
+     109              :  *    Tool.make({
+     110              :  *      description: "Get current weather",
+     111              :  *      parameters: Schema.Struct({ city: Schema.String }),
+     112              :  *      success: Schema.Struct({ temperature: Schema.Number }),
+     113              :  *      execute: ({ city }) => Effect.succeed({ temperature: 22 }),
+     114              :  *    })
+     115              :  *    ```
+     116              :  *
+     117              :  * 2. **Dynamic** — pass raw JSON Schema as `jsonSchema`. Use this when the
+     118              :  *    schema comes from an external source (MCP server, plugin manifest,
+     119              :  *    dynamic config) and is not known at compile time. Inputs are typed as
+     120              :  *    `unknown`; the handler is responsible for any validation it needs.
+     121              :  *
+     122              :  *    ```ts
+     123              :  *    Tool.make({
+     124              :  *      description: "Look something up",
+     125              :  *      jsonSchema: { type: "object", properties: { ... } },
+     126              :  *      execute: (params) => Effect.succeed(...),
+     127              :  *    })
+     128              :  *    ```
+     129              :  *
+     130              :  * In both modes the produced tool flows through `toDefinitions(...)`
+     131              :  * identically.
+     132              :  */
+     133              : export function make<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>>(config: {
+     134              :   readonly description: string
+     135              :   readonly parameters: Parameters
+     136              :   readonly success: Success
+     137              :   readonly execute: ToolExecute<Parameters, Success>
+     138              :   readonly toModelOutput?: ToolToModelOutput<Parameters, Success>
+     139              :   readonly toStructuredOutput?: (output: Success["Encoded"]) => unknown
+     140              : }): ExecutableTool<Parameters, Success>
+     141              : export function make<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>>(config: {
+     142              :   readonly description: string
+     143              :   readonly parameters: Parameters
+     144              :   readonly success: Success
+     145              :   readonly execute?: undefined
+     146              :   readonly toModelOutput?: ToolToModelOutput<Parameters, Success>
+     147              :   readonly toStructuredOutput?: (output: Success["Encoded"]) => unknown
+     148              : }): Tool<Parameters, Success>
+     149              : export function make(config: {
+     150              :   readonly description: string
+     151              :   readonly jsonSchema: JsonSchema.JsonSchema
+     152              :   readonly outputSchema?: JsonSchema.JsonSchema
+     153              :   readonly execute: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure>
+     154              :   readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent>
+     155              :   readonly toStructuredOutput?: (output: unknown) => unknown
+     156              : }): AnyExecutableTool
+     157              : export function make(config: {
+     158              :   readonly description: string
+     159              :   readonly jsonSchema: JsonSchema.JsonSchema
+     160              :   readonly outputSchema?: JsonSchema.JsonSchema
+     161              :   readonly execute?: undefined
+     162              :   readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent>
+     163              :   readonly toStructuredOutput?: (output: unknown) => unknown
+     164              : }): AnyTool
+     165            0 : export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool {
+     166            0 :   if ("jsonSchema" in config) {
+     167            0 :     return {
+     168            0 :       description: config.description,
+     169            0 :       parameters: Schema.Unknown as ToolSchema<unknown>,
+     170            0 :       success: Schema.Unknown as ToolSchema<unknown>,
+     171            0 :       execute: config.execute,
+     172            0 :       toModelOutput: config.toModelOutput,
+     173            0 :       toStructuredOutput: config.toStructuredOutput,
+     174            0 :       _decode: Effect.succeed,
+     175            0 :       _encode: Effect.succeed,
+     176            0 :       _project: (parameters, callID, output) =>
+     177            0 :         project(config.toModelOutput, config.toStructuredOutput, parameters, callID, output),
+     178            0 :       _legacyResult: config.toModelOutput === undefined && config.toStructuredOutput === undefined,
+     179            0 :       _definition: new ToolDefinition({
+     180            0 :         name: "",
+     181            0 :         description: config.description,
+     182            0 :         inputSchema: config.jsonSchema,
+     183            0 :         outputSchema: config.outputSchema,
+     184            0 :       }),
+     185            0 :     }
+     186            0 :   }
+     187            0 :   return {
+     188            0 :     description: config.description,
+     189            0 :     parameters: config.parameters,
+     190            0 :     success: config.success,
+     191            0 :     execute: config.execute,
+     192            0 :     toModelOutput: config.toModelOutput,
+     193            0 :     toStructuredOutput: config.toStructuredOutput,
+     194            0 :     _decode: Schema.decodeUnknownEffect(config.parameters),
+     195            0 :     _encode: Schema.encodeEffect(config.success),
+     196            0 :     _project: (parameters, callID, output) =>
+     197            0 :       project(config.toModelOutput, config.toStructuredOutput, parameters, callID, output),
+     198            0 :     _legacyResult: false,
+     199            0 :     _definition: new ToolDefinition({
+     200            0 :       name: "",
+     201            0 :       description: config.description,
+     202            0 :       inputSchema: toJsonSchema(config.parameters),
+     203            0 :       outputSchema: toJsonSchema(config.success),
+     204            0 :     }),
+     205            1 :   }
+     206              : }
+     207              : 
+     208              : /**
+     209              :  * A record of named tools. The record key becomes the tool name on the wire.
+     210              :  */
+     211              : export type Tools = Record<string, AnyTool>
+     212              : 
+     213              : /**
+     214              :  * Convert a tools record into the `ToolDefinition[]` shape that
+     215              :  * `LLMRequest.tools` expects.
+     216              :  *
+     217              :  * Tool names come from the record keys, so the per-tool cached
+     218              :  * `_definition` is rebuilt with the correct name here. The JSON Schema body
+     219              :  * is reused.
+     220              :  */
+     221            0 : export const toDefinitions = (tools: Tools): ReadonlyArray<ToolDefinitionClass> =>
+     222            0 :   Object.entries(tools).map(
+     223            0 :     ([name, item]) =>
+     224            0 :       new ToolDefinition({
+     225            0 :         name,
+     226            0 :         description: item._definition.description,
+     227            0 :         inputSchema: item._definition.inputSchema,
+     228            0 :         outputSchema: item._definition.outputSchema,
+     229              :       }),
+     230            2 :   )
+     231              : 
+     232            0 : const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => {
+     233            0 :   const document = Schema.toJsonSchemaDocument(schema)
+     234            0 :   if (Object.keys(document.definitions).length === 0) return document.schema
+     235            2 :   return { ...document.schema, $defs: document.definitions }
+     236              : }
+     237              : 
+     238            0 : const project = (
+     239            0 :   toModelOutput: ((input: ToolModelOutputInput<any, any>) => ReadonlyArray<ToolContent>) | undefined,
+     240            0 :   toStructuredOutput: ((output: unknown) => unknown) | undefined,
+     241            0 :   parameters: unknown,
+     242            0 :   callID: ToolCallPart["id"],
+     243            0 :   output: unknown,
+     244            0 : ): ToolOutputType =>
+     245            0 :   ToolOutput.make(
+     246            0 :     toStructuredOutput?.(output) ?? output,
+     247            0 :     toModelOutput?.({ callID, parameters, output }) ??
+     248              :       (typeof output === "string" ? [{ type: "text", text: output }] : []),
+     249            2 :   )
+     250              : 
+     251           23 : export { ToolFailure }
+     252              : 
+     253           30 : export * as Tool from "./tool"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/utils/index.html b/packages/core/llm/src/utils/index.html new file mode 100644 index 00000000..4ed6e148 --- /dev/null +++ b/packages/core/llm/src/utils/index.html @@ -0,0 +1,98 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/utils + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/utilsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %22
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
record.ts +
100.0%
+
100.0 %22
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/llm/src/utils/record.ts.gcov.html b/packages/core/llm/src/utils/record.ts.gcov.html new file mode 100644 index 00000000..f98ceef4 --- /dev/null +++ b/packages/core/llm/src/utils/record.ts.gcov.html @@ -0,0 +1,79 @@ + + + + + + + LCOV - opencode-lcov.info - ../llm/src/utils/record.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../llm/src/utils - record.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %22
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : /** Plain-record narrowing. Excludes arrays so JSON object checks don't accept tuples as key/value bags. */
+       2           33 : export const isRecord = (value: unknown): value is Record<string, unknown> =>
+       3           68 :   typeof value === "object" && value !== null && !Array.isArray(value)
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/plugin/src/v2/effect/index-sort-f.html b/packages/core/plugin/src/v2/effect/index-sort-f.html new file mode 100644 index 00000000..6ba5eb5c --- /dev/null +++ b/packages/core/plugin/src/v2/effect/index-sort-f.html @@ -0,0 +1,107 @@ + + + + + + + LCOV - opencode-lcov.info - ../plugin/src/v2/effect + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../plugin/src/v2/effectCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %33
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
index.ts +
100.0%
+
100.0 %11
plugin.ts +
100.0%
+
100.0 %22
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/plugin/src/v2/effect/index-sort-l.html b/packages/core/plugin/src/v2/effect/index-sort-l.html new file mode 100644 index 00000000..f28801d4 --- /dev/null +++ b/packages/core/plugin/src/v2/effect/index-sort-l.html @@ -0,0 +1,107 @@ + + + + + + + LCOV - opencode-lcov.info - ../plugin/src/v2/effect + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../plugin/src/v2/effectCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %33
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
index.ts +
100.0%
+
100.0 %11
plugin.ts +
100.0%
+
100.0 %22
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/plugin/src/v2/effect/index.html b/packages/core/plugin/src/v2/effect/index.html new file mode 100644 index 00000000..1479ae05 --- /dev/null +++ b/packages/core/plugin/src/v2/effect/index.html @@ -0,0 +1,107 @@ + + + + + + + LCOV - opencode-lcov.info - ../plugin/src/v2/effect + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../plugin/src/v2/effectCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %33
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
index.ts +
100.0%
+
100.0 %11
plugin.ts +
100.0%
+
100.0 %22
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/plugin/src/v2/effect/index.ts.gcov.html b/packages/core/plugin/src/v2/effect/index.ts.gcov.html new file mode 100644 index 00000000..62080678 --- /dev/null +++ b/packages/core/plugin/src/v2/effect/index.ts.gcov.html @@ -0,0 +1,79 @@ + + + + + + + LCOV - opencode-lcov.info - ../plugin/src/v2/effect/index.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../plugin/src/v2/effect - index.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %11
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : export type { PluginContext } from "./context.js"
+       2           36 : export { define } from "./plugin.js"
+       3              : export type { Plugin } from "./plugin.js"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/plugin/src/v2/effect/plugin.ts.gcov.html b/packages/core/plugin/src/v2/effect/plugin.ts.gcov.html new file mode 100644 index 00000000..b8fc9500 --- /dev/null +++ b/packages/core/plugin/src/v2/effect/plugin.ts.gcov.html @@ -0,0 +1,92 @@ + + + + + + + LCOV - opencode-lcov.info - ../plugin/src/v2/effect/plugin.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../plugin/src/v2/effect - plugin.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %22
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import type { Effect, Scope } from "effect"
+       2              : import type { PluginContext } from "./context.js"
+       3              : 
+       4              : export interface Plugin<R = Scope.Scope> {
+       5              :   readonly id: string
+       6              :   readonly effect: (context: PluginContext) => Effect.Effect<void, never, R>
+       7              : }
+       8              : 
+       9           17 : export function define<R = Scope.Scope>(plugin: Plugin<R>) {
+      10           13 :   return plugin
+      11              : }
+      12              : 
+      13              : export interface PluginDomain {
+      14              :   readonly add: (plugin: Plugin) => Effect.Effect<void>
+      15              :   readonly remove: (id: string) => Effect.Effect<void>
+      16              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/plugin/src/v2/promise/index-sort-f.html b/packages/core/plugin/src/v2/promise/index-sort-f.html new file mode 100644 index 00000000..b86eb377 --- /dev/null +++ b/packages/core/plugin/src/v2/promise/index-sort-f.html @@ -0,0 +1,107 @@ + + + + + + + LCOV - opencode-lcov.info - ../plugin/src/v2/promise + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../plugin/src/v2/promiseCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %33
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
index.ts +
100.0%
+
100.0 %11
plugin.ts +
100.0%
+
100.0 %22
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/plugin/src/v2/promise/index-sort-l.html b/packages/core/plugin/src/v2/promise/index-sort-l.html new file mode 100644 index 00000000..cc634768 --- /dev/null +++ b/packages/core/plugin/src/v2/promise/index-sort-l.html @@ -0,0 +1,107 @@ + + + + + + + LCOV - opencode-lcov.info - ../plugin/src/v2/promise + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../plugin/src/v2/promiseCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %33
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
index.ts +
100.0%
+
100.0 %11
plugin.ts +
100.0%
+
100.0 %22
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/plugin/src/v2/promise/index.html b/packages/core/plugin/src/v2/promise/index.html new file mode 100644 index 00000000..4e8db455 --- /dev/null +++ b/packages/core/plugin/src/v2/promise/index.html @@ -0,0 +1,107 @@ + + + + + + + LCOV - opencode-lcov.info - ../plugin/src/v2/promise + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../plugin/src/v2/promiseCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %33
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
index.ts +
100.0%
+
100.0 %11
plugin.ts +
100.0%
+
100.0 %22
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/plugin/src/v2/promise/index.ts.gcov.html b/packages/core/plugin/src/v2/promise/index.ts.gcov.html new file mode 100644 index 00000000..99464d54 --- /dev/null +++ b/packages/core/plugin/src/v2/promise/index.ts.gcov.html @@ -0,0 +1,88 @@ + + + + + + + LCOV - opencode-lcov.info - ../plugin/src/v2/promise/index.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../plugin/src/v2/promise - index.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %11
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : export type { PluginContext } from "./context.js"
+       2              : export type { PluginOptions } from "../options.js"
+       3           36 : export { define } from "./plugin.js"
+       4              : export type { Plugin, PluginDomain } from "./plugin.js"
+       5              : export type { Registration, Reload } from "./registration.js"
+       6              : export type { AgentDraft, AgentHooks } from "./agent.js"
+       7              : export type { AISDKHooks } from "./aisdk.js"
+       8              : export type { CatalogDraft, CatalogHooks, CatalogProviderRecord } from "./catalog.js"
+       9              : export type { CommandDraft, CommandHooks } from "./command.js"
+      10              : export type { IntegrationDraft, IntegrationHooks, IntegrationMethodRegistration } from "./integration.js"
+      11              : export type { ReferenceDraft, ReferenceHooks } from "./reference.js"
+      12              : export type { SkillDraft, SkillHooks } from "./skill.js"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/plugin/src/v2/promise/plugin.ts.gcov.html b/packages/core/plugin/src/v2/promise/plugin.ts.gcov.html new file mode 100644 index 00000000..46ce79fd --- /dev/null +++ b/packages/core/plugin/src/v2/promise/plugin.ts.gcov.html @@ -0,0 +1,91 @@ + + + + + + + LCOV - opencode-lcov.info - ../plugin/src/v2/promise/plugin.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../plugin/src/v2/promise - plugin.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %22
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : import type { PluginContext } from "./context.js"
+       2              : 
+       3              : export interface Plugin {
+       4              :   readonly id: string
+       5              :   readonly setup: (context: PluginContext) => Promise<void> | void
+       6              : }
+       7              : 
+       8           17 : export function define(plugin: Plugin) {
+       9           13 :   return plugin
+      10              : }
+      11              : 
+      12              : export interface PluginDomain {
+      13              :   readonly add: (plugin: Plugin) => Promise<void>
+      14              :   readonly remove: (id: string) => Promise<void>
+      15              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/agent.ts.gcov.html b/packages/core/schema/src/agent.ts.gcov.html new file mode 100644 index 00000000..5af208a0 --- /dev/null +++ b/packages/core/schema/src/agent.ts.gcov.html @@ -0,0 +1,114 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/agent.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - agent.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %3131
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           33 : export * as Agent from "./agent"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           32 : import { Model } from "./model"
+       6           42 : import { Permission } from "./permission"
+       7           38 : import { Provider } from "./provider"
+       8           48 : import { PositiveInt, statics } from "./schema"
+       9              : 
+      10           65 : export const ID = Schema.String.pipe(Schema.brand("AgentV2.ID"))
+      11              : export type ID = typeof ID.Type
+      12              : 
+      13           36 : export const Color = Schema.Union([
+      14           61 :   Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
+      15           91 :   Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
+      16           43 : ]).annotate({ identifier: "Agent.Color" })
+      17              : export type Color = typeof Color.Type
+      18              : 
+      19              : export interface Info extends Schema.Schema.Type<typeof Info> {}
+      20           36 : export const Info = Schema.Struct({
+      21            9 :   id: ID,
+      22           34 :   model: Model.Ref.pipe(optional),
+      23           28 :   request: Provider.Request,
+      24           39 :   system: Schema.String.pipe(optional),
+      25           44 :   description: Schema.String.pipe(optional),
+      26           56 :   mode: Schema.Literals(["subagent", "primary", "all"]),
+      27           25 :   hidden: Schema.Boolean,
+      28           30 :   color: Color.pipe(optional),
+      29           36 :   steps: PositiveInt.pipe(optional),
+      30           32 :   permissions: Permission.Ruleset,
+      31            2 : })
+      32           41 :   .annotate({ identifier: "AgentV2.Info" })
+      33            5 :   .pipe(
+      34           23 :     statics((schema) => ({
+      35           14 :       empty: (id: ID) =>
+      36          100 :         schema.make({ id, request: { headers: {}, body: {} }, mode: "all", hidden: false, permissions: [] }),
+      37            1 :     })),
+      38            2 :   )
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/catalog.ts.gcov.html b/packages/core/schema/src/catalog.ts.gcov.html new file mode 100644 index 00000000..78f98d6c --- /dev/null +++ b/packages/core/schema/src/catalog.ts.gcov.html @@ -0,0 +1,82 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/catalog.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - catalog.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %44
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           37 : export * as Catalog from "./catalog"
+       2              : 
+       3           44 : import { define, inventory } from "./event"
+       4              : 
+       5           64 : const Updated = define({ type: "catalog.updated", schema: {} })
+       6           65 : export const Event = { Updated, Definitions: inventory(Updated) }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/command.ts.gcov.html b/packages/core/schema/src/command.ts.gcov.html new file mode 100644 index 00000000..4c923b5c --- /dev/null +++ b/packages/core/schema/src/command.ts.gcov.html @@ -0,0 +1,91 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/command.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - command.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %1212
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           37 : export * as Command from "./command"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           32 : import { Model } from "./model"
+       6              : 
+       7              : export interface Info extends Schema.Schema.Type<typeof Info> {}
+       8           36 : export const Info = Schema.Struct({
+       9           22 :   name: Schema.String,
+      10           26 :   template: Schema.String,
+      11           44 :   description: Schema.String.pipe(optional),
+      12           38 :   agent: Schema.String.pipe(optional),
+      13           34 :   model: Model.Ref.pipe(optional),
+      14           39 :   subtask: Schema.Boolean.pipe(optional),
+      15           45 : }).annotate({ identifier: "CommandV2.Info" })
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/connection.ts.gcov.html b/packages/core/schema/src/connection.ts.gcov.html new file mode 100644 index 00000000..224524a7 --- /dev/null +++ b/packages/core/schema/src/connection.ts.gcov.html @@ -0,0 +1,98 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/connection.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - connection.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %1515
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           43 : export * as Connection from "./connection"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           42 : import { Credential } from "./credential"
+       5              : 
+       6              : export interface CredentialInfo extends Schema.Schema.Type<typeof CredentialInfo> {}
+       7           46 : export const CredentialInfo = Schema.Struct({
+       8           37 :   type: Schema.Literal("credential"),
+       9           20 :   id: Credential.ID,
+      10           21 :   label: Schema.String,
+      11           57 : }).annotate({ identifier: "Connection.CredentialInfo" })
+      12              : 
+      13              : export interface EnvInfo extends Schema.Schema.Type<typeof EnvInfo> {}
+      14           39 : export const EnvInfo = Schema.Struct({
+      15           30 :   type: Schema.Literal("env"),
+      16           20 :   name: Schema.String,
+      17           50 : }).annotate({ identifier: "Connection.EnvInfo" })
+      18              : 
+      19           59 : export const Info = Schema.Union([CredentialInfo, EnvInfo])
+      20           35 :   .pipe(Schema.toTaggedUnion("type"))
+      21           44 :   .annotate({ identifier: "Connection.Info" })
+      22              : export type Info = typeof Info.Type
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/credential.ts.gcov.html b/packages/core/schema/src/credential.ts.gcov.html new file mode 100644 index 00000000..ad4e5e68 --- /dev/null +++ b/packages/core/schema/src/credential.ts.gcov.html @@ -0,0 +1,111 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/credential.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - credential.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %2626
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           43 : export * as Credential from "./credential"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           55 : import { IntegrationMethodID } from "./integration-id"
+       6           41 : import { ascending } from "./identifier"
+       7           51 : import { NonNegativeInt, statics } from "./schema"
+       8              : 
+       9           36 : export const ID = Schema.String.pipe(
+      10           31 :   Schema.brand("Credential.ID"),
+      11           71 :   statics((schema) => ({ create: () => schema.make("cred_" + ascending()) })),
+      12            3 : )
+      13              : export type ID = typeof ID.Type
+      14              : 
+      15              : export interface OAuth extends Schema.Schema.Type<typeof OAuth> {}
+      16           37 : export const OAuth = Schema.Struct({
+      17           32 :   type: Schema.Literal("oauth"),
+      18           32 :   methodID: IntegrationMethodID,
+      19           25 :   refresh: Schema.String,
+      20           24 :   access: Schema.String,
+      21           26 :   expires: NonNegativeInt,
+      22           65 :   metadata: optional(Schema.Record(Schema.String, Schema.Unknown)),
+      23           48 : }).annotate({ identifier: "Credential.OAuth" })
+      24              : 
+      25              : export interface Key extends Schema.Schema.Type<typeof Key> {}
+      26           35 : export const Key = Schema.Struct({
+      27           30 :   type: Schema.Literal("key"),
+      28           21 :   key: Schema.String,
+      29           65 :   metadata: optional(Schema.Record(Schema.String, Schema.Unknown)),
+      30           46 : }).annotate({ identifier: "Credential.Key" })
+      31              : 
+      32           47 : export const Value = Schema.Union([OAuth, Key])
+      33           35 :   .pipe(Schema.toTaggedUnion("type"))
+      34           45 :   .annotate({ identifier: "Credential.Value" })
+      35              : export type Value = Schema.Schema.Type<typeof Value>
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/durable-event-manifest.ts.gcov.html b/packages/core/schema/src/durable-event-manifest.ts.gcov.html new file mode 100644 index 00000000..8671be02 --- /dev/null +++ b/packages/core/schema/src/durable-event-manifest.ts.gcov.html @@ -0,0 +1,91 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/durable-event-manifest.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - durable-event-manifest.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %1212
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           65 : export * as DurableEventManifest from "./durable-event-manifest"
+       2              : 
+       3           32 : import { Event } from "./event"
+       4           47 : import { SessionEvent } from "./session-event"
+       5           41 : import { SessionV1 } from "./session-v1"
+       6              : 
+       7           32 : export const SessionDurable = {
+       8           62 :   definitions: Event.durable(SessionEvent.DurableDefinitions),
+       9           29 :   schema: SessionEvent.Durable,
+      10            2 : } as const
+      11              : 
+      12           39 : export const Durable = Event.durable([
+      13           88 :   ...SessionV1.Event.Definitions.filter((definition) => definition.durable !== undefined),
+      14           35 :   ...SessionEvent.DurableDefinitions,
+      15            2 : ])
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/event.ts.gcov.html b/packages/core/schema/src/event.ts.gcov.html new file mode 100644 index 00000000..5c14a3e2 --- /dev/null +++ b/packages/core/schema/src/event.ts.gcov.html @@ -0,0 +1,201 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/event.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - event.tsCoverageTotalHit
Test:opencode-lcov.infoLines:93.2 %7469
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           33 : export * as Event from "./event"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           41 : import { ascending } from "./identifier"
+       6           38 : import { Location } from "./location"
+       7           35 : import { statics } from "./schema"
+       8              : 
+       9           71 : export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe(
+      10           26 :   Schema.brand("Event.ID"),
+      11           70 :   statics((schema) => ({ create: () => schema.make("evt_" + ascending()) })),
+      12            3 : )
+      13              : export type ID = typeof ID.Type
+      14              : 
+      15              : export type Definition<
+      16              :   Type extends string = string,
+      17              :   DataSchema extends Schema.Codec<unknown, unknown> = Schema.Codec<unknown, unknown>,
+      18              : > = Schema.Top & {
+      19              :   readonly type: Type
+      20              :   readonly durable?: {
+      21              :     readonly version: number
+      22              :     readonly aggregate: string
+      23              :   }
+      24              :   readonly data: DataSchema
+      25              : }
+      26              : 
+      27              : export type Data<D extends Definition> = Schema.Schema.Type<D["data"]>
+      28              : 
+      29              : export type Payload<D extends Definition = Definition> = {
+      30              :   readonly id: ID
+      31              :   readonly type: D["type"]
+      32              :   readonly data: Data<D>
+      33              :   readonly durable?: {
+      34              :     readonly aggregateID: string
+      35              :     readonly seq: number
+      36              :     readonly version: number
+      37              :   }
+      38              :   readonly location?: Location.Ref
+      39              :   readonly metadata?: Record<string, unknown>
+      40              : }
+      41              : 
+      42            5 : export function define<
+      43              :   const Type extends string,
+      44              :   const Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>,
+      45            8 : >(input: {
+      46              :   readonly type: Type
+      47              :   readonly durable?: {
+      48              :     readonly version: number
+      49              :     readonly aggregate: string
+      50              :   }
+      51              :   readonly schema: Fields
+      52            3 : }) {
+      53           43 :   const data = Schema.Struct(input.schema)
+      54           26 :   return Schema.Struct({
+      55           11 :     id: ID,
+      56           69 :     metadata: optional(Schema.Record(Schema.String, Schema.Unknown)),
+      57           37 :     type: Schema.Literal(input.type),
+      58          107 :     durable: optional(Schema.Struct({ aggregateID: Schema.String, seq: Schema.Int, version: Schema.Int })),
+      59           37 :     location: optional(Location.Ref),
+      60            6 :     data,
+      61            3 :   })
+      62           37 :     .annotate({ identifier: input.type })
+      63            5 :     .pipe(
+      64           19 :       statics(() => ({
+      65           24 :         type: input.type,
+      66           63 :         ...(input.durable === undefined ? {} : { durable: input.durable }),
+      67            6 :         data,
+      68            2 :       })),
+      69            2 :     ) satisfies Definition<Type, typeof data>
+      70              : }
+      71              : 
+      72           25 : export function inventory<const Definitions extends ReadonlyArray<Definition>>(...definitions: Definitions) {
+      73           34 :   return Object.freeze(definitions)
+      74              : }
+      75              : 
+      76           22 : export function latest(definitions: ReadonlyArray<Definition>) {
+      77           19 :   return readonlyMap(
+      78           47 :     definitions.reduce((result, definition) => {
+      79           49 :       const existing = result.get(definition.type)
+      80           21 :       if (!existing) {
+      81           46 :         result.set(definition.type, definition)
+      82           13 :         return result
+      83            4 :       }
+      84          109 :       if (definition.durable && existing.durable && definition.durable.version !== existing.durable.version) {
+      85          110 :         if (definition.durable.version > existing.durable.version) result.set(definition.type, definition)
+      86           13 :         return result
+      87            0 :       }
+      88            0 :       if (definition !== existing) throw new Error(`Duplicate latest event definition for ${definition.type}`)
+      89            2 :       return result
+      90            8 :     }, new Map<string, Definition>()),
+      91            2 :   )
+      92              : }
+      93              : 
+      94           24 : export function versionedType(type: string, version: number) {
+      95           28 :   return `${type}.${version}`
+      96              : }
+      97              : 
+      98           22 : export function durable<const Definitions extends ReadonlyArray<Definition>>(definitions: Definitions) {
+      99           19 :   return readonlyMap(
+     100           47 :     definitions.reduce((result, definition) => {
+     101           33 :       if (!definition.durable) return result
+     102           75 :       const key = versionedType(definition.type, definition.durable.version)
+     103           29 :       if (result.has(key)) throw new Error(`Duplicate durable event definition for ${key}`)
+     104           32 :       result.set(key, definition)
+     105           15 :       return result
+     106            8 :     }, new Map<string, Definitions[number]>()),
+     107            1 :   )
+     108              : }
+     109              : 
+     110            9 : function readonlyMap<Key, Value>(map: Map<Key, Value>): ReadonlyMap<Key, Value> {
+     111            0 :   const result: ReadonlyMap<Key, Value> = Object.freeze({
+     112            0 :     get size() {
+     113            5 :       return map.size
+     114              :     },
+     115           13 :     entries: () => map.entries(),
+     116            0 :     forEach: (callback: (value: Value, key: Key, map: ReadonlyMap<Key, Value>) => void, thisArg?: unknown) =>
+     117            5 :       map.forEach((value, key) => callback.call(thisArg, value, key, result)),
+     118           29 :     get: (key: Key) => map.get(key),
+     119            9 :     has: (key: Key) => map.has(key),
+     120           25 :     keys: () => map.keys(),
+     121           13 :     values: () => map.values(),
+     122           19 :     [Symbol.iterator]: () => map[Symbol.iterator](),
+     123            5 :   })
+     124           13 :   return result
+     125              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/file-diff.ts.gcov.html b/packages/core/schema/src/file-diff.ts.gcov.html new file mode 100644 index 00000000..214310bb --- /dev/null +++ b/packages/core/schema/src/file-diff.ts.gcov.html @@ -0,0 +1,89 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/file-diff.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - file-diff.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %1010
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           40 : export * as FileDiff from "./file-diff"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5              : 
+       6           36 : export const Info = Schema.Struct({
+       7           32 :   file: optional(Schema.String),
+       8           33 :   patch: optional(Schema.String),
+       9           27 :   additions: Schema.Finite,
+      10           27 :   deletions: Schema.Finite,
+      11           68 :   status: optional(Schema.Literals(["added", "deleted", "modified"])),
+      12           47 : }).annotate({ identifier: "SnapshotFileDiff" })
+      13              : export interface Info extends Schema.Schema.Type<typeof Info> {}
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/filesystem-watcher.ts.gcov.html b/packages/core/schema/src/filesystem-watcher.ts.gcov.html new file mode 100644 index 00000000..3705710b --- /dev/null +++ b/packages/core/schema/src/filesystem-watcher.ts.gcov.html @@ -0,0 +1,89 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/filesystem-watcher.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - filesystem-watcher.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %1111
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           58 : export * as FileSystemWatcher from "./filesystem-watcher"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           44 : import { define, inventory } from "./event"
+       5              : 
+       6           25 : const Updated = define({
+       7           31 :   type: "file.watcher.updated",
+       8           13 :   schema: {
+       9           24 :     file: Schema.String,
+      10           53 :     event: Schema.Literals(["add", "change", "unlink"]),
+      11            2 :   },
+      12            3 : })
+      13           65 : export const Event = { Updated, Definitions: inventory(Updated) }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/filesystem.ts.gcov.html b/packages/core/schema/src/filesystem.ts.gcov.html new file mode 100644 index 00000000..1608dadd --- /dev/null +++ b/packages/core/schema/src/filesystem.ts.gcov.html @@ -0,0 +1,116 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/filesystem.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - filesystem.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %3131
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1            5 : export * as FileSystem from "./filesystem"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           44 : import { define, inventory } from "./event"
+       6           69 : import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
+       7              : 
+       8           24 : const Edited = define({
+       9           22 :   type: "file.edited",
+      10           32 :   schema: { file: Schema.String },
+      11            3 : })
+      12           64 : export const Event = { Edited, Definitions: inventory(Edited) }
+      13              : 
+      14              : export interface Entry extends Schema.Schema.Type<typeof Entry> {}
+      15           37 : export const Entry = Schema.Struct({
+      16           21 :   path: RelativePath,
+      17           45 :   type: Schema.Literals(["file", "directory"]),
+      18           48 : }).annotate({ identifier: "FileSystem.Entry" })
+      19              : 
+      20              : export interface Submatch extends Schema.Schema.Type<typeof Submatch> {}
+      21           40 : export const Submatch = Schema.Struct({
+      22           22 :   text: Schema.String,
+      23           24 :   start: NonNegativeInt,
+      24           20 :   end: NonNegativeInt,
+      25           51 : }).annotate({ identifier: "FileSystem.Submatch" })
+      26              : 
+      27              : export interface Match extends Schema.Schema.Type<typeof Match> {}
+      28           37 : export const Match = Schema.Struct({
+      29           15 :   entry: Entry,
+      30           20 :   line: PositiveInt,
+      31           25 :   offset: NonNegativeInt,
+      32           22 :   text: Schema.String,
+      33           35 :   submatches: Schema.Array(Submatch),
+      34           48 : }).annotate({ identifier: "FileSystem.Match" })
+      35              : 
+      36           70 : export class FindInput extends Schema.Class<FindInput>("FileSystem.FindInput")({
+      37           23 :   query: Schema.String,
+      38           62 :   type: Schema.Literals(["file", "directory"]).pipe(optional),
+      39           34 :   limit: PositiveInt.pipe(optional),
+      40            4 : }) {}
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/identifier.ts.gcov.html b/packages/core/schema/src/identifier.ts.gcov.html new file mode 100644 index 00000000..16347e32 --- /dev/null +++ b/packages/core/schema/src/identifier.ts.gcov.html @@ -0,0 +1,106 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/identifier.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - identifier.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %2323
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           18 : const length = 26
+       2          144 : const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+       3           22 : let lastTimestamp = 0
+       4           16 : let counter = 0
+       5              : 
+       6           11 : export function ascending() {
+       7           21 :   return create(false)
+       8              : }
+       9              : 
+      10           11 : export function descending() {
+      11           20 :   return create(true)
+      12              : }
+      13              : 
+      14           45 : export function create(descending: boolean, timestamp = Date.now()) {
+      15           37 :   if (timestamp !== lastTimestamp) {
+      16           30 :     lastTimestamp = timestamp
+      17           14 :     counter = 0
+      18            2 :   }
+      19           12 :   counter++
+      20              : 
+      21           64 :   const current = BigInt(timestamp) * 0x1000n + BigInt(counter)
+      22           45 :   const value = descending ? ~current : current
+      23           52 :   const time = Array.from({ length: 6 }, (_, index) =>
+      24           48 :     Number((value >> BigInt(40 - 8 * index)) & 0xffn)
+      25           13 :       .toString(16)
+      26           15 :       .padStart(2, "0"),
+      27           13 :   ).join("")
+      28           59 :   const bytes = crypto.getRandomValues(new Uint8Array(length - 12))
+      29           60 :   return time + Array.from(bytes, (byte) => chars[byte % 62]).join("")
+      30              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/index-sort-f.html b/packages/core/schema/src/index-sort-f.html new file mode 100644 index 00000000..890772df --- /dev/null +++ b/packages/core/schema/src/index-sort-f.html @@ -0,0 +1,494 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/srcCoverageTotalHit
Test:opencode-lcov.infoLines:99.1 %17091693
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
agent.ts +
100.0%
+
100.0 %3131
catalog.ts +
100.0%
+
100.0 %44
command.ts +
100.0%
+
100.0 %1212
connection.ts +
100.0%
+
100.0 %1515
credential.ts +
100.0%
+
100.0 %2626
durable-event-manifest.ts +
100.0%
+
100.0 %1212
event.ts +
93.2%93.2%
+
93.2 %7469
file-diff.ts +
100.0%
+
100.0 %1010
filesystem-watcher.ts +
100.0%
+
100.0 %1111
filesystem.ts +
100.0%
+
100.0 %3131
identifier.ts +
100.0%
+
100.0 %2323
integration-id.ts +
100.0%
+
100.0 %33
integration.ts +
100.0%
+
100.0 %9696
llm.ts +
100.0%
+
100.0 %1919
location.ts +
94.4%94.4%
+
94.4 %1817
model.ts +
100.0%
+
100.0 %8989
models-dev.ts +
100.0%
+
100.0 %77
permission-saved.ts +
100.0%
+
100.0 %1515
permission-v1.ts +
100.0%
+
100.0 %11
permission.ts +
100.0%
+
100.0 %4848
plugin.ts +
100.0%
+
100.0 %99
project-copy.ts +
100.0%
+
100.0 %2121
project-directories.ts +
100.0%
+
100.0 %88
project-id.ts +
100.0%
+
100.0 %66
project.ts +
100.0%
+
100.0 %3434
prompt.ts +
85.7%85.7%
+
85.7 %4942
provider.ts +
100.0%
+
100.0 %5959
pty-ticket.ts +
100.0%
+
100.0 %77
pty.ts +
100.0%
+
100.0 %4747
question.ts +
100.0%
+
100.0 %6767
reference.ts +
100.0%
+
100.0 %3030
revert.ts +
100.0%
+
100.0 %1919
schema.ts +
100.0%
+
100.0 %2222
session-delivery.ts +
100.0%
+
100.0 %33
session-event.ts +
100.0%
+
100.0 %429429
session-id.ts +
100.0%
+
100.0 %1313
session-input.ts +
100.0%
+
100.0 %1818
session-message.ts +
100.0%
+
100.0 %170170
session-todo.ts +
100.0%
+
100.0 %2121
session-v1.ts +
100.0%
+
100.0 %11
session.ts +
100.0%
+
100.0 %4343
skill.ts +
95.2%95.2%
+
95.2 %4240
workspace-event.ts +
100.0%
+
100.0 %2525
workspace-id.ts +
93.8%93.8%
+
93.8 %1615
workspace.ts +
100.0%
+
100.0 %55
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/index-sort-l.html b/packages/core/schema/src/index-sort-l.html new file mode 100644 index 00000000..041a7568 --- /dev/null +++ b/packages/core/schema/src/index-sort-l.html @@ -0,0 +1,494 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/srcCoverageTotalHit
Test:opencode-lcov.infoLines:99.1 %17091693
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
prompt.ts +
85.7%85.7%
+
85.7 %4942
event.ts +
93.2%93.2%
+
93.2 %7469
workspace-id.ts +
93.8%93.8%
+
93.8 %1615
location.ts +
94.4%94.4%
+
94.4 %1817
skill.ts +
95.2%95.2%
+
95.2 %4240
permission-v1.ts +
100.0%
+
100.0 %11
session-v1.ts +
100.0%
+
100.0 %11
integration-id.ts +
100.0%
+
100.0 %33
session-delivery.ts +
100.0%
+
100.0 %33
catalog.ts +
100.0%
+
100.0 %44
workspace.ts +
100.0%
+
100.0 %55
project-id.ts +
100.0%
+
100.0 %66
models-dev.ts +
100.0%
+
100.0 %77
pty-ticket.ts +
100.0%
+
100.0 %77
project-directories.ts +
100.0%
+
100.0 %88
plugin.ts +
100.0%
+
100.0 %99
file-diff.ts +
100.0%
+
100.0 %1010
filesystem-watcher.ts +
100.0%
+
100.0 %1111
command.ts +
100.0%
+
100.0 %1212
durable-event-manifest.ts +
100.0%
+
100.0 %1212
session-id.ts +
100.0%
+
100.0 %1313
connection.ts +
100.0%
+
100.0 %1515
permission-saved.ts +
100.0%
+
100.0 %1515
session-input.ts +
100.0%
+
100.0 %1818
llm.ts +
100.0%
+
100.0 %1919
revert.ts +
100.0%
+
100.0 %1919
project-copy.ts +
100.0%
+
100.0 %2121
session-todo.ts +
100.0%
+
100.0 %2121
schema.ts +
100.0%
+
100.0 %2222
identifier.ts +
100.0%
+
100.0 %2323
workspace-event.ts +
100.0%
+
100.0 %2525
credential.ts +
100.0%
+
100.0 %2626
reference.ts +
100.0%
+
100.0 %3030
agent.ts +
100.0%
+
100.0 %3131
filesystem.ts +
100.0%
+
100.0 %3131
project.ts +
100.0%
+
100.0 %3434
session.ts +
100.0%
+
100.0 %4343
pty.ts +
100.0%
+
100.0 %4747
permission.ts +
100.0%
+
100.0 %4848
provider.ts +
100.0%
+
100.0 %5959
question.ts +
100.0%
+
100.0 %6767
model.ts +
100.0%
+
100.0 %8989
integration.ts +
100.0%
+
100.0 %9696
session-message.ts +
100.0%
+
100.0 %170170
session-event.ts +
100.0%
+
100.0 %429429
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/index.html b/packages/core/schema/src/index.html new file mode 100644 index 00000000..7fe0758e --- /dev/null +++ b/packages/core/schema/src/index.html @@ -0,0 +1,494 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/srcCoverageTotalHit
Test:opencode-lcov.infoLines:99.1 %17091693
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
agent.ts +
100.0%
+
100.0 %3131
catalog.ts +
100.0%
+
100.0 %44
command.ts +
100.0%
+
100.0 %1212
connection.ts +
100.0%
+
100.0 %1515
credential.ts +
100.0%
+
100.0 %2626
durable-event-manifest.ts +
100.0%
+
100.0 %1212
event.ts +
93.2%93.2%
+
93.2 %7469
file-diff.ts +
100.0%
+
100.0 %1010
filesystem-watcher.ts +
100.0%
+
100.0 %1111
filesystem.ts +
100.0%
+
100.0 %3131
identifier.ts +
100.0%
+
100.0 %2323
integration-id.ts +
100.0%
+
100.0 %33
integration.ts +
100.0%
+
100.0 %9696
llm.ts +
100.0%
+
100.0 %1919
location.ts +
94.4%94.4%
+
94.4 %1817
model.ts +
100.0%
+
100.0 %8989
models-dev.ts +
100.0%
+
100.0 %77
permission-saved.ts +
100.0%
+
100.0 %1515
permission-v1.ts +
100.0%
+
100.0 %11
permission.ts +
100.0%
+
100.0 %4848
plugin.ts +
100.0%
+
100.0 %99
project-copy.ts +
100.0%
+
100.0 %2121
project-directories.ts +
100.0%
+
100.0 %88
project-id.ts +
100.0%
+
100.0 %66
project.ts +
100.0%
+
100.0 %3434
prompt.ts +
85.7%85.7%
+
85.7 %4942
provider.ts +
100.0%
+
100.0 %5959
pty-ticket.ts +
100.0%
+
100.0 %77
pty.ts +
100.0%
+
100.0 %4747
question.ts +
100.0%
+
100.0 %6767
reference.ts +
100.0%
+
100.0 %3030
revert.ts +
100.0%
+
100.0 %1919
schema.ts +
100.0%
+
100.0 %2222
session-delivery.ts +
100.0%
+
100.0 %33
session-event.ts +
100.0%
+
100.0 %429429
session-id.ts +
100.0%
+
100.0 %1313
session-input.ts +
100.0%
+
100.0 %1818
session-message.ts +
100.0%
+
100.0 %170170
session-todo.ts +
100.0%
+
100.0 %2121
session-v1.ts +
100.0%
+
100.0 %11
session.ts +
100.0%
+
100.0 %4343
skill.ts +
95.2%95.2%
+
95.2 %4240
workspace-event.ts +
100.0%
+
100.0 %2525
workspace-id.ts +
93.8%93.8%
+
93.8 %1615
workspace.ts +
100.0%
+
100.0 %55
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/integration-id.ts.gcov.html b/packages/core/schema/src/integration-id.ts.gcov.html new file mode 100644 index 00000000..c4ee1284 --- /dev/null +++ b/packages/core/schema/src/integration-id.ts.gcov.html @@ -0,0 +1,83 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/integration-id.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - integration-id.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %33
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Schema } from "effect"
+       2              : 
+       3           80 : export const IntegrationID = Schema.String.pipe(Schema.brand("Integration.ID"))
+       4              : export type IntegrationID = typeof IntegrationID.Type
+       5              : 
+       6           91 : export const IntegrationMethodID = Schema.String.pipe(Schema.brand("Integration.MethodID"))
+       7              : export type IntegrationMethodID = typeof IntegrationMethodID.Type
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/integration.ts.gcov.html b/packages/core/schema/src/integration.ts.gcov.html new file mode 100644 index 00000000..1a87e5ed --- /dev/null +++ b/packages/core/schema/src/integration.ts.gcov.html @@ -0,0 +1,205 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/integration.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - integration.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %9696
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1            7 : export * as Integration from "./integration"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           44 : import { define, inventory } from "./event"
+       6           42 : import { Connection } from "./connection"
+       7           41 : import { ascending } from "./identifier"
+       8           35 : import { statics } from "./schema"
+       9           70 : import { IntegrationID, IntegrationMethodID } from "./integration-id"
+      10              : 
+      11           32 : export const ID = IntegrationID
+      12              : export type ID = typeof ID.Type
+      13              : 
+      14           44 : export const MethodID = IntegrationMethodID
+      15              : export type MethodID = typeof MethodID.Type
+      16              : 
+      17              : export interface When extends Schema.Schema.Type<typeof When> {}
+      18           36 : export const When = Schema.Struct({
+      19           21 :   key: Schema.String,
+      20           37 :   op: Schema.Literals(["eq", "neq"]),
+      21           21 :   value: Schema.String,
+      22           48 : }).annotate({ identifier: "Integration.When" })
+      23              : 
+      24              : export interface TextPrompt extends Schema.Schema.Type<typeof TextPrompt> {}
+      25           42 : export const TextPrompt = Schema.Struct({
+      26           31 :   type: Schema.Literal("text"),
+      27           21 :   key: Schema.String,
+      28           25 :   message: Schema.String,
+      29           39 :   placeholder: optional(Schema.String),
+      30           21 :   when: optional(When),
+      31           54 : }).annotate({ identifier: "Integration.TextPrompt" })
+      32              : 
+      33              : export interface SelectPrompt extends Schema.Schema.Type<typeof SelectPrompt> {}
+      34           44 : export const SelectPrompt = Schema.Struct({
+      35           33 :   type: Schema.Literal("select"),
+      36           21 :   key: Schema.String,
+      37           25 :   message: Schema.String,
+      38           22 :   options: Schema.Array(
+      39           19 :     Schema.Struct({
+      40           25 :       label: Schema.String,
+      41           25 :       value: Schema.String,
+      42           31 :       hint: optional(Schema.String),
+      43            2 :     }),
+      44            4 :   ),
+      45           21 :   when: optional(When),
+      46           56 : }).annotate({ identifier: "Integration.SelectPrompt" })
+      47              : 
+      48           98 : export const Prompt = Schema.Union([TextPrompt, SelectPrompt]).pipe(Schema.toTaggedUnion("type"))
+      49              : export type Prompt = typeof Prompt.Type
+      50              : 
+      51              : export interface OAuthMethod extends Schema.Schema.Type<typeof OAuthMethod> {}
+      52           43 : export const OAuthMethod = Schema.Struct({
+      53           15 :   id: MethodID,
+      54           32 :   type: Schema.Literal("oauth"),
+      55           23 :   label: Schema.String,
+      56           40 :   prompts: optional(Schema.Array(Prompt)),
+      57           55 : }).annotate({ identifier: "Integration.OAuthMethod" })
+      58              : 
+      59              : export interface KeyMethod extends Schema.Schema.Type<typeof KeyMethod> {}
+      60           41 : export const KeyMethod = Schema.Struct({
+      61           30 :   type: Schema.Literal("key"),
+      62           31 :   label: optional(Schema.String),
+      63           53 : }).annotate({ identifier: "Integration.KeyMethod" })
+      64              : 
+      65              : export interface EnvMethod extends Schema.Schema.Type<typeof EnvMethod> {}
+      66           41 : export const EnvMethod = Schema.Struct({
+      67           30 :   type: Schema.Literal("env"),
+      68           35 :   names: Schema.Array(Schema.String),
+      69           53 : }).annotate({ identifier: "Integration.EnvMethod" })
+      70              : 
+      71           71 : export const Method = Schema.Union([OAuthMethod, KeyMethod, EnvMethod])
+      72           35 :   .pipe(Schema.toTaggedUnion("type"))
+      73           48 :   .annotate({ identifier: "Integration.Method" })
+      74              : export type Method = typeof Method.Type
+      75              : 
+      76          113 : export const Inputs = Schema.Record(Schema.String, Schema.String).annotate({ identifier: "Integration.Inputs" })
+      77              : export type Inputs = typeof Inputs.Type
+      78              : 
+      79           25 : const Updated = define({
+      80           30 :   type: "integration.updated",
+      81           11 :   schema: {},
+      82            3 : })
+      83           35 : const ConnectionUpdated = define({
+      84           41 :   type: "integration.connection.updated",
+      85           30 :   schema: { integrationID: ID },
+      86            3 : })
+      87          104 : export const Event = { Updated, ConnectionUpdated, Definitions: inventory(Updated, ConnectionUpdated) }
+      88              : 
+      89              : export interface Ref extends Schema.Schema.Type<typeof Ref> {}
+      90           35 : export const Ref = Schema.Struct({
+      91            9 :   id: ID,
+      92           20 :   name: Schema.String,
+      93           47 : }).annotate({ identifier: "Integration.Ref" })
+      94              : 
+      95           61 : export class Info extends Schema.Class<Info>("Integration.Info")({
+      96            9 :   id: ID,
+      97           22 :   name: Schema.String,
+      98           32 :   methods: Schema.Array(Method),
+      99           43 :   connections: Schema.Array(Connection.Info),
+     100            5 : }) {}
+     101              : 
+     102           43 : export const AttemptID = Schema.String.pipe(
+     103           39 :   Schema.brand("Integration.AttemptID"),
+     104           70 :   statics((schema) => ({ create: () => schema.make("con_" + ascending()) })),
+     105            3 : )
+     106              : export type AttemptID = typeof AttemptID.Type
+     107              : 
+     108           36 : const AttemptTime = Schema.Struct({
+     109           25 :   created: Schema.Number,
+     110           23 :   expires: Schema.Number,
+     111            3 : })
+     112              : 
+     113           67 : export class Attempt extends Schema.Class<Attempt>("Integration.Attempt")({
+     114           23 :   attemptID: AttemptID,
+     115           21 :   url: Schema.String,
+     116           30 :   instructions: Schema.String,
+     117           42 :   mode: Schema.Literals(["auto", "code"]),
+     118           18 :   time: AttemptTime,
+     119            5 : }) {}
+     120              : 
+     121           44 : export const AttemptStatus = Schema.Union([
+     122           74 :   Schema.Struct({ status: Schema.Literal("pending"), time: AttemptTime }),
+     123           75 :   Schema.Struct({ status: Schema.Literal("complete"), time: AttemptTime }),
+     124           97 :   Schema.Struct({ status: Schema.Literal("failed"), message: Schema.String, time: AttemptTime }),
+     125           72 :   Schema.Struct({ status: Schema.Literal("expired"), time: AttemptTime }),
+     126            2 : ])
+     127           37 :   .pipe(Schema.toTaggedUnion("status"))
+     128           54 :   .annotate({ identifier: "Integration.AttemptStatus" })
+     129              : export type AttemptStatus = typeof AttemptStatus.Type
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/llm.ts.gcov.html b/packages/core/schema/src/llm.ts.gcov.html new file mode 100644 index 00000000..6dd412ce --- /dev/null +++ b/packages/core/schema/src/llm.ts.gcov.html @@ -0,0 +1,104 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/llm.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - llm.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %1919
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           29 : export * as LLM from "./llm"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5              : 
+       6          118 : export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)).annotate({
+       7           35 :   identifier: "LLM.ProviderMetadata",
+       8            3 : })
+       9              : export type ProviderMetadata = Schema.Schema.Type<typeof ProviderMetadata>
+      10              : 
+      11              : export interface ToolTextContent extends Schema.Schema.Type<typeof ToolTextContent> {}
+      12           47 : export const ToolTextContent = Schema.Struct({
+      13           31 :   type: Schema.Literal("text"),
+      14           20 :   text: Schema.String,
+      15           48 : }).annotate({ identifier: "Tool.TextContent" })
+      16              : 
+      17              : export interface ToolFileContent extends Schema.Schema.Type<typeof ToolFileContent> {}
+      18           47 : export const ToolFileContent = Schema.Struct({
+      19           31 :   type: Schema.Literal("file"),
+      20           21 :   uri: Schema.String,
+      21           22 :   mime: Schema.String,
+      22           30 :   name: optional(Schema.String),
+      23           48 : }).annotate({ identifier: "Tool.FileContent" })
+      24              : 
+      25           75 : export const ToolContent = Schema.Union([ToolTextContent, ToolFileContent])
+      26           35 :   .pipe(Schema.toTaggedUnion("type"))
+      27           44 :   .annotate({ identifier: "LLM.ToolContent" })
+      28              : export type ToolContent = Schema.Schema.Type<typeof ToolContent>
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/location.ts.gcov.html b/packages/core/schema/src/location.ts.gcov.html new file mode 100644 index 00000000..2bf3dfb0 --- /dev/null +++ b/packages/core/schema/src/location.ts.gcov.html @@ -0,0 +1,101 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/location.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - location.tsCoverageTotalHit
Test:opencode-lcov.infoLines:94.4 %1817
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1            1 : export * as Location from "./location"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           50 : import { AbsolutePath, optional } from "./schema"
+       5           41 : import { ProjectID } from "./project-id"
+       6           45 : import { WorkspaceID } from "./workspace-id"
+       7              : 
+       8              : export interface Ref extends Schema.Schema.Type<typeof Ref> {}
+       9           35 : export const Ref = Schema.Struct({
+      10           26 :   directory: AbsolutePath,
+      11           35 :   workspaceID: optional(WorkspaceID),
+      12           44 : }).annotate({ identifier: "Location.Ref" })
+      13              : 
+      14           58 : export class Info extends Schema.Class<Info>("Location.Info")({
+      15           26 :   directory: AbsolutePath,
+      16           37 :   workspaceID: optional(WorkspaceID),
+      17           28 :   project: Schema.Struct({
+      18           18 :     id: ProjectID,
+      19           25 :     directory: AbsolutePath,
+      20            3 :   }),
+      21            5 : }) {}
+      22              : 
+      23            0 : export function response<S extends Schema.Top>(data: S) {
+      24              :   return Schema.Struct({ location: Info, data })
+      25              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/model.ts.gcov.html b/packages/core/schema/src/model.ts.gcov.html new file mode 100644 index 00000000..ca2b65d8 --- /dev/null +++ b/packages/core/schema/src/model.ts.gcov.html @@ -0,0 +1,182 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/model.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - model.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %8989
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           33 : export * as Model from "./model"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           38 : import { Provider } from "./provider"
+       6           35 : import { statics } from "./schema"
+       7              : 
+       8           65 : export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID"))
+       9              : export type ID = typeof ID.Type
+      10              : 
+      11           71 : export const VariantID = Schema.String.pipe(Schema.brand("VariantID"))
+      12              : export type VariantID = typeof VariantID.Type
+      13              : 
+      14           35 : export const Ref = Schema.Struct({
+      15            9 :   id: ID,
+      16           26 :   providerID: Provider.ID,
+      17           34 :   variant: VariantID.pipe(optional),
+      18           41 : }).annotate({ identifier: "Model.Ref" })
+      19              : export interface Ref extends Schema.Schema.Type<typeof Ref> {}
+      20              : 
+      21           65 : export const Family = Schema.String.pipe(Schema.brand("Family"))
+      22              : export type Family = typeof Family.Type
+      23              : 
+      24              : export interface Capabilities extends Schema.Schema.Type<typeof Capabilities> {}
+      25           44 : export const Capabilities = Schema.Struct({
+      26           24 :   tools: Schema.Boolean,
+      27           37 :   input: Schema.Array(Schema.String),
+      28           36 :   output: Schema.Array(Schema.String),
+      29           50 : }).annotate({ identifier: "Model.Capabilities" })
+      30              : 
+      31              : export interface Cost extends Schema.Schema.Type<typeof Cost> {}
+      32           36 : export const Cost = Schema.Struct({
+      33           25 :   tier: Schema.Struct({
+      34           36 :     type: Schema.Literal("context"),
+      35           18 :     size: Schema.Int,
+      36           20 :   }).pipe(optional),
+      37           23 :   input: Schema.Finite,
+      38           24 :   output: Schema.Finite,
+      39           26 :   cache: Schema.Struct({
+      40           24 :     read: Schema.Finite,
+      41           22 :     write: Schema.Finite,
+      42            3 :   }),
+      43           42 : }).annotate({ identifier: "Model.Cost" })
+      44              : 
+      45           34 : export const Api = Schema.Union([
+      46           19 :   Schema.Struct({
+      47           14 :     id: ID,
+      48           23 :     ...Provider.AISDK.fields,
+      49            5 :   }),
+      50           19 :   Schema.Struct({
+      51           14 :     id: ID,
+      52           24 :     ...Provider.Native.fields,
+      53            3 :   }),
+      54            2 : ])
+      55           35 :   .pipe(Schema.toTaggedUnion("type"))
+      56           39 :   .annotate({ identifier: "Model.Api" })
+      57              : export type Api = typeof Api.Type
+      58              : 
+      59              : export interface Info extends Schema.Schema.Type<typeof Info> {}
+      60           36 : export const Info = Schema.Struct({
+      61            9 :   id: ID,
+      62           26 :   providerID: Provider.ID,
+      63           32 :   family: Family.pipe(optional),
+      64           22 :   name: Schema.String,
+      65           11 :   api: Api,
+      66           29 :   capabilities: Capabilities,
+      67           31 :   request: Schema.Struct({
+      68           28 :     ...Provider.Request.fields,
+      69           39 :     variant: Schema.String.pipe(optional),
+      70            5 :   }),
+      71           29 :   variants: Schema.Struct({
+      72           21 :     id: VariantID,
+      73           25 :     ...Provider.Request.fields,
+      74           24 :   }).pipe(Schema.Array),
+      75           25 :   time: Schema.Struct({
+      76           25 :     released: Schema.Finite,
+      77            5 :   }),
+      78           27 :   cost: Schema.Array(Cost),
+      79           69 :   status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
+      80           26 :   enabled: Schema.Boolean,
+      81           26 :   limit: Schema.Struct({
+      82           24 :     context: Schema.Int,
+      83           37 :     input: Schema.Int.pipe(optional),
+      84           20 :     output: Schema.Int,
+      85            3 :   }),
+      86            2 : })
+      87           41 :   .annotate({ identifier: "ModelV2.Info" })
+      88            5 :   .pipe(
+      89           23 :     statics((schema) => ({
+      90           31 :       empty: (providerID: Provider.ID, modelID: ID) =>
+      91           17 :         schema.make({
+      92           16 :           id: modelID,
+      93           15 :           providerID,
+      94           18 :           name: modelID,
+      95           55 :           api: { id: modelID, type: "native", settings: {} },
+      96           58 :           capabilities: { tools: false, input: [], output: [] },
+      97           39 :           request: { headers: {}, body: {} },
+      98           17 :           variants: [],
+      99           26 :           time: { released: 0 },
+     100           13 :           cost: [],
+     101           21 :           status: "active",
+     102           18 :           enabled: true,
+     103           34 :           limit: { context: 0, output: 0 },
+     104            2 :         }),
+     105            1 :     })),
+     106            2 :   )
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/models-dev.ts.gcov.html b/packages/core/schema/src/models-dev.ts.gcov.html new file mode 100644 index 00000000..06b24a02 --- /dev/null +++ b/packages/core/schema/src/models-dev.ts.gcov.html @@ -0,0 +1,85 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/models-dev.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - models-dev.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %77
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           42 : export * as ModelsDev from "./models-dev"
+       2              : 
+       3           44 : import { define, inventory } from "./event"
+       4              : 
+       5           27 : const Refreshed = define({
+       6           31 :   type: "models-dev.refreshed",
+       7           11 :   schema: {},
+       8            3 : })
+       9           69 : export const Event = { Refreshed, Definitions: inventory(Refreshed) }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/permission-saved.ts.gcov.html b/packages/core/schema/src/permission-saved.ts.gcov.html new file mode 100644 index 00000000..3d9200b0 --- /dev/null +++ b/packages/core/schema/src/permission-saved.ts.gcov.html @@ -0,0 +1,96 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/permission-saved.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - permission-saved.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %1515
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           54 : export * as PermissionSaved from "./permission-saved"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           41 : import { ascending } from "./identifier"
+       5           41 : import { ProjectID } from "./project-id"
+       6           35 : import { statics } from "./schema"
+       7              : 
+       8           36 : export const ID = Schema.String.pipe(
+       9           36 :   Schema.brand("PermissionSaved.ID"),
+      10           70 :   statics((schema) => ({ create: () => schema.make("psv_" + ascending()) })),
+      11            3 : )
+      12              : export type ID = typeof ID.Type
+      13              : 
+      14           36 : export const Info = Schema.Struct({
+      15            9 :   id: ID,
+      16           23 :   projectID: ProjectID,
+      17           24 :   action: Schema.String,
+      18           24 :   resource: Schema.String,
+      19           51 : }).annotate({ identifier: "PermissionSaved.Info" })
+      20              : export interface Info extends Schema.Schema.Type<typeof Info> {}
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/permission-v1.ts.gcov.html b/packages/core/schema/src/permission-v1.ts.gcov.html new file mode 100644 index 00000000..38169336 --- /dev/null +++ b/packages/core/schema/src/permission-v1.ts.gcov.html @@ -0,0 +1,77 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/permission-v1.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - permission-v1.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %11
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           31 : export * from "./v1/permission"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/permission.ts.gcov.html b/packages/core/schema/src/permission.ts.gcov.html new file mode 100644 index 00000000..d818979b --- /dev/null +++ b/packages/core/schema/src/permission.ts.gcov.html @@ -0,0 +1,141 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/permission.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - permission.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %4848
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           43 : export * as Permission from "./permission"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           44 : import { define, inventory } from "./event"
+       6           41 : import { ascending } from "./identifier"
+       7           41 : import { SessionID } from "./session-id"
+       8           35 : import { statics } from "./schema"
+       9              : 
+      10           70 : export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
+      11           33 :   Schema.brand("PermissionV2.ID"),
+      12           78 :   statics((schema) => ({ create: (id?: string) => schema.make(id ?? "per_" + ascending()) })),
+      13            3 : )
+      14              : export type ID = typeof ID.Type
+      15              : 
+      16           37 : export const Source = Schema.Union([
+      17           19 :   Schema.Struct({
+      18           33 :     type: Schema.Literal("tool"),
+      19           29 :     messageID: Schema.String,
+      20           23 :     callID: Schema.String,
+      21            3 :   }),
+      22           51 : ]).annotate({ identifier: "PermissionV2.Source" })
+      23              : export type Source = typeof Source.Type
+      24              : 
+      25           24 : const RequestFields = {
+      26           23 :   sessionID: SessionID,
+      27           24 :   action: Schema.String,
+      28           41 :   resources: Schema.Array(Schema.String),
+      29           51 :   save: Schema.Array(Schema.String).pipe(optional),
+      30           72 :   metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional),
+      31           30 :   source: Source.pipe(optional),
+      32            2 : }
+      33              : 
+      34           39 : export const Request = Schema.Struct({
+      35           12 :   id: ID,
+      36           14 :   ...RequestFields,
+      37           52 : }).annotate({ identifier: "PermissionV2.Request" })
+      38              : export interface Request extends Schema.Schema.Type<typeof Request> {}
+      39              : 
+      40          114 : export const Reply = Schema.Literals(["once", "always", "reject"]).annotate({ identifier: "PermissionV2.Reply" })
+      41              : export type Reply = typeof Reply.Type
+      42              : 
+      43           78 : const Asked = define({ type: "permission.v2.asked", schema: Request.fields })
+      44           25 : const Replied = define({
+      45           32 :   type: "permission.v2.replied",
+      46           13 :   schema: {
+      47           25 :     sessionID: SessionID,
+      48           18 :     requestID: ID,
+      49           14 :     reply: Reply,
+      50            2 :   },
+      51            3 : })
+      52           80 : export const Event = { Asked, Replied, Definitions: inventory(Asked, Replied) }
+      53              : 
+      54          112 : export const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Effect" })
+      55              : export type Effect = typeof Effect.Type
+      56              : 
+      57              : export interface Rule extends Schema.Schema.Type<typeof Rule> {}
+      58           36 : export const Rule = Schema.Struct({
+      59           24 :   action: Schema.String,
+      60           26 :   resource: Schema.String,
+      61           15 :   effect: Effect,
+      62           49 : }).annotate({ identifier: "PermissionV2.Rule" })
+      63              : 
+      64           90 : export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV2.Ruleset" })
+      65              : export type Ruleset = typeof Ruleset.Type
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/plugin.ts.gcov.html b/packages/core/schema/src/plugin.ts.gcov.html new file mode 100644 index 00000000..143c8a84 --- /dev/null +++ b/packages/core/schema/src/plugin.ts.gcov.html @@ -0,0 +1,89 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/plugin.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - plugin.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %99
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           35 : export * as Plugin from "./plugin"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           44 : import { define, inventory } from "./event"
+       5              : 
+       6           64 : export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
+       7              : export type ID = typeof ID.Type
+       8              : 
+       9           23 : const Added = define({
+      10           23 :   type: "plugin.added",
+      11           19 :   schema: { id: ID },
+      12            3 : })
+      13           61 : export const Event = { Added, Definitions: inventory(Added) }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/project-copy.ts.gcov.html b/packages/core/schema/src/project-copy.ts.gcov.html new file mode 100644 index 00000000..fdb2995b --- /dev/null +++ b/packages/core/schema/src/project-copy.ts.gcov.html @@ -0,0 +1,106 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/project-copy.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - project-copy.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %2121
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           46 : export * as ProjectCopy from "./project-copy"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           41 : import { ProjectID } from "./project-id"
+       6           40 : import { AbsolutePath } from "./schema"
+       7              : 
+       8          118 : export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID"))
+       9              : export type StrategyID = typeof StrategyID.Type
+      10              : 
+      11           43 : export const CreateInput = Schema.Struct({
+      12           23 :   projectID: ProjectID,
+      13           23 :   strategy: StrategyID,
+      14           32 :   sourceDirectory: AbsolutePath,
+      15           26 :   directory: AbsolutePath,
+      16           30 :   name: optional(Schema.String),
+      17           55 : }).annotate({ identifier: "ProjectCopy.CreateInput" })
+      18              : export interface CreateInput extends Schema.Schema.Type<typeof CreateInput> {}
+      19              : 
+      20           43 : export const RemoveInput = Schema.Struct({
+      21           23 :   projectID: ProjectID,
+      22           26 :   directory: AbsolutePath,
+      23           22 :   force: Schema.Boolean,
+      24           55 : }).annotate({ identifier: "ProjectCopy.RemoveInput" })
+      25              : export interface RemoveInput extends Schema.Schema.Type<typeof RemoveInput> {}
+      26              : 
+      27           36 : export const Copy = Schema.Struct({
+      28           24 :   directory: AbsolutePath,
+      29           47 : }).annotate({ identifier: "ProjectCopy.Copy" })
+      30              : export interface Copy extends Schema.Schema.Type<typeof Copy> {}
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/project-directories.ts.gcov.html b/packages/core/schema/src/project-directories.ts.gcov.html new file mode 100644 index 00000000..a2dae723 --- /dev/null +++ b/packages/core/schema/src/project-directories.ts.gcov.html @@ -0,0 +1,86 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/project-directories.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - project-directories.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %88
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           60 : export * as ProjectDirectories from "./project-directories"
+       2              : 
+       3           44 : import { define, inventory } from "./event"
+       4           36 : import { Project } from "./project"
+       5              : 
+       6           25 : const Updated = define({
+       7           38 :   type: "project.directories.updated",
+       8           34 :   schema: { projectID: Project.ID },
+       9            3 : })
+      10           65 : export const Event = { Updated, Definitions: inventory(Updated) }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/project-id.ts.gcov.html b/packages/core/schema/src/project-id.ts.gcov.html new file mode 100644 index 00000000..a0262378 --- /dev/null +++ b/packages/core/schema/src/project-id.ts.gcov.html @@ -0,0 +1,84 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/project-id.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - project-id.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %66
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Schema } from "effect"
+       2           35 : import { statics } from "./schema"
+       3              : 
+       4           43 : export const ProjectID = Schema.String.pipe(
+       5           28 :   Schema.brand("Project.ID"),
+       6           54 :   statics((schema) => ({ global: schema.make("global") })),
+       7            2 : )
+       8              : export type ProjectID = typeof ProjectID.Type
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/project.ts.gcov.html b/packages/core/schema/src/project.ts.gcov.html new file mode 100644 index 00000000..f037c3c2 --- /dev/null +++ b/packages/core/schema/src/project.ts.gcov.html @@ -0,0 +1,120 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/project.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - project.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %3434
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           37 : export * as Project from "./project"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           44 : import { define, inventory } from "./event"
+       5           52 : import { NonNegativeInt, optional } from "./schema"
+       6           41 : import { ProjectID } from "./project-id"
+       7              : 
+       8           28 : export const ID = ProjectID
+       9              : export type ID = typeof ID.Type
+      10              : 
+      11           81 : export const Vcs = Schema.Literal("git").annotate({ identifier: "Project.Vcs" })
+      12           36 : export const Icon = Schema.Struct({
+      13           31 :   url: optional(Schema.String),
+      14           36 :   override: optional(Schema.String),
+      15           31 :   color: optional(Schema.String),
+      16           44 : }).annotate({ identifier: "Project.Icon" })
+      17              : export interface Icon extends Schema.Schema.Type<typeof Icon> {}
+      18           40 : export const Commands = Schema.Struct({
+      19           16 :   start: optional(
+      20          105 :     Schema.String.annotate({ description: "Startup script to run when creating a new workspace (worktree)" }),
+      21            2 :   ),
+      22           48 : }).annotate({ identifier: "Project.Commands" })
+      23              : export interface Commands extends Schema.Schema.Type<typeof Commands> {}
+      24           36 : export const Time = Schema.Struct({
+      25           26 :   created: NonNegativeInt,
+      26           26 :   updated: NonNegativeInt,
+      27           38 :   initialized: optional(NonNegativeInt),
+      28           44 : }).annotate({ identifier: "Project.Time" })
+      29              : export interface Time extends Schema.Schema.Type<typeof Time> {}
+      30              : 
+      31           36 : export const Info = Schema.Struct({
+      32            9 :   id: ID,
+      33           26 :   worktree: Schema.String,
+      34           21 :   vcs: optional(Vcs),
+      35           32 :   name: optional(Schema.String),
+      36           23 :   icon: optional(Icon),
+      37           31 :   commands: optional(Commands),
+      38           13 :   time: Time,
+      39           39 :   sandboxes: Schema.Array(Schema.String),
+      40           39 : }).annotate({ identifier: "Project" })
+      41              : export interface Info extends Schema.Schema.Type<typeof Info> {}
+      42              : 
+      43           73 : const Updated = define({ type: "project.updated", schema: Info.fields })
+      44           65 : export const Event = { Updated, Definitions: inventory(Updated) }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/prompt.ts.gcov.html b/packages/core/schema/src/prompt.ts.gcov.html new file mode 100644 index 00000000..ff9e680a --- /dev/null +++ b/packages/core/schema/src/prompt.ts.gcov.html @@ -0,0 +1,133 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/prompt.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - prompt.tsCoverageTotalHit
Test:opencode-lcov.infoLines:85.7 %4942
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Schema } from "effect"
+       2           36 : import { optional } from "./schema"
+       3           35 : import { statics } from "./schema"
+       4              : 
+       5              : export interface Source extends Schema.Schema.Type<typeof Source> {}
+       6           38 : export const Source = Schema.Struct({
+       7           23 :   start: Schema.Finite,
+       8           21 :   end: Schema.Finite,
+       9           20 :   text: Schema.String,
+      10           45 : }).annotate({ identifier: "Prompt.Source" })
+      11              : 
+      12              : export interface FileAttachment extends Schema.Schema.Type<typeof FileAttachment> {}
+      13           46 : export const FileAttachment = Schema.Struct({
+      14           21 :   uri: Schema.String,
+      15           22 :   mime: Schema.String,
+      16           37 :   name: Schema.String.pipe(optional),
+      17           44 :   description: Schema.String.pipe(optional),
+      18           30 :   source: Source.pipe(optional),
+      19            2 : })
+      20           50 :   .annotate({ identifier: "Prompt.FileAttachment" })
+      21            5 :   .pipe(
+      22           23 :     statics((schema) => ({
+      23            0 :       create: (input: FileAttachment) =>
+      24            0 :         schema.make({
+      25            0 :           uri: input.uri,
+      26            0 :           mime: input.mime,
+      27            0 :           name: input.name,
+      28            0 :           description: input.description,
+      29            0 :           source: input.source,
+      30            1 :         }),
+      31            1 :     })),
+      32            3 :   )
+      33              : 
+      34              : export interface AgentAttachment extends Schema.Schema.Type<typeof AgentAttachment> {}
+      35           47 : export const AgentAttachment = Schema.Struct({
+      36           22 :   name: Schema.String,
+      37           30 :   source: Source.pipe(optional),
+      38           54 : }).annotate({ identifier: "Prompt.AgentAttachment" })
+      39              : 
+      40              : export interface Prompt extends Schema.Schema.Type<typeof Prompt> {}
+      41           38 : export const Prompt = Schema.Struct({
+      42           22 :   text: Schema.String,
+      43           53 :   files: Schema.Array(FileAttachment).pipe(optional),
+      44           53 :   agents: Schema.Array(AgentAttachment).pipe(optional),
+      45            2 : })
+      46           35 :   .annotate({ identifier: "Prompt" })
+      47            5 :   .pipe(
+      48           23 :     statics((schema) => ({
+      49           44 :       equivalence: Schema.toEquivalence(schema),
+      50           27 :       fromUserMessage: (input: Pick<Prompt, "text" | "files" | "agents">) =>
+      51           17 :         schema.make({
+      52           24 :           text: input.text,
+      53           36 :           ...(input.files === undefined ? {} : { files: input.files }),
+      54           31 :           ...(input.agents === undefined ? {} : { agents: input.agents }),
+      55            2 :         }),
+      56            1 :     })),
+      57            2 :   )
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/provider.ts.gcov.html b/packages/core/schema/src/provider.ts.gcov.html new file mode 100644 index 00000000..d7d78189 --- /dev/null +++ b/packages/core/schema/src/provider.ts.gcov.html @@ -0,0 +1,148 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/provider.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - provider.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %5959
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           39 : export * as Provider from "./provider"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           44 : import { Integration } from "./integration"
+       6           35 : import { statics } from "./schema"
+       7              : 
+       8           36 : export const ID = Schema.String.pipe(
+       9           31 :   Schema.brand("ProviderV2.ID"),
+      10           23 :   statics((schema) => ({
+      11           36 :     opencode: schema.make("opencode"),
+      12           38 :     anthropic: schema.make("anthropic"),
+      13           32 :     openai: schema.make("openai"),
+      14           32 :     google: schema.make("google"),
+      15           45 :     googleVertex: schema.make("google-vertex"),
+      16           47 :     githubCopilot: schema.make("github-copilot"),
+      17           47 :     amazonBedrock: schema.make("amazon-bedrock"),
+      18           30 :     azure: schema.make("azure"),
+      19           40 :     openrouter: schema.make("openrouter"),
+      20           34 :     mistral: schema.make("mistral"),
+      21           30 :     gitlab: schema.make("gitlab"),
+      22            1 :   })),
+      23            3 : )
+      24              : export type ID = typeof ID.Type
+      25              : 
+      26              : export interface AISDK extends Schema.Schema.Type<typeof AISDK> {}
+      27           37 : export const AISDK = Schema.Struct({
+      28           32 :   type: Schema.Literal("aisdk"),
+      29           25 :   package: Schema.String,
+      30           36 :   url: Schema.String.pipe(optional),
+      31           70 :   settings: Schema.Record(Schema.String, Schema.Unknown).pipe(optional),
+      32           46 : }).annotate({ identifier: "Provider.AISDK" })
+      33              : 
+      34              : export interface Native extends Schema.Schema.Type<typeof Native> {}
+      35           38 : export const Native = Schema.Struct({
+      36           33 :   type: Schema.Literal("native"),
+      37           36 :   url: Schema.String.pipe(optional),
+      38           55 :   settings: Schema.Record(Schema.String, Schema.Unknown),
+      39           47 : }).annotate({ identifier: "Provider.Native" })
+      40              : 
+      41           48 : export const Api = Schema.Union([AISDK, Native])
+      42           35 :   .pipe(Schema.toTaggedUnion("type"))
+      43           42 :   .annotate({ identifier: "Provider.Api" })
+      44              : export type Api = typeof Api.Type
+      45              : 
+      46              : export interface Request extends Schema.Schema.Type<typeof Request> {}
+      47           39 : export const Request = Schema.Struct({
+      48           55 :   headers: Schema.Record(Schema.String, Schema.String),
+      49           48 :   body: Schema.Record(Schema.String, Schema.Json),
+      50           48 : }).annotate({ identifier: "Provider.Request" })
+      51              : 
+      52              : export interface Info extends Schema.Schema.Type<typeof Info> {}
+      53           36 : export const Info = Schema.Struct({
+      54            9 :   id: ID,
+      55           47 :   integrationID: Integration.ID.pipe(optional),
+      56           22 :   name: Schema.String,
+      57           42 :   disabled: Schema.Boolean.pipe(optional),
+      58           11 :   api: Api,
+      59           17 :   request: Request,
+      60            2 : })
+      61           44 :   .annotate({ identifier: "ProviderV2.Info" })
+      62            5 :   .pipe(
+      63           23 :     statics((schema) => ({
+      64           14 :       empty: (id: ID) =>
+      65           17 :         schema.make({
+      66            7 :           id,
+      67           13 :           name: id,
+      68           42 :           api: { type: "native", settings: {} },
+      69           36 :           request: { headers: {}, body: {} },
+      70            2 :         }),
+      71            1 :     })),
+      72            2 :   )
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/pty-ticket.ts.gcov.html b/packages/core/schema/src/pty-ticket.ts.gcov.html new file mode 100644 index 00000000..298ab395 --- /dev/null +++ b/packages/core/schema/src/pty-ticket.ts.gcov.html @@ -0,0 +1,86 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/pty-ticket.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - pty-ticket.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %77
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           42 : export * as PtyTicket from "./pty-ticket"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           39 : import { PositiveInt } from "./schema"
+       5              : 
+       6           44 : export const ConnectToken = Schema.Struct({
+       7           24 :   ticket: Schema.String,
+       8           24 :   expires_in: PositiveInt,
+       9           53 : }).annotate({ identifier: "PtyTicket.ConnectToken" })
+      10              : export interface ConnectToken extends Schema.Schema.Type<typeof ConnectToken> {}
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/pty.ts.gcov.html b/packages/core/schema/src/pty.ts.gcov.html new file mode 100644 index 00000000..0d1b36a9 --- /dev/null +++ b/packages/core/schema/src/pty.ts.gcov.html @@ -0,0 +1,134 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/pty.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - pty.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %4747
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           29 : export * as Pty from "./pty"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           44 : import { define, inventory } from "./event"
+       6           41 : import { ascending } from "./identifier"
+       7           64 : import { NonNegativeInt, PositiveInt, statics } from "./schema"
+       8              : 
+       9           93 : const IDSchema = Schema.String.check(Schema.isStartsWith("pty")).pipe(Schema.brand("PtyID"))
+      10              : 
+      11           31 : export const ID = IDSchema.pipe(
+      12           22 :   statics((schema: typeof IDSchema) => {
+      13           55 :     const create = () => schema.make("pty_" + ascending())
+      14           12 :     return {
+      15           11 :       create,
+      16           46 :       ascending: (id?: string) => (id === undefined ? create() : schema.make(id)),
+      17            1 :     }
+      18            1 :   }),
+      19            3 : )
+      20              : export type ID = typeof ID.Type
+      21              : 
+      22           36 : export const Info = Schema.Struct({
+      23            9 :   id: ID,
+      24           23 :   title: Schema.String,
+      25           25 :   command: Schema.String,
+      26           36 :   args: Schema.Array(Schema.String),
+      27           21 :   cwd: Schema.String,
+      28           49 :   status: Schema.Literals(["running", "exited"]),
+      29           22 :   pid: NonNegativeInt,
+      30           35 :   exitCode: optional(NonNegativeInt),
+      31           35 : }).annotate({ identifier: "Pty" })
+      32              : export interface Info extends Schema.Schema.Type<typeof Info> {}
+      33              : 
+      34           72 : const Created = define({ type: "pty.created", schema: { info: Info } })
+      35           72 : const Updated = define({ type: "pty.updated", schema: { info: Info } })
+      36           92 : const Exited = define({ type: "pty.exited", schema: { id: ID, exitCode: NonNegativeInt } })
+      37           68 : const Deleted = define({ type: "pty.deleted", schema: { id: ID } })
+      38          118 : export const Event = { Created, Updated, Exited, Deleted, Definitions: inventory(Created, Updated, Exited, Deleted) }
+      39              : 
+      40           43 : export const CreateInput = Schema.Struct({
+      41           35 :   command: optional(Schema.String),
+      42           46 :   args: optional(Schema.Array(Schema.String)),
+      43           31 :   cwd: optional(Schema.String),
+      44           33 :   title: optional(Schema.String),
+      45           59 :   env: optional(Schema.Record(Schema.String, Schema.String)),
+      46            3 : })
+      47              : export interface CreateInput extends Schema.Schema.Type<typeof CreateInput> {}
+      48              : 
+      49           43 : export const UpdateInput = Schema.Struct({
+      50           33 :   title: optional(Schema.String),
+      51           15 :   size: optional(
+      52           19 :     Schema.Struct({
+      53           22 :       rows: PositiveInt,
+      54           19 :       cols: PositiveInt,
+      55            2 :     }),
+      56            2 :   ),
+      57            2 : })
+      58              : export interface UpdateInput extends Schema.Schema.Type<typeof UpdateInput> {}
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/question.ts.gcov.html b/packages/core/schema/src/question.ts.gcov.html new file mode 100644 index 00000000..ace69c14 --- /dev/null +++ b/packages/core/schema/src/question.ts.gcov.html @@ -0,0 +1,162 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/question.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - question.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %6767
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           39 : export * as Question from "./question"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           44 : import { define, inventory } from "./event"
+       6           41 : import { ascending } from "./identifier"
+       7           41 : import { SessionID } from "./session-id"
+       8           35 : import { statics } from "./schema"
+       9              : 
+      10           70 : export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe(
+      11           31 :   Schema.brand("QuestionV2.ID"),
+      12           22 :   statics((schema) => {
+      13           55 :     const create = () => schema.make("que_" + ascending())
+      14           12 :     return {
+      15           11 :       create,
+      16           63 :       ascending: (id?: string) => (id === undefined ? create() : schema.make(id)),
+      17            1 :     }
+      18            1 :   }),
+      19            3 : )
+      20              : export type ID = typeof ID.Type
+      21              : 
+      22           38 : export const Option = Schema.Struct({
+      23           86 :   label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }),
+      24           78 :   description: Schema.String.annotate({ description: "Explanation of choice" }),
+      25           49 : }).annotate({ identifier: "QuestionV2.Option" })
+      26              : export interface Option extends Schema.Schema.Type<typeof Option> {}
+      27              : 
+      28           15 : const base = {
+      29           73 :   question: Schema.String.annotate({ description: "Complete question" }),
+      30           85 :   header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }),
+      31           79 :   options: Schema.Array(Option).annotate({ description: "Available choices" }),
+      32          102 :   multiple: Schema.Boolean.pipe(optional).annotate({ description: "Allow selecting multiple choices" }),
+      33            2 : }
+      34              : 
+      35           39 : export const Info = Schema.Struct({
+      36            7 :   ...base,
+      37           52 :   custom: Schema.Boolean.pipe(optional).annotate({
+      38           61 :     description: "Allow typing a custom answer (default: true)",
+      39            3 :   }),
+      40           47 : }).annotate({ identifier: "QuestionV2.Info" })
+      41              : export interface Info extends Schema.Schema.Type<typeof Info> {}
+      42              : 
+      43           88 : export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV2.Prompt" })
+      44              : export interface Prompt extends Schema.Schema.Type<typeof Prompt> {}
+      45              : 
+      46           36 : export const Tool = Schema.Struct({
+      47           27 :   messageID: Schema.String,
+      48           22 :   callID: Schema.String,
+      49           47 : }).annotate({ identifier: "QuestionV2.Tool" })
+      50              : export interface Tool extends Schema.Schema.Type<typeof Tool> {}
+      51              : 
+      52           39 : export const Request = Schema.Struct({
+      53            9 :   id: ID,
+      54           23 :   sessionID: SessionID,
+      55           78 :   questions: Schema.Array(Info).annotate({ description: "Questions to ask" }),
+      56           26 :   tool: Tool.pipe(optional),
+      57           50 : }).annotate({ identifier: "QuestionV2.Request" })
+      58              : export interface Request extends Schema.Schema.Type<typeof Request> {}
+      59              : 
+      60           96 : export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV2.Answer" })
+      61              : export type Answer = typeof Answer.Type
+      62              : 
+      63           37 : export const Reply = Schema.Struct({
+      64           44 :   answers: Schema.Array(Answer).annotate({
+      65           96 :     description: "User answers in order of questions (each answer is an array of selected labels)",
+      66            3 :   }),
+      67           48 : }).annotate({ identifier: "QuestionV2.Reply" })
+      68              : export interface Reply extends Schema.Schema.Type<typeof Reply> {}
+      69              : 
+      70           76 : const Asked = define({ type: "question.v2.asked", schema: Request.fields })
+      71           25 : const Replied = define({
+      72           30 :   type: "question.v2.replied",
+      73           13 :   schema: {
+      74           25 :     sessionID: SessionID,
+      75           18 :     requestID: ID,
+      76           31 :     answers: Schema.Array(Answer),
+      77            2 :   },
+      78            3 : })
+      79           26 : const Rejected = define({
+      80           31 :   type: "question.v2.rejected",
+      81           13 :   schema: {
+      82           25 :     sessionID: SessionID,
+      83           15 :     requestID: ID,
+      84            2 :   },
+      85            3 : })
+      86           99 : export const Event = { Asked, Replied, Rejected, Definitions: inventory(Asked, Replied, Rejected) }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/reference.ts.gcov.html b/packages/core/schema/src/reference.ts.gcov.html new file mode 100644 index 00000000..8e7dc143 --- /dev/null +++ b/packages/core/schema/src/reference.ts.gcov.html @@ -0,0 +1,115 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/reference.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - reference.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %3030
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1            3 : export * as Reference from "./reference"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           44 : import { define, inventory } from "./event"
+       6           40 : import { AbsolutePath } from "./schema"
+       7              : 
+       8           66 : const Updated = define({ type: "reference.updated", schema: {} })
+       9           66 : export const Event = { Updated, Definitions: inventory(Updated) }
+      10              : 
+      11              : export interface LocalSource extends Schema.Schema.Type<typeof LocalSource> {}
+      12           43 : export const LocalSource = Schema.Struct({
+      13           32 :   type: Schema.Literal("local"),
+      14           21 :   path: AbsolutePath,
+      15           44 :   description: Schema.String.pipe(optional),
+      16           38 :   hidden: Schema.Boolean.pipe(optional),
+      17           53 : }).annotate({ identifier: "Reference.LocalSource" })
+      18              : 
+      19              : export interface GitSource extends Schema.Schema.Type<typeof GitSource> {}
+      20           41 : export const GitSource = Schema.Struct({
+      21           30 :   type: Schema.Literal("git"),
+      22           28 :   repository: Schema.String,
+      23           39 :   branch: Schema.String.pipe(optional),
+      24           44 :   description: Schema.String.pipe(optional),
+      25           38 :   hidden: Schema.Boolean.pipe(optional),
+      26           51 : }).annotate({ identifier: "Reference.GitSource" })
+      27              : 
+      28           60 : export const Source = Schema.Union([LocalSource, GitSource])
+      29           35 :   .pipe(Schema.toTaggedUnion("type"))
+      30           46 :   .annotate({ identifier: "Reference.Source" })
+      31              : export type Source = typeof Source.Type
+      32              : 
+      33           59 : export class Info extends Schema.Class<Info>("Reference.Info")({
+      34           22 :   name: Schema.String,
+      35           21 :   path: AbsolutePath,
+      36           44 :   description: Schema.String.pipe(optional),
+      37           40 :   hidden: Schema.Boolean.pipe(optional),
+      38           15 :   source: Source,
+      39            4 : }) {}
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/revert.ts.gcov.html b/packages/core/schema/src/revert.ts.gcov.html new file mode 100644 index 00000000..f47f1928 --- /dev/null +++ b/packages/core/schema/src/revert.ts.gcov.html @@ -0,0 +1,100 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/revert.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - revert.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %1919
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           35 : export * as Revert from "./revert"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           56 : import { NonNegativeInt, RelativePath } from "./schema"
+       6           51 : import { SessionMessage } from "./session-message"
+       7              : 
+       8           40 : export const FileDiff = Schema.Struct({
+       9           21 :   path: RelativePath,
+      10           60 :   status: Schema.Literals(["added", "modified", "deleted"]),
+      11           28 :   additions: NonNegativeInt,
+      12           28 :   deletions: NonNegativeInt,
+      13           21 :   patch: Schema.String,
+      14           41 : }).annotate({ identifier: "File.Diff" })
+      15              : export interface FileDiff extends Schema.Schema.Type<typeof FileDiff> {}
+      16              : 
+      17           37 : export const State = Schema.Struct({
+      18           31 :   messageID: SessionMessage.ID,
+      19           39 :   partID: Schema.String.pipe(optional),
+      20           41 :   snapshot: Schema.String.pipe(optional),
+      21           37 :   diff: Schema.String.pipe(optional),
+      22           45 :   files: Schema.Array(FileDiff).pipe(optional),
+      23           43 : }).annotate({ identifier: "Revert.State" })
+      24              : export interface State extends Schema.Schema.Type<typeof State> {}
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/schema.ts.gcov.html b/packages/core/schema/src/schema.ts.gcov.html new file mode 100644 index 00000000..f29b1cd5 --- /dev/null +++ b/packages/core/schema/src/schema.ts.gcov.html @@ -0,0 +1,106 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/schema.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - schema.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %2222
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           64 : import { DateTime, Option, Schema, SchemaGetter } from "effect"
+       2              : 
+       3           69 : export const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0))
+       4           81 : export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
+       5              : 
+       6           77 : export const RelativePath = Schema.String.pipe(Schema.brand("RelativePath"))
+       7              : export type RelativePath = typeof RelativePath.Type
+       8              : 
+       9           77 : export const AbsolutePath = Schema.String.pipe(Schema.brand("AbsolutePath"))
+      10              : export type AbsolutePath = typeof AbsolutePath.Type
+      11              : 
+      12           34 : export const optional = <S extends Schema.Top>(schema: S) =>
+      13           32 :   Schema.optionalKey(schema).pipe(
+      14           59 :     Schema.decodeTo(Schema.optional(Schema.toType(schema)), {
+      15           54 :       decode: SchemaGetter.passthrough({ strict: false }),
+      16           84 :       encode: SchemaGetter.transformOptional(Option.filter((value) => value !== undefined)),
+      17            1 :     }),
+      18            2 :   )
+      19              : 
+      20           21 : export const statics =
+      21           12 :   <S extends object, M extends Record<string, unknown>>(methods: (schema: S) => M) =>
+      22           12 :   (schema: S): S & M =>
+      23           40 :     Object.assign(schema, methods(schema))
+      24              : 
+      25           55 : export const DateTimeUtcFromMillis = Schema.Finite.pipe(
+      26           39 :   Schema.decodeTo(Schema.DateTimeUtc, {
+      27           70 :     decode: SchemaGetter.transform((value) => DateTime.makeUnsafe(value)),
+      28           71 :     encode: SchemaGetter.transform((value) => DateTime.toEpochMillis(value)),
+      29            1 :   }),
+      30            2 : )
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/session-delivery.ts.gcov.html b/packages/core/schema/src/session-delivery.ts.gcov.html new file mode 100644 index 00000000..e6ec8781 --- /dev/null +++ b/packages/core/schema/src/session-delivery.ts.gcov.html @@ -0,0 +1,82 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/session-delivery.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - session-delivery.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %33
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           54 : export * as SessionDelivery from "./session-delivery"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4              : 
+       5           59 : export const Delivery = Schema.Literals(["steer", "queue"])
+       6              : export type Delivery = typeof Delivery.Type
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/session-event.ts.gcov.html b/packages/core/schema/src/session-event.ts.gcov.html new file mode 100644 index 00000000..9e9adf84 --- /dev/null +++ b/packages/core/schema/src/session-event.ts.gcov.html @@ -0,0 +1,597 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/session-event.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - session-event.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %429429
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           48 : export * as SessionEvent from "./session-event"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           32 : import { Event } from "./event"
+       6           54 : import { ProviderMetadata, ToolContent } from "./llm"
+       7           46 : import { Delivery } from "./session-delivery"
+       8           32 : import { Model } from "./model"
+       9           79 : import { DateTimeUtcFromMillis, NonNegativeInt, RelativePath } from "./schema"
+      10           50 : import { FileAttachment, Prompt } from "./prompt"
+      11           41 : import { SessionID } from "./session-id"
+      12           38 : import { Location } from "./location"
+      13           51 : import { SessionMessage } from "./session-message"
+      14           34 : import { Revert } from "./revert"
+      15              : 
+      16           26 : export { FileAttachment }
+      17              : 
+      18           38 : export const Source = Schema.Struct({
+      19           24 :   start: NonNegativeInt,
+      20           22 :   end: NonNegativeInt,
+      21           20 :   text: Schema.String,
+      22           14 : }).annotate({
+      23           40 :   identifier: "session.next.event.source",
+      24            3 : })
+      25              : export interface Source extends Schema.Schema.Type<typeof Source> {}
+      26              : 
+      27           15 : const Base = {
+      28           35 :   timestamp: DateTimeUtcFromMillis,
+      29           21 :   sessionID: SessionID,
+      30            2 : }
+      31           26 : const PromptFields = {
+      32            7 :   ...Base,
+      33           31 :   messageID: SessionMessage.ID,
+      34           17 :   prompt: Prompt,
+      35           19 :   delivery: Delivery,
+      36            2 : }
+      37              : 
+      38           18 : const options = {
+      39           14 :   durable: {
+      40           27 :     aggregate: "sessionID",
+      41           12 :     version: 1,
+      42            2 :   },
+      43            2 : } as const
+      44           32 : const stepSettlementOptions = {
+      45           14 :   durable: {
+      46           27 :     aggregate: "sessionID",
+      47           12 :     version: 2,
+      48            2 :   },
+      49            2 : } as const
+      50              : 
+      51           56 : export const UnknownError = SessionMessage.UnknownError
+      52              : export type UnknownError = SessionMessage.UnknownError
+      53              : 
+      54           44 : export const AgentSwitched = Event.define({
+      55           41 :   type: "session.next.agent.switched",
+      56           10 :   ...options,
+      57           16 :   schema: {
+      58            9 :     ...Base,
+      59           33 :     messageID: SessionMessage.ID,
+      60           22 :     agent: Schema.String,
+      61            2 :   },
+      62            3 : })
+      63              : export type AgentSwitched = typeof AgentSwitched.Type
+      64              : 
+      65           44 : export const ModelSwitched = Event.define({
+      66           41 :   type: "session.next.model.switched",
+      67           10 :   ...options,
+      68           16 :   schema: {
+      69            9 :     ...Base,
+      70           33 :     messageID: SessionMessage.ID,
+      71           18 :     model: Model.Ref,
+      72            2 :   },
+      73            3 : })
+      74              : export type ModelSwitched = typeof ModelSwitched.Type
+      75              : 
+      76           36 : export const Moved = Event.define({
+      77           32 :   type: "session.next.moved",
+      78           10 :   ...options,
+      79           16 :   schema: {
+      80            9 :     ...Base,
+      81           27 :     location: Location.Ref,
+      82           43 :     subdirectory: RelativePath.pipe(optional),
+      83            2 :   },
+      84            3 : })
+      85              : export type Moved = typeof Moved.Type
+      86              : 
+      87           39 : export const Prompted = Event.define({
+      88           35 :   type: "session.next.prompted",
+      89           10 :   ...options,
+      90           21 :   schema: PromptFields,
+      91            3 : })
+      92              : export type Prompted = typeof Prompted.Type
+      93              : 
+      94           45 : export const PromptAdmitted = Event.define({
+      95           42 :   type: "session.next.prompt.admitted",
+      96           10 :   ...options,
+      97           21 :   schema: PromptFields,
+      98            3 : })
+      99              : export type PromptAdmitted = typeof PromptAdmitted.Type
+     100              : 
+     101           45 : export const ContextUpdated = Event.define({
+     102           42 :   type: "session.next.context.updated",
+     103           10 :   ...options,
+     104           16 :   schema: {
+     105            9 :     ...Base,
+     106           33 :     messageID: SessionMessage.ID,
+     107           21 :     text: Schema.String,
+     108            2 :   },
+     109            3 : })
+     110              : export type ContextUpdated = typeof ContextUpdated.Type
+     111              : 
+     112           40 : export const Synthetic = Event.define({
+     113           36 :   type: "session.next.synthetic",
+     114           10 :   ...options,
+     115           16 :   schema: {
+     116            9 :     ...Base,
+     117           33 :     messageID: SessionMessage.ID,
+     118           21 :     text: Schema.String,
+     119            2 :   },
+     120            3 : })
+     121              : export type Synthetic = typeof Synthetic.Type
+     122              : 
+     123           46 : export namespace Shell {
+     124           34 :   export const Started = Event.define({
+     125           42 :     type: "session.next.shell.started",
+     126           12 :     ...options,
+     127           18 :     schema: {
+     128           11 :       ...Base,
+     129           35 :       messageID: SessionMessage.ID,
+     130           28 :       callID: Schema.String,
+     131           26 :       command: Schema.String,
+     132            3 :     },
+     133            5 :   })
+     134              :   export type Started = typeof Started.Type
+     135              : 
+     136           32 :   export const Ended = Event.define({
+     137           40 :     type: "session.next.shell.ended",
+     138           12 :     ...options,
+     139           18 :     schema: {
+     140           11 :       ...Base,
+     141           28 :       callID: Schema.String,
+     142           25 :       output: Schema.String,
+     143            3 :     },
+     144            6 :   })
+     145              :   export type Ended = typeof Ended.Type
+     146              : }
+     147              : 
+     148           43 : export namespace Step {
+     149           33 :   export const Started = Event.define({
+     150           41 :     type: "session.next.step.started",
+     151           12 :     ...options,
+     152           18 :     schema: {
+     153           11 :       ...Base,
+     154           44 :       assistantMessageID: SessionMessage.ID,
+     155           27 :       agent: Schema.String,
+     156           23 :       model: Model.Ref,
+     157           42 :       snapshot: Schema.String.pipe(optional),
+     158            3 :     },
+     159            5 :   })
+     160              :   export type Started = typeof Started.Type
+     161              : 
+     162           31 :   export const Ended = Event.define({
+     163           39 :     type: "session.next.step.ended",
+     164           26 :     ...stepSettlementOptions,
+     165           18 :     schema: {
+     166           11 :       ...Base,
+     167           44 :       assistantMessageID: SessionMessage.ID,
+     168           28 :       finish: Schema.String,
+     169           26 :       cost: Schema.Finite,
+     170           31 :       tokens: Schema.Struct({
+     171           29 :         input: Schema.Finite,
+     172           30 :         output: Schema.Finite,
+     173           33 :         reasoning: Schema.Finite,
+     174           32 :         cache: Schema.Struct({
+     175           30 :           read: Schema.Finite,
+     176           28 :           write: Schema.Finite,
+     177            8 :         }),
+     178            9 :       }),
+     179           45 :       snapshot: Schema.String.pipe(optional),
+     180           52 :       files: Schema.Array(RelativePath).pipe(optional),
+     181            3 :     },
+     182            5 :   })
+     183              :   export type Ended = typeof Ended.Type
+     184              : 
+     185           32 :   export const Failed = Event.define({
+     186           40 :     type: "session.next.step.failed",
+     187           26 :     ...stepSettlementOptions,
+     188           18 :     schema: {
+     189           11 :       ...Base,
+     190           44 :       assistantMessageID: SessionMessage.ID,
+     191           23 :       error: UnknownError,
+     192            3 :     },
+     193            6 :   })
+     194              :   export type Failed = typeof Failed.Type
+     195              : }
+     196              : 
+     197           43 : export namespace Text {
+     198           33 :   export const Started = Event.define({
+     199           41 :     type: "session.next.text.started",
+     200           12 :     ...options,
+     201           18 :     schema: {
+     202           11 :       ...Base,
+     203           44 :       assistantMessageID: SessionMessage.ID,
+     204           25 :       textID: Schema.String,
+     205            3 :     },
+     206            5 :   })
+     207              :   export type Started = typeof Started.Type
+     208              : 
+     209              :   // Stream fragments are live-only; Text.Ended is the replayable full-value boundary.
+     210           31 :   export const Delta = Event.define({
+     211           36 :     type: "session.next.text.delta",
+     212           18 :     schema: {
+     213           11 :       ...Base,
+     214           44 :       assistantMessageID: SessionMessage.ID,
+     215           28 :       textID: Schema.String,
+     216           24 :       delta: Schema.String,
+     217            3 :     },
+     218            5 :   })
+     219              :   export type Delta = typeof Delta.Type
+     220              : 
+     221           31 :   export const Ended = Event.define({
+     222           39 :     type: "session.next.text.ended",
+     223           12 :     ...options,
+     224           18 :     schema: {
+     225           11 :       ...Base,
+     226           44 :       assistantMessageID: SessionMessage.ID,
+     227           28 :       textID: Schema.String,
+     228           23 :       text: Schema.String,
+     229            3 :     },
+     230            6 :   })
+     231              :   export type Ended = typeof Ended.Type
+     232              : }
+     233              : 
+     234           58 : export namespace Reasoning {
+     235           38 :   export const Started = Event.define({
+     236           46 :     type: "session.next.reasoning.started",
+     237           12 :     ...options,
+     238           18 :     schema: {
+     239           11 :       ...Base,
+     240           44 :       assistantMessageID: SessionMessage.ID,
+     241           33 :       reasoningID: Schema.String,
+     242           53 :       providerMetadata: ProviderMetadata.pipe(optional),
+     243            3 :     },
+     244            5 :   })
+     245              :   export type Started = typeof Started.Type
+     246              : 
+     247              :   // Stream fragments are live-only; Reasoning.Ended is the replayable full-value boundary.
+     248           36 :   export const Delta = Event.define({
+     249           41 :     type: "session.next.reasoning.delta",
+     250           18 :     schema: {
+     251           11 :       ...Base,
+     252           44 :       assistantMessageID: SessionMessage.ID,
+     253           33 :       reasoningID: Schema.String,
+     254           24 :       delta: Schema.String,
+     255            3 :     },
+     256            5 :   })
+     257              :   export type Delta = typeof Delta.Type
+     258              : 
+     259           36 :   export const Ended = Event.define({
+     260           44 :     type: "session.next.reasoning.ended",
+     261           12 :     ...options,
+     262           18 :     schema: {
+     263           11 :       ...Base,
+     264           44 :       assistantMessageID: SessionMessage.ID,
+     265           33 :       reasoningID: Schema.String,
+     266           26 :       text: Schema.String,
+     267           53 :       providerMetadata: ProviderMetadata.pipe(optional),
+     268            3 :     },
+     269            6 :   })
+     270              :   export type Ended = typeof Ended.Type
+     271              : }
+     272              : 
+     273           43 : export namespace Tool {
+     274           25 :   const ToolBase = {
+     275            9 :     ...Base,
+     276           42 :     assistantMessageID: SessionMessage.ID,
+     277           23 :     callID: Schema.String,
+     278            4 :   }
+     279              : 
+     280           57 :   export namespace Input {
+     281           36 :     export const Started = Event.define({
+     282           49 :       type: "session.next.tool.input.started",
+     283           14 :       ...options,
+     284           20 :       schema: {
+     285           17 :         ...ToolBase,
+     286           25 :         name: Schema.String,
+     287            5 :       },
+     288            7 :     })
+     289              :     export type Started = typeof Started.Type
+     290              : 
+     291              :     // Stream fragments are live-only; Input.Ended is the replayable raw-input boundary.
+     292           34 :     export const Delta = Event.define({
+     293           44 :       type: "session.next.tool.input.delta",
+     294           20 :       schema: {
+     295           17 :         ...ToolBase,
+     296           26 :         delta: Schema.String,
+     297            5 :       },
+     298            7 :     })
+     299              :     export type Delta = typeof Delta.Type
+     300              : 
+     301           34 :     export const Ended = Event.define({
+     302           47 :       type: "session.next.tool.input.ended",
+     303           14 :       ...options,
+     304           20 :       schema: {
+     305           17 :         ...ToolBase,
+     306           25 :         text: Schema.String,
+     307            5 :       },
+     308            7 :     })
+     309              :     export type Ended = typeof Ended.Type
+     310              :   }
+     311              : 
+     312           32 :   export const Called = Event.define({
+     313           40 :     type: "session.next.tool.called",
+     314           12 :     ...options,
+     315           18 :     schema: {
+     316           15 :       ...ToolBase,
+     317           26 :       tool: Schema.String,
+     318           58 :       input: Schema.Record(Schema.String, Schema.Unknown),
+     319           33 :       provider: Schema.Struct({
+     320           33 :         executed: Schema.Boolean,
+     321           47 :         metadata: ProviderMetadata.pipe(optional),
+     322            6 :       }),
+     323            3 :     },
+     324            5 :   })
+     325              :   export type Called = typeof Called.Type
+     326              : 
+     327              :   /**
+     328              :    * Replayable bounded running-tool state. Tools should checkpoint semantic
+     329              :    * transitions or at a bounded cadence, not persist every stdout/stderr chunk.
+     330              :    */
+     331           34 :   export const Progress = Event.define({
+     332           42 :     type: "session.next.tool.progress",
+     333           12 :     ...options,
+     334           18 :     schema: {
+     335           15 :       ...ToolBase,
+     336           63 :       structured: Schema.Record(Schema.String, Schema.Unknown),
+     337           38 :       content: Schema.Array(ToolContent),
+     338            3 :     },
+     339            5 :   })
+     340              :   export type Progress = typeof Progress.Type
+     341              : 
+     342           33 :   export const Success = Event.define({
+     343           41 :     type: "session.next.tool.success",
+     344           12 :     ...options,
+     345           18 :     schema: {
+     346           15 :       ...ToolBase,
+     347           63 :       structured: Schema.Record(Schema.String, Schema.Unknown),
+     348           41 :       content: Schema.Array(ToolContent),
+     349           62 :       outputPaths: Schema.Array(Schema.String).pipe(optional),
+     350           44 :       result: Schema.Unknown.pipe(optional),
+     351           33 :       provider: Schema.Struct({
+     352           33 :         executed: Schema.Boolean,
+     353           47 :         metadata: ProviderMetadata.pipe(optional),
+     354            6 :       }),
+     355            3 :     },
+     356            5 :   })
+     357              :   export type Success = typeof Success.Type
+     358              : 
+     359           32 :   export const Failed = Event.define({
+     360           40 :     type: "session.next.tool.failed",
+     361           12 :     ...options,
+     362           18 :     schema: {
+     363           15 :       ...ToolBase,
+     364           26 :       error: UnknownError,
+     365           44 :       result: Schema.Unknown.pipe(optional),
+     366           33 :       provider: Schema.Struct({
+     367           33 :         executed: Schema.Boolean,
+     368           47 :         metadata: ProviderMetadata.pipe(optional),
+     369            6 :       }),
+     370            3 :     },
+     371            6 :   })
+     372              :   export type Failed = typeof Failed.Type
+     373              : }
+     374              : 
+     375           42 : export const RetryError = Schema.Struct({
+     376           25 :   message: Schema.String,
+     377           43 :   statusCode: Schema.Finite.pipe(optional),
+     378           30 :   isRetryable: Schema.Boolean,
+     379           78 :   responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(optional),
+     380           45 :   responseBody: Schema.String.pipe(optional),
+     381           69 :   metadata: Schema.Record(Schema.String, Schema.String).pipe(optional),
+     382           14 : }).annotate({
+     383           39 :   identifier: "session.next.retry_error",
+     384            3 : })
+     385              : export interface RetryError extends Schema.Schema.Type<typeof RetryError> {}
+     386              : 
+     387           38 : export const Retried = Event.define({
+     388           34 :   type: "session.next.retried",
+     389           10 :   ...options,
+     390           16 :   schema: {
+     391            9 :     ...Base,
+     392           27 :     attempt: Schema.Finite,
+     393           19 :     error: RetryError,
+     394            2 :   },
+     395            3 : })
+     396              : export type Retried = typeof Retried.Type
+     397              : 
+     398           61 : export namespace Compaction {
+     399           39 :   export const Started = Event.define({
+     400           47 :     type: "session.next.compaction.started",
+     401           12 :     ...options,
+     402           18 :     schema: {
+     403           11 :       ...Base,
+     404           35 :       messageID: SessionMessage.ID,
+     405           76 :       reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]),
+     406            3 :     },
+     407            5 :   })
+     408              :   export type Started = typeof Started.Type
+     409              : 
+     410           37 :   export const Delta = Event.define({
+     411           42 :     type: "session.next.compaction.delta",
+     412           18 :     schema: {
+     413           11 :       ...Base,
+     414           35 :       messageID: SessionMessage.ID,
+     415           23 :       text: Schema.String,
+     416            3 :     },
+     417            5 :   })
+     418              :   export type Delta = typeof Delta.Type
+     419              : 
+     420           37 :   export const Ended = Event.define({
+     421           45 :     type: "session.next.compaction.ended",
+     422           12 :     ...options,
+     423           18 :     schema: {
+     424           11 :       ...Base,
+     425           35 :       messageID: SessionMessage.ID,
+     426           52 :       reason: Started.data.fields.reason,
+     427           26 :       text: Schema.String,
+     428           25 :       recent: Schema.String,
+     429            3 :     },
+     430            6 :   })
+     431              :   export type Ended = typeof Ended.Type
+     432              : }
+     433              : 
+     434           64 : export namespace RevertEvent {
+     435           39 :   export const Staged = Event.define({
+     436           42 :     type: "session.next.revert.staged",
+     437           12 :     ...options,
+     438           43 :     schema: { ...Base, revert: Revert.State },
+     439            5 :   })
+     440          104 :   export const Cleared = Event.define({ type: "session.next.revert.cleared", ...options, schema: Base })
+     441           42 :   export const Committed = Event.define({
+     442           45 :     type: "session.next.revert.committed",
+     443           12 :     ...options,
+     444           51 :     schema: { ...Base, messageID: SessionMessage.ID },
+     445            6 :   })
+     446              : }
+     447              : 
+     448           49 : export const DurableDefinitions = Event.inventory(
+     449           15 :   AgentSwitched,
+     450           15 :   ModelSwitched,
+     451            7 :   Moved,
+     452           10 :   Prompted,
+     453           16 :   PromptAdmitted,
+     454           16 :   ContextUpdated,
+     455           11 :   Synthetic,
+     456           15 :   Shell.Started,
+     457           13 :   Shell.Ended,
+     458           14 :   Step.Started,
+     459           12 :   Step.Ended,
+     460           13 :   Step.Failed,
+     461           14 :   Text.Started,
+     462           12 :   Text.Ended,
+     463           20 :   Tool.Input.Started,
+     464           18 :   Tool.Input.Ended,
+     465           13 :   Tool.Called,
+     466           15 :   Tool.Progress,
+     467           14 :   Tool.Success,
+     468           13 :   Tool.Failed,
+     469           19 :   Reasoning.Started,
+     470           17 :   Reasoning.Ended,
+     471            9 :   Retried,
+     472           20 :   Compaction.Started,
+     473           18 :   Compaction.Ended,
+     474           20 :   RevertEvent.Staged,
+     475           21 :   RevertEvent.Cleared,
+     476           21 :   RevertEvent.Committed,
+     477            3 : )
+     478              : 
+     479           42 : export const Definitions = Event.inventory(
+     480           15 :   AgentSwitched,
+     481           15 :   ModelSwitched,
+     482            7 :   Moved,
+     483           10 :   Prompted,
+     484           16 :   PromptAdmitted,
+     485           16 :   ContextUpdated,
+     486           11 :   Synthetic,
+     487           15 :   Shell.Started,
+     488           13 :   Shell.Ended,
+     489           14 :   Step.Started,
+     490           12 :   Step.Ended,
+     491           13 :   Step.Failed,
+     492           14 :   Text.Started,
+     493           12 :   Text.Delta,
+     494           12 :   Text.Ended,
+     495           19 :   Reasoning.Started,
+     496           17 :   Reasoning.Delta,
+     497           17 :   Reasoning.Ended,
+     498           20 :   Tool.Input.Started,
+     499           18 :   Tool.Input.Delta,
+     500           18 :   Tool.Input.Ended,
+     501           13 :   Tool.Called,
+     502           15 :   Tool.Progress,
+     503           14 :   Tool.Success,
+     504           13 :   Tool.Failed,
+     505            9 :   Retried,
+     506           20 :   Compaction.Started,
+     507           18 :   Compaction.Delta,
+     508           18 :   Compaction.Ended,
+     509           20 :   RevertEvent.Staged,
+     510           21 :   RevertEvent.Cleared,
+     511           21 :   RevertEvent.Committed,
+     512            3 : )
+     513              : 
+     514           74 : export const Durable = Schema.Union(DurableDefinitions, { mode: "oneOf" })
+     515           35 :   .pipe(Schema.toTaggedUnion("type"))
+     516           49 :   .annotate({ identifier: "SessionDurableEvent" })
+     517              : export type DurableEvent = typeof Durable.Type
+     518              : 
+     519           98 : export const All = Schema.Union(Definitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type"))
+     520              : export type Event = typeof All.Type
+     521              : export type Type = Event["type"]
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/session-id.ts.gcov.html b/packages/core/schema/src/session-id.ts.gcov.html new file mode 100644 index 00000000..8726449d --- /dev/null +++ b/packages/core/schema/src/session-id.ts.gcov.html @@ -0,0 +1,91 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/session-id.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - session-id.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %1313
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Schema } from "effect"
+       2           42 : import { descending } from "./identifier"
+       3           35 : import { statics } from "./schema"
+       4              : 
+       5           77 : export const SessionID = Schema.String.check(Schema.isStartsWith("ses")).pipe(
+       6           27 :   Schema.brand("SessionID"),
+       7           22 :   statics((schema) => {
+       8           56 :     const create = () => schema.make("ses_" + descending())
+       9           12 :     return {
+      10           11 :       create,
+      11           13 :       descending: (id?: string) => (id === undefined ? create() : schema.make(id)),
+      12            1 :     }
+      13            1 :   }),
+      14            2 : )
+      15              : export type SessionID = typeof SessionID.Type
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/session-input.ts.gcov.html b/packages/core/schema/src/session-input.ts.gcov.html new file mode 100644 index 00000000..85fbc556 --- /dev/null +++ b/packages/core/schema/src/session-input.ts.gcov.html @@ -0,0 +1,99 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/session-input.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - session-input.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %1818
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           48 : export * as SessionInput from "./session-input"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           34 : import { Prompt } from "./prompt"
+       6           65 : import { DateTimeUtcFromMillis, NonNegativeInt } from "./schema"
+       7           53 : import { SessionDelivery } from "./session-delivery"
+       8           41 : import { SessionID } from "./session-id"
+       9           51 : import { SessionMessage } from "./session-message"
+      10              : 
+      11           49 : export const Delivery = SessionDelivery.Delivery
+      12              : export type Delivery = SessionDelivery.Delivery
+      13              : 
+      14              : export interface Admitted extends Schema.Schema.Type<typeof Admitted> {}
+      15           40 : export const Admitted = Schema.Struct({
+      16           30 :   admittedSeq: NonNegativeInt,
+      17           24 :   id: SessionMessage.ID,
+      18           23 :   sessionID: SessionID,
+      19           17 :   prompt: Prompt,
+      20           21 :   delivery: Delivery,
+      21           37 :   timeCreated: DateTimeUtcFromMillis,
+      22           43 :   promotedSeq: NonNegativeInt.pipe(optional),
+      23           52 : }).annotate({ identifier: "SessionInput.Admitted" })
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/session-message.ts.gcov.html b/packages/core/schema/src/session-message.ts.gcov.html new file mode 100644 index 00000000..22bf01b1 --- /dev/null +++ b/packages/core/schema/src/session-message.ts.gcov.html @@ -0,0 +1,289 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/session-message.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - session-message.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %170170
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           52 : export * as SessionMessage from "./session-message"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           54 : import { ProviderMetadata, ToolContent } from "./llm"
+       6           32 : import { Model } from "./model"
+       7           50 : import { FileAttachment, Prompt } from "./prompt"
+       8           72 : import { DateTimeUtcFromMillis, RelativePath, statics } from "./schema"
+       9           41 : import { SessionID } from "./session-id"
+      10           41 : import { ascending } from "./identifier"
+      11              : 
+      12           71 : export const ID = Schema.String.check(Schema.isStartsWith("msg_")).pipe(
+      13           36 :   Schema.brand("Session.Message.ID"),
+      14           70 :   statics((schema) => ({ create: () => schema.make("msg_" + ascending()) })),
+      15            3 : )
+      16              : export type ID = typeof ID.Type
+      17              : 
+      18              : export interface UnknownError extends Schema.Schema.Type<typeof UnknownError> {}
+      19           44 : export const UnknownError = Schema.Struct({
+      20           34 :   type: Schema.Literal("unknown"),
+      21           23 :   message: Schema.String,
+      22           53 : }).annotate({ identifier: "Session.Error.Unknown" })
+      23              : 
+      24           15 : const Base = {
+      25            9 :   id: ID,
+      26           72 :   metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional),
+      27           56 :   time: Schema.Struct({ created: DateTimeUtcFromMillis }),
+      28            2 : }
+      29              : 
+      30              : export interface AgentSwitched extends Schema.Schema.Type<typeof AgentSwitched> {}
+      31           48 : export const AgentSwitched = Schema.Struct({
+      32            7 :   ...Base,
+      33           41 :   type: Schema.Literal("agent-switched"),
+      34           21 :   agent: Schema.String,
+      35           61 : }).annotate({ identifier: "Session.Message.AgentSwitched" })
+      36              : 
+      37              : export interface ModelSwitched extends Schema.Schema.Type<typeof ModelSwitched> {}
+      38           48 : export const ModelSwitched = Schema.Struct({
+      39            7 :   ...Base,
+      40           41 :   type: Schema.Literal("model-switched"),
+      41           17 :   model: Model.Ref,
+      42           61 : }).annotate({ identifier: "Session.Message.ModelSwitched" })
+      43              : 
+      44              : export interface User extends Schema.Schema.Type<typeof User> {}
+      45           39 : export const User = Schema.Struct({
+      46            7 :   ...Base,
+      47           27 :   text: Prompt.fields.text,
+      48           29 :   files: Prompt.fields.files,
+      49           31 :   agents: Prompt.fields.agents,
+      50           29 :   type: Schema.Literal("user"),
+      51           52 : }).annotate({ identifier: "Session.Message.User" })
+      52              : 
+      53              : export interface Synthetic extends Schema.Schema.Type<typeof Synthetic> {}
+      54           44 : export const Synthetic = Schema.Struct({
+      55            7 :   ...Base,
+      56           23 :   sessionID: SessionID,
+      57           22 :   text: Schema.String,
+      58           34 :   type: Schema.Literal("synthetic"),
+      59           57 : }).annotate({ identifier: "Session.Message.Synthetic" })
+      60              : 
+      61              : export interface System extends Schema.Schema.Type<typeof System> {}
+      62           41 : export const System = Schema.Struct({
+      63            7 :   ...Base,
+      64           33 :   type: Schema.Literal("system"),
+      65           20 :   text: Schema.String,
+      66           54 : }).annotate({ identifier: "Session.Message.System" })
+      67              : 
+      68              : export interface Shell extends Schema.Schema.Type<typeof Shell> {}
+      69           40 : export const Shell = Schema.Struct({
+      70            7 :   ...Base,
+      71           32 :   type: Schema.Literal("shell"),
+      72           24 :   callID: Schema.String,
+      73           25 :   command: Schema.String,
+      74           24 :   output: Schema.String,
+      75           25 :   time: Schema.Struct({
+      76           35 :     created: DateTimeUtcFromMillis,
+      77           49 :     completed: DateTimeUtcFromMillis.pipe(optional),
+      78            3 :   }),
+      79           53 : }).annotate({ identifier: "Session.Message.Shell" })
+      80              : 
+      81              : export interface ToolStatePending extends Schema.Schema.Type<typeof ToolStatePending> {}
+      82           48 : export const ToolStatePending = Schema.Struct({
+      83           36 :   status: Schema.Literal("pending"),
+      84           21 :   input: Schema.String,
+      85           65 : }).annotate({ identifier: "Session.Message.ToolState.Pending" })
+      86              : 
+      87              : export interface ToolStateRunning extends Schema.Schema.Type<typeof ToolStateRunning> {}
+      88           48 : export const ToolStateRunning = Schema.Struct({
+      89           36 :   status: Schema.Literal("running"),
+      90           54 :   input: Schema.Record(Schema.String, Schema.Unknown),
+      91           59 :   structured: Schema.Record(Schema.String, Schema.Unknown),
+      92           40 :   content: ToolContent.pipe(Schema.Array),
+      93           65 : }).annotate({ identifier: "Session.Message.ToolState.Running" })
+      94              : 
+      95              : export interface ToolStateCompleted extends Schema.Schema.Type<typeof ToolStateCompleted> {}
+      96           50 : export const ToolStateCompleted = Schema.Struct({
+      97           38 :   status: Schema.Literal("completed"),
+      98           54 :   input: Schema.Record(Schema.String, Schema.Unknown),
+      99           59 :   attachments: FileAttachment.pipe(Schema.Array, optional),
+     100           42 :   content: ToolContent.pipe(Schema.Array),
+     101           58 :   outputPaths: Schema.Array(Schema.String).pipe(optional),
+     102           59 :   structured: Schema.Record(Schema.String, Schema.Unknown),
+     103           38 :   result: Schema.Unknown.pipe(optional),
+     104           67 : }).annotate({ identifier: "Session.Message.ToolState.Completed" })
+     105              : 
+     106              : export interface ToolStateError extends Schema.Schema.Type<typeof ToolStateError> {}
+     107           46 : export const ToolStateError = Schema.Struct({
+     108           34 :   status: Schema.Literal("error"),
+     109           54 :   input: Schema.Record(Schema.String, Schema.Unknown),
+     110           42 :   content: ToolContent.pipe(Schema.Array),
+     111           59 :   structured: Schema.Record(Schema.String, Schema.Unknown),
+     112           22 :   error: UnknownError,
+     113           38 :   result: Schema.Unknown.pipe(optional),
+     114           63 : }).annotate({ identifier: "Session.Message.ToolState.Error" })
+     115              : 
+     116          116 : export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe(
+     117           30 :   Schema.toTaggedUnion("status"),
+     118            3 : )
+     119              : export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError
+     120              : 
+     121              : export interface AssistantTool extends Schema.Schema.Type<typeof AssistantTool> {}
+     122           45 : export const AssistantTool = Schema.Struct({
+     123           31 :   type: Schema.Literal("tool"),
+     124           20 :   id: Schema.String,
+     125           22 :   name: Schema.String,
+     126           29 :   provider: Schema.Struct({
+     127           29 :     executed: Schema.Boolean,
+     128           46 :     metadata: ProviderMetadata.pipe(optional),
+     129           49 :     resultMetadata: ProviderMetadata.pipe(optional),
+     130           20 :   }).pipe(optional),
+     131           19 :   state: ToolState,
+     132           25 :   time: Schema.Struct({
+     133           35 :     created: DateTimeUtcFromMillis,
+     134           46 :     ran: DateTimeUtcFromMillis.pipe(optional),
+     135           52 :     completed: DateTimeUtcFromMillis.pipe(optional),
+     136           46 :     pruned: DateTimeUtcFromMillis.pipe(optional),
+     137            3 :   }),
+     138           62 : }).annotate({ identifier: "Session.Message.Assistant.Tool" })
+     139              : 
+     140              : export interface AssistantText extends Schema.Schema.Type<typeof AssistantText> {}
+     141           45 : export const AssistantText = Schema.Struct({
+     142           31 :   type: Schema.Literal("text"),
+     143           20 :   id: Schema.String,
+     144           20 :   text: Schema.String,
+     145           62 : }).annotate({ identifier: "Session.Message.Assistant.Text" })
+     146              : 
+     147              : export interface AssistantReasoning extends Schema.Schema.Type<typeof AssistantReasoning> {}
+     148           50 : export const AssistantReasoning = Schema.Struct({
+     149           36 :   type: Schema.Literal("reasoning"),
+     150           20 :   id: Schema.String,
+     151           22 :   text: Schema.String,
+     152           52 :   providerMetadata: ProviderMetadata.pipe(optional),
+     153           25 :   time: Schema.Struct({
+     154           35 :     created: DateTimeUtcFromMillis,
+     155           49 :     completed: DateTimeUtcFromMillis.pipe(optional),
+     156           18 :   }).pipe(optional),
+     157           67 : }).annotate({ identifier: "Session.Message.Assistant.Reasoning" })
+     158              : 
+     159          101 : export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe(
+     160           28 :   Schema.toTaggedUnion("type"),
+     161            3 : )
+     162              : export type AssistantContent = AssistantText | AssistantReasoning | AssistantTool
+     163              : 
+     164              : export interface Assistant extends Schema.Schema.Type<typeof Assistant> {}
+     165           44 : export const Assistant = Schema.Struct({
+     166            7 :   ...Base,
+     167           36 :   type: Schema.Literal("assistant"),
+     168           23 :   agent: Schema.String,
+     169           19 :   model: Model.Ref,
+     170           47 :   content: AssistantContent.pipe(Schema.Array),
+     171           29 :   snapshot: Schema.Struct({
+     172           40 :     start: Schema.String.pipe(optional),
+     173           38 :     end: Schema.String.pipe(optional),
+     174           50 :     files: Schema.Array(RelativePath).pipe(optional),
+     175           20 :   }).pipe(optional),
+     176           39 :   finish: Schema.String.pipe(optional),
+     177           37 :   cost: Schema.Finite.pipe(optional),
+     178           27 :   tokens: Schema.Struct({
+     179           25 :     input: Schema.Finite,
+     180           26 :     output: Schema.Finite,
+     181           29 :     reasoning: Schema.Finite,
+     182           69 :     cache: Schema.Struct({ read: Schema.Finite, write: Schema.Finite }),
+     183           20 :   }).pipe(optional),
+     184           37 :   error: UnknownError.pipe(optional),
+     185           25 :   time: Schema.Struct({
+     186           35 :     created: DateTimeUtcFromMillis,
+     187           49 :     completed: DateTimeUtcFromMillis.pipe(optional),
+     188            3 :   }),
+     189           57 : }).annotate({ identifier: "Session.Message.Assistant" })
+     190              : 
+     191              : export interface Compaction extends Schema.Schema.Type<typeof Compaction> {}
+     192           42 : export const Compaction = Schema.Struct({
+     193           37 :   type: Schema.Literal("compaction"),
+     194           46 :   reason: Schema.Literals(["auto", "manual"]),
+     195           25 :   summary: Schema.String,
+     196           27 :   recent: Schema.String,
+     197            5 :   ...Base,
+     198           58 : }).annotate({ identifier: "Session.Message.Compaction" })
+     199              : 
+     200           38 : export const Message = Schema.Union([
+     201           16 :   AgentSwitched,
+     202           16 :   ModelSwitched,
+     203            7 :   User,
+     204           12 :   Synthetic,
+     205            9 :   System,
+     206            8 :   Shell,
+     207           12 :   Assistant,
+     208           11 :   Compaction,
+     209            2 : ])
+     210           35 :   .pipe(Schema.toTaggedUnion("type"))
+     211           44 :   .annotate({ identifier: "Session.Message" })
+     212              : export type Message = AgentSwitched | ModelSwitched | User | Synthetic | System | Shell | Assistant | Compaction
+     213              : export type Type = Message["type"]
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/session-todo.ts.gcov.html b/packages/core/schema/src/session-todo.ts.gcov.html new file mode 100644 index 00000000..8ac1fbd9 --- /dev/null +++ b/packages/core/schema/src/session-todo.ts.gcov.html @@ -0,0 +1,101 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/session-todo.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - session-todo.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %2121
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           46 : export * as SessionTodo from "./session-todo"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           44 : import { define, inventory } from "./event"
+       5           41 : import { SessionID } from "./session-id"
+       6              : 
+       7           36 : export const Info = Schema.Struct({
+       8           84 :   content: Schema.String.annotate({ description: "Brief description of the task" }),
+       9           36 :   status: Schema.String.annotate({
+      10           87 :     description: "Current status of the task: pending, in_progress, completed, cancelled",
+      11            5 :   }),
+      12           38 :   priority: Schema.String.annotate({
+      13           62 :     description: "Priority level of the task: high, medium, low",
+      14            3 :   }),
+      15           36 : }).annotate({ identifier: "Todo" })
+      16              : export interface Info extends Schema.Schema.Type<typeof Info> {}
+      17              : 
+      18           25 : const Updated = define({
+      19           23 :   type: "todo.updated",
+      20           13 :   schema: {
+      21           25 :     sessionID: SessionID,
+      22           27 :     todos: Schema.Array(Info),
+      23            2 :   },
+      24            3 : })
+      25           65 : export const Event = { Updated, Definitions: inventory(Updated) }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/session-v1.ts.gcov.html b/packages/core/schema/src/session-v1.ts.gcov.html new file mode 100644 index 00000000..70ddd2de --- /dev/null +++ b/packages/core/schema/src/session-v1.ts.gcov.html @@ -0,0 +1,77 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/session-v1.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - session-v1.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %11
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           28 : export * from "./v1/session"
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/session.ts.gcov.html b/packages/core/schema/src/session.ts.gcov.html new file mode 100644 index 00000000..9b072c0e --- /dev/null +++ b/packages/core/schema/src/session.ts.gcov.html @@ -0,0 +1,127 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/session.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - session.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %4343
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           37 : export * as Session from "./session"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           32 : import { Agent } from "./agent"
+       5           38 : import { Location } from "./location"
+       6           32 : import { Model } from "./model"
+       7           36 : import { Project } from "./project"
+       8           73 : import { DateTimeUtcFromMillis, optional, RelativePath } from "./schema"
+       9           47 : import { SessionEvent } from "./session-event"
+      10           41 : import { SessionID } from "./session-id"
+      11           34 : import { Revert } from "./revert"
+      12              : 
+      13           28 : export const ID = SessionID
+      14              : export type ID = SessionID
+      15              : 
+      16           34 : export const Event = SessionEvent
+      17              : 
+      18              : export interface Info extends Schema.Schema.Type<typeof Info> {}
+      19           36 : export const Info = Schema.Struct({
+      20            9 :   id: ID,
+      21           30 :   parentID: ID.pipe(optional),
+      22           24 :   projectID: Project.ID,
+      23           33 :   agent: Agent.ID.pipe(optional),
+      24           34 :   model: Model.Ref.pipe(optional),
+      25           22 :   cost: Schema.Finite,
+      26           27 :   tokens: Schema.Struct({
+      27           25 :     input: Schema.Finite,
+      28           26 :     output: Schema.Finite,
+      29           29 :     reasoning: Schema.Finite,
+      30           28 :     cache: Schema.Struct({
+      31           26 :       read: Schema.Finite,
+      32           24 :       write: Schema.Finite,
+      33            4 :     }),
+      34            5 :   }),
+      35           25 :   time: Schema.Struct({
+      36           35 :     created: DateTimeUtcFromMillis,
+      37           35 :     updated: DateTimeUtcFromMillis,
+      38           48 :     archived: DateTimeUtcFromMillis.pipe(optional),
+      39            5 :   }),
+      40           23 :   title: Schema.String,
+      41           25 :   location: Location.Ref,
+      42           39 :   subpath: RelativePath.pipe(optional),
+      43           36 :   revert: Revert.State.pipe(optional),
+      44           46 : }).annotate({ identifier: "SessionV2.Info" })
+      45              : 
+      46           42 : export const ListAnchor = Schema.Struct({
+      47            9 :   id: ID,
+      48           22 :   time: Schema.Finite,
+      49           49 :   direction: Schema.Literals(["previous", "next"]),
+      50           49 : }).annotate({ identifier: "Session.ListAnchor" })
+      51              : export interface ListAnchor extends Schema.Schema.Type<typeof ListAnchor> {}
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/skill.ts.gcov.html b/packages/core/schema/src/skill.ts.gcov.html new file mode 100644 index 00000000..a4fccb89 --- /dev/null +++ b/packages/core/schema/src/skill.ts.gcov.html @@ -0,0 +1,131 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/skill.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - skill.tsCoverageTotalHit
Test:opencode-lcov.infoLines:95.2 %4240
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           33 : export * as Skill from "./skill"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           36 : import { optional } from "./schema"
+       5           40 : import { AbsolutePath } from "./schema"
+       6              : 
+       7              : export interface DirectorySource extends Schema.Schema.Type<typeof DirectorySource> {}
+       8           47 : export const DirectorySource = Schema.Struct({
+       9           36 :   type: Schema.Literal("directory"),
+      10           19 :   path: AbsolutePath,
+      11           55 : }).annotate({ identifier: "SkillV2.DirectorySource" })
+      12              : 
+      13              : export interface UrlSource extends Schema.Schema.Type<typeof UrlSource> {}
+      14           41 : export const UrlSource = Schema.Struct({
+      15           30 :   type: Schema.Literal("url"),
+      16           19 :   url: Schema.String,
+      17           49 : }).annotate({ identifier: "SkillV2.UrlSource" })
+      18              : 
+      19              : export interface Info extends Schema.Schema.Type<typeof Info> {}
+      20           36 : export const Info = Schema.Struct({
+      21           22 :   name: Schema.String,
+      22           44 :   description: Schema.String.pipe(optional),
+      23           39 :   slash: Schema.Boolean.pipe(optional),
+      24           25 :   location: AbsolutePath,
+      25           23 :   content: Schema.String,
+      26           44 : }).annotate({ identifier: "SkillV2.Info" })
+      27              : 
+      28              : export interface EmbeddedSource extends Schema.Schema.Type<typeof EmbeddedSource> {}
+      29           46 : export const EmbeddedSource = Schema.Struct({
+      30           35 :   type: Schema.Literal("embedded"),
+      31           32 :   skill: Schema.suspend(() => Info),
+      32           54 : }).annotate({ identifier: "SkillV2.EmbeddedSource" })
+      33              : 
+      34              : export type Source = DirectorySource | UrlSource | EmbeddedSource
+      35           35 : export const Source = Object.assign(
+      36           64 :   Schema.Union([DirectorySource, UrlSource, EmbeddedSource]).pipe(
+      37           30 :     Schema.toTaggedUnion("type"),
+      38           49 :     Schema.annotate({ identifier: "SkillV2.Source" }),
+      39            3 :   ),
+      40            3 :   {
+      41           22 :     equals: (a: Source, b: Source) => {
+      42           43 :       if (a.type !== b.type) return false
+      43           82 :       if (a.type === "directory" && b.type === "directory") return a.path === b.path
+      44            0 :       if (a.type === "url" && b.type === "url") return a.url === b.url
+      45            0 :       if (a.type === "embedded" && b.type === "embedded") return a.skill.name === b.skill.name
+      46            3 :       return false
+      47              :     },
+      48           16 :     key: (source: Source) =>
+      49           29 :       source.type === "directory"
+      50           28 :         ? `directory:${source.path}`
+      51           23 :         : source.type === "url"
+      52           21 :           ? `url:${source.url}`
+      53           32 :           : `embedded:${source.skill.name}`,
+      54              :   },
+      55            2 : )
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/v1/index-sort-f.html b/packages/core/schema/src/v1/index-sort-f.html new file mode 100644 index 00000000..a1b5f84e --- /dev/null +++ b/packages/core/schema/src/v1/index-sort-f.html @@ -0,0 +1,107 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/v1 + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src/v1CoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %607607
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
permission.ts +
100.0%
+
100.0 %4444
session.ts +
100.0%
+
100.0 %563563
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/v1/index-sort-l.html b/packages/core/schema/src/v1/index-sort-l.html new file mode 100644 index 00000000..dfab68b7 --- /dev/null +++ b/packages/core/schema/src/v1/index-sort-l.html @@ -0,0 +1,107 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/v1 + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src/v1CoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %607607
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
permission.ts +
100.0%
+
100.0 %4444
session.ts +
100.0%
+
100.0 %563563
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/v1/index.html b/packages/core/schema/src/v1/index.html new file mode 100644 index 00000000..a1acca08 --- /dev/null +++ b/packages/core/schema/src/v1/index.html @@ -0,0 +1,107 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/v1 + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src/v1CoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %607607
Test Date:2026-08-28 03:27:30Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
permission.ts +
100.0%
+
100.0 %4444
session.ts +
100.0%
+
100.0 %563563
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/v1/permission.ts.gcov.html b/packages/core/schema/src/v1/permission.ts.gcov.html new file mode 100644 index 00000000..8d0d60b8 --- /dev/null +++ b/packages/core/schema/src/v1/permission.ts.gcov.html @@ -0,0 +1,142 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/v1/permission.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src/v1 - permission.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %4444
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           45 : export * as PermissionV1 from "./permission"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           45 : import { define, inventory } from "../event"
+       5           42 : import { ascending } from "../identifier"
+       6           37 : import { Project } from "../project"
+       7           36 : import { statics } from "../schema"
+       8           42 : import { SessionID } from "../session-id"
+       9              : 
+      10           70 : export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
+      11           30 :   Schema.brand("PermissionID"),
+      12           35 :   statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "per_" + ascending()) })),
+      13            3 : )
+      14              : export type ID = typeof ID.Type
+      15              : 
+      16          109 : export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionAction" })
+      17              : export type Action = typeof Action.Type
+      18              : 
+      19          116 : export const Rule = Schema.Struct({ permission: Schema.String, pattern: Schema.String, action: Action }).annotate({
+      20           29 :   identifier: "PermissionRule",
+      21            3 : })
+      22              : export type Rule = typeof Rule.Type
+      23              : 
+      24           88 : export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionRuleset" })
+      25              : export type Ruleset = typeof Ruleset.Type
+      26              : 
+      27           39 : export const Request = Schema.Struct({
+      28            9 :   id: ID,
+      29           23 :   sessionID: SessionID,
+      30           28 :   permission: Schema.String,
+      31           40 :   patterns: Schema.Array(Schema.String),
+      32           57 :   metadata: Schema.Record(Schema.String, Schema.Unknown),
+      33           38 :   always: Schema.Array(Schema.String),
+      34           90 :   tool: Schema.optional(Schema.Struct({ messageID: Schema.String, callID: Schema.String })),
+      35           49 : }).annotate({ identifier: "PermissionRequest" })
+      36              : export type Request = typeof Request.Type
+      37              : 
+      38           67 : export const Reply = Schema.Literals(["once", "always", "reject"])
+      39              : export type Reply = typeof Reply.Type
+      40              : 
+      41          109 : export const ReplyBody = Schema.Struct({ reply: Reply, message: Schema.optional(Schema.String) }).annotate({
+      42           34 :   identifier: "PermissionReplyBody",
+      43            3 : })
+      44              : export type ReplyBody = typeof ReplyBody.Type
+      45              : 
+      46          115 : export const Approval = Schema.Struct({ projectID: Project.ID, patterns: Schema.Array(Schema.String) }).annotate({
+      47           33 :   identifier: "PermissionApproval",
+      48            3 : })
+      49              : export type Approval = typeof Approval.Type
+      50              : 
+      51          115 : export const AskInput = Schema.Struct({ ...Request.fields, id: Schema.optional(ID), ruleset: Ruleset }).annotate({
+      52           33 :   identifier: "PermissionAskInput",
+      53            3 : })
+      54              : export type AskInput = typeof AskInput.Type
+      55              : 
+      56           91 : export const ReplyInput = Schema.Struct({ requestID: ID, ...ReplyBody.fields }).annotate({
+      57           35 :   identifier: "PermissionReplyInput",
+      58            3 : })
+      59              : export type ReplyInput = typeof ReplyInput.Type
+      60              : 
+      61           75 : const Asked = define({ type: "permission.asked", schema: Request.fields })
+      62           25 : const Replied = define({
+      63           29 :   type: "permission.replied",
+      64           62 :   schema: { sessionID: SessionID, requestID: ID, reply: Reply },
+      65            3 : })
+      66           79 : export const Event = { Asked, Replied, Definitions: inventory(Asked, Replied) }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/v1/session.ts.gcov.html b/packages/core/schema/src/v1/session.ts.gcov.html new file mode 100644 index 00000000..aadf3d53 --- /dev/null +++ b/packages/core/schema/src/v1/session.ts.gcov.html @@ -0,0 +1,752 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/v1/session.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src/v1 - session.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %563563
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1            1 : export * as SessionV1 from "./session"
+       2              : 
+       3           40 : import { Effect, Schema, Types } from "effect"
+       4           45 : import { define, inventory } from "../event"
+       5           40 : import { FileDiff } from "../file-diff"
+       6           37 : import { Project } from "../project"
+       7           39 : import { Provider } from "../provider"
+       8           33 : import { Model } from "../model"
+       9           62 : import { NonNegativeInt, optional, statics } from "../schema"
+      10           42 : import { ascending } from "../identifier"
+      11           42 : import { SessionID } from "../session-id"
+      12           46 : import { WorkspaceID } from "../workspace-id"
+      13           44 : import { PermissionV1 } from "./permission"
+      14              : 
+      15           72 : const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0))
+      16              : 
+      17           77 : export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe(
+      18           27 :   Schema.brand("MessageID"),
+      19           81 :   statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "msg_" + ascending()) })),
+      20            3 : )
+      21              : export type MessageID = typeof MessageID.Type
+      22              : 
+      23           74 : export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe(
+      24           24 :   Schema.brand("PartID"),
+      25           35 :   statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "prt_" + ascending()) })),
+      26            3 : )
+      27              : export type PartID = typeof PartID.Type
+      28              : 
+      29           38 : const namedError = <Name extends string, Fields extends Schema.Struct.Fields>(name: Name, fields: Fields) => {
+      30          104 :   const schema = Schema.Struct({ name: Schema.Literal(name), data: Schema.Struct(fields) }).annotate({
+      31           18 :     identifier: name,
+      32            5 :   })
+      33           49 :   return { Schema: schema, EffectSchema: schema }
+      34              : }
+      35              : 
+      36           76 : export const OutputLengthError = namedError("MessageOutputLengthError", {})
+      37              : 
+      38           59 : export const AuthError = namedError("ProviderAuthError", {
+      39           28 :   providerID: Schema.String,
+      40           23 :   message: Schema.String,
+      41            3 : })
+      42              : 
+      43           90 : export const AbortedError = namedError("MessageAbortedError", { message: Schema.String })
+      44           75 : export const StructuredOutputError = namedError("StructuredOutputError", {
+      45           25 :   message: Schema.String,
+      46           24 :   retries: NonNegativeInt,
+      47            3 : })
+      48           49 : export const APIError = namedError("APIError", {
+      49           25 :   message: Schema.String,
+      50           46 :   statusCode: Schema.optional(NonNegativeInt),
+      51           30 :   isRetryable: Schema.Boolean,
+      52           80 :   responseHeaders: Schema.optional(Schema.Record(Schema.String, Schema.String)),
+      53           47 :   responseBody: Schema.optional(Schema.String),
+      54           71 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
+      55            3 : })
+      56              : export type APIError = Schema.Schema.Type<typeof APIError.Schema>
+      57           73 : export const ContextOverflowError = namedError("ContextOverflowError", {
+      58           25 :   message: Schema.String,
+      59           45 :   responseBody: Schema.optional(Schema.String),
+      60            3 : })
+      61           69 : export const ContentFilterError = namedError("ContentFilterError", {
+      62           23 :   message: Schema.String,
+      63            3 : })
+      64              : 
+      65           73 : export class OutputFormatText extends Schema.Class<OutputFormatText>("OutputFormatText")({
+      66           29 :   type: Schema.Literal("text"),
+      67            5 : }) {}
+      68              : 
+      69           85 : export class OutputFormatJsonSchema extends Schema.Class<OutputFormatJsonSchema>("OutputFormatJsonSchema")({
+      70           38 :   type: Schema.Literal("json_schema"),
+      71           90 :   schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }),
+      72           96 :   retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))),
+      73            5 : }) {}
+      74              : 
+      75           90 : export const Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({
+      76           24 :   discriminator: "type",
+      77           27 :   identifier: "OutputFormat",
+      78            3 : })
+      79              : export type OutputFormat = Schema.Schema.Type<typeof Format>
+      80              : 
+      81           19 : const partBase = {
+      82           13 :   id: PartID,
+      83           23 :   sessionID: SessionID,
+      84           21 :   messageID: MessageID,
+      85            2 : }
+      86              : 
+      87           47 : export const SnapshotPart = Schema.Struct({
+      88           11 :   ...partBase,
+      89           35 :   type: Schema.Literal("snapshot"),
+      90           24 :   snapshot: Schema.String,
+      91           44 : }).annotate({ identifier: "SnapshotPart" })
+      92              : export type SnapshotPart = Types.DeepMutable<Schema.Schema.Type<typeof SnapshotPart>>
+      93              : 
+      94           44 : export const PatchPart = Schema.Struct({
+      95           11 :   ...partBase,
+      96           32 :   type: Schema.Literal("patch"),
+      97           22 :   hash: Schema.String,
+      98           35 :   files: Schema.Array(Schema.String),
+      99           41 : }).annotate({ identifier: "PatchPart" })
+     100              : export type PatchPart = Types.DeepMutable<Schema.Schema.Type<typeof PatchPart>>
+     101              : 
+     102           43 : export const TextPart = Schema.Struct({
+     103           11 :   ...partBase,
+     104           31 :   type: Schema.Literal("text"),
+     105           22 :   text: Schema.String,
+     106           45 :   synthetic: Schema.optional(Schema.Boolean),
+     107           43 :   ignored: Schema.optional(Schema.Boolean),
+     108           22 :   time: Schema.optional(
+     109           19 :     Schema.Struct({
+     110           26 :       start: NonNegativeInt,
+     111           38 :       end: Schema.optional(NonNegativeInt),
+     112            2 :     }),
+     113            4 :   ),
+     114           68 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
+     115           40 : }).annotate({ identifier: "TextPart" })
+     116              : export type TextPart = Types.DeepMutable<Schema.Schema.Type<typeof TextPart>>
+     117              : 
+     118           48 : export const ReasoningPart = Schema.Struct({
+     119           11 :   ...partBase,
+     120           36 :   type: Schema.Literal("reasoning"),
+     121           22 :   text: Schema.String,
+     122           70 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
+     123           25 :   time: Schema.Struct({
+     124           26 :     start: NonNegativeInt,
+     125           38 :     end: Schema.optional(NonNegativeInt),
+     126            3 :   }),
+     127           45 : }).annotate({ identifier: "ReasoningPart" })
+     128              : export type ReasoningPart = Types.DeepMutable<Schema.Schema.Type<typeof ReasoningPart>>
+     129              : 
+     130           29 : const filePartSourceBase = {
+     131           25 :   text: Schema.Struct({
+     132           25 :     value: Schema.String,
+     133           25 :     start: Schema.Finite,
+     134           20 :     end: Schema.Finite,
+     135           50 :   }).annotate({ identifier: "FilePartSourceText" }),
+     136            2 : }
+     137              : 
+     138           37 : export const Range = Schema.Struct({
+     139           76 :   start: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }),
+     140           72 :   end: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }),
+     141           37 : }).annotate({ identifier: "Range" })
+     142              : export type Range = typeof Range.Type
+     143              : 
+     144           45 : export const FileSource = Schema.Struct({
+     145           21 :   ...filePartSourceBase,
+     146           31 :   type: Schema.Literal("file"),
+     147           20 :   path: Schema.String,
+     148           42 : }).annotate({ identifier: "FileSource" })
+     149              : 
+     150           47 : export const SymbolSource = Schema.Struct({
+     151           21 :   ...filePartSourceBase,
+     152           33 :   type: Schema.Literal("symbol"),
+     153           22 :   path: Schema.String,
+     154           15 :   range: Range,
+     155           22 :   name: Schema.String,
+     156           21 :   kind: NonNegativeInt,
+     157           44 : }).annotate({ identifier: "SymbolSource" })
+     158              : 
+     159           49 : export const ResourceSource = Schema.Struct({
+     160           21 :   ...filePartSourceBase,
+     161           35 :   type: Schema.Literal("resource"),
+     162           28 :   clientName: Schema.String,
+     163           19 :   uri: Schema.String,
+     164           46 : }).annotate({ identifier: "ResourceSource" })
+     165              : 
+     166           98 : export const FilePartSource = Schema.Union([FileSource, SymbolSource, ResourceSource]).annotate({
+     167           24 :   discriminator: "type",
+     168           29 :   identifier: "FilePartSource",
+     169            3 : })
+     170              : 
+     171           43 : export const FilePart = Schema.Struct({
+     172           11 :   ...partBase,
+     173           31 :   type: Schema.Literal("file"),
+     174           22 :   mime: Schema.String,
+     175           43 :   filename: Schema.optional(Schema.String),
+     176           21 :   url: Schema.String,
+     177           40 :   source: Schema.optional(FilePartSource),
+     178           40 : }).annotate({ identifier: "FilePart" })
+     179              : export type FilePart = Types.DeepMutable<Schema.Schema.Type<typeof FilePart>>
+     180              : 
+     181           44 : export const AgentPart = Schema.Struct({
+     182           11 :   ...partBase,
+     183           32 :   type: Schema.Literal("agent"),
+     184           22 :   name: Schema.String,
+     185           24 :   source: Schema.optional(
+     186           19 :     Schema.Struct({
+     187           25 :       value: Schema.String,
+     188           26 :       start: NonNegativeInt,
+     189           21 :       end: NonNegativeInt,
+     190            2 :     }),
+     191            2 :   ),
+     192           41 : }).annotate({ identifier: "AgentPart" })
+     193              : export type AgentPart = Types.DeepMutable<Schema.Schema.Type<typeof AgentPart>>
+     194              : 
+     195           49 : export const CompactionPart = Schema.Struct({
+     196           11 :   ...partBase,
+     197           37 :   type: Schema.Literal("compaction"),
+     198           23 :   auto: Schema.Boolean,
+     199           44 :   overflow: Schema.optional(Schema.Boolean),
+     200           42 :   tail_start_id: Schema.optional(MessageID),
+     201           46 : }).annotate({ identifier: "CompactionPart" })
+     202              : export type CompactionPart = Types.DeepMutable<Schema.Schema.Type<typeof CompactionPart>>
+     203              : 
+     204           46 : export const SubtaskPart = Schema.Struct({
+     205           11 :   ...partBase,
+     206           34 :   type: Schema.Literal("subtask"),
+     207           24 :   prompt: Schema.String,
+     208           29 :   description: Schema.String,
+     209           23 :   agent: Schema.String,
+     210           23 :   model: Schema.optional(
+     211           19 :     Schema.Struct({
+     212           28 :       providerID: Provider.ID,
+     213           19 :       modelID: Model.ID,
+     214            2 :     }),
+     215            4 :   ),
+     216           40 :   command: Schema.optional(Schema.String),
+     217           43 : }).annotate({ identifier: "SubtaskPart" })
+     218              : export type SubtaskPart = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPart>>
+     219              : 
+     220           44 : export const RetryPart = Schema.Struct({
+     221           11 :   ...partBase,
+     222           32 :   type: Schema.Literal("retry"),
+     223           26 :   attempt: NonNegativeInt,
+     224           31 :   error: APIError.EffectSchema,
+     225           25 :   time: Schema.Struct({
+     226           25 :     created: NonNegativeInt,
+     227            3 :   }),
+     228           41 : }).annotate({ identifier: "RetryPart" })
+     229              : export type RetryPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof RetryPart>>, "error"> & {
+     230              :   error: APIError
+     231              : }
+     232              : 
+     233           48 : export const StepStartPart = Schema.Struct({
+     234           11 :   ...partBase,
+     235           37 :   type: Schema.Literal("step-start"),
+     236           41 :   snapshot: Schema.optional(Schema.String),
+     237           45 : }).annotate({ identifier: "StepStartPart" })
+     238              : export type StepStartPart = Types.DeepMutable<Schema.Schema.Type<typeof StepStartPart>>
+     239              : 
+     240           49 : export const StepFinishPart = Schema.Struct({
+     241           11 :   ...partBase,
+     242           38 :   type: Schema.Literal("step-finish"),
+     243           24 :   reason: Schema.String,
+     244           43 :   snapshot: Schema.optional(Schema.String),
+     245           22 :   cost: Schema.Finite,
+     246           27 :   tokens: Schema.Struct({
+     247           42 :     total: Schema.optional(Schema.Finite),
+     248           25 :     input: Schema.Finite,
+     249           26 :     output: Schema.Finite,
+     250           29 :     reasoning: Schema.Finite,
+     251           28 :     cache: Schema.Struct({
+     252           26 :       read: Schema.Finite,
+     253           24 :       write: Schema.Finite,
+     254            4 :     }),
+     255            3 :   }),
+     256           46 : }).annotate({ identifier: "StepFinishPart" })
+     257              : export type StepFinishPart = Types.DeepMutable<Schema.Schema.Type<typeof StepFinishPart>>
+     258              : 
+     259           48 : export const ToolStatePending = Schema.Struct({
+     260           36 :   status: Schema.Literal("pending"),
+     261           50 :   input: Schema.Record(Schema.String, Schema.Any),
+     262           19 :   raw: Schema.String,
+     263           48 : }).annotate({ identifier: "ToolStatePending" })
+     264              : export type ToolStatePending = Types.DeepMutable<Schema.Schema.Type<typeof ToolStatePending>>
+     265              : 
+     266           48 : export const ToolStateRunning = Schema.Struct({
+     267           36 :   status: Schema.Literal("running"),
+     268           50 :   input: Schema.Record(Schema.String, Schema.Any),
+     269           40 :   title: Schema.optional(Schema.String),
+     270           70 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
+     271           25 :   time: Schema.Struct({
+     272           23 :     start: NonNegativeInt,
+     273            3 :   }),
+     274           48 : }).annotate({ identifier: "ToolStateRunning" })
+     275              : export type ToolStateRunning = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateRunning>>
+     276              : 
+     277           50 : export const ToolStateCompleted = Schema.Struct({
+     278           38 :   status: Schema.Literal("completed"),
+     279           50 :   input: Schema.Record(Schema.String, Schema.Any),
+     280           24 :   output: Schema.String,
+     281           23 :   title: Schema.String,
+     282           53 :   metadata: Schema.Record(Schema.String, Schema.Any),
+     283           25 :   time: Schema.Struct({
+     284           26 :     start: NonNegativeInt,
+     285           24 :     end: NonNegativeInt,
+     286           44 :     compacted: Schema.optional(NonNegativeInt),
+     287            5 :   }),
+     288           53 :   attachments: Schema.optional(Schema.Array(FilePart)),
+     289           50 : }).annotate({ identifier: "ToolStateCompleted" })
+     290              : export type ToolStateCompleted = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateCompleted>>
+     291              : 
+     292           46 : export const ToolStateError = Schema.Struct({
+     293           34 :   status: Schema.Literal("error"),
+     294           50 :   input: Schema.Record(Schema.String, Schema.Any),
+     295           23 :   error: Schema.String,
+     296           70 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
+     297           25 :   time: Schema.Struct({
+     298           26 :     start: NonNegativeInt,
+     299           21 :     end: NonNegativeInt,
+     300            3 :   }),
+     301           46 : }).annotate({ identifier: "ToolStateError" })
+     302              : export type ToolStateError = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateError>>
+     303              : 
+     304           40 : export const ToolState = Schema.Union([
+     305           19 :   ToolStatePending,
+     306           19 :   ToolStateRunning,
+     307           21 :   ToolStateCompleted,
+     308           15 :   ToolStateError,
+     309           14 : ]).annotate({
+     310           26 :   discriminator: "status",
+     311           24 :   identifier: "ToolState",
+     312            3 : })
+     313              : export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError
+     314              : 
+     315           43 : export const ToolPart = Schema.Struct({
+     316           11 :   ...partBase,
+     317           31 :   type: Schema.Literal("tool"),
+     318           24 :   callID: Schema.String,
+     319           22 :   tool: Schema.String,
+     320           19 :   state: ToolState,
+     321           68 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
+     322           40 : }).annotate({ identifier: "ToolPart" })
+     323              : export type ToolPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof ToolPart>>, "state"> & {
+     324              :   state: ToolState
+     325              : }
+     326              : 
+     327           22 : const messageBase = {
+     328           16 :   id: MessageID,
+     329           30 :   sessionID: partBase.sessionID,
+     330            2 : }
+     331              : 
+     332           39 : export const User = Schema.Struct({
+     333           14 :   ...messageBase,
+     334           31 :   role: Schema.Literal("user"),
+     335           25 :   time: Schema.Struct({
+     336           20 :     created: Timestamp,
+     337            5 :   }),
+     338           34 :   format: Schema.optional(Format),
+     339           25 :   summary: Schema.optional(
+     340           19 :     Schema.Struct({
+     341           42 :       title: Schema.optional(Schema.String),
+     342           41 :       body: Schema.optional(Schema.String),
+     343           36 :       diffs: Schema.Array(FileDiff.Info),
+     344            2 :     }),
+     345            4 :   ),
+     346           23 :   agent: Schema.String,
+     347           26 :   model: Schema.Struct({
+     348           28 :     providerID: Provider.ID,
+     349           22 :     modelID: Model.ID,
+     350           41 :     variant: Schema.optional(Schema.String),
+     351            5 :   }),
+     352           41 :   system: Schema.optional(Schema.String),
+     353           69 :   tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
+     354           43 : }).annotate({ identifier: "UserMessage" })
+     355              : export type User = Types.DeepMutable<Schema.Schema.Type<typeof User>>
+     356              : 
+     357           35 : export const Part = Schema.Union([
+     358           11 :   TextPart,
+     359           14 :   SubtaskPart,
+     360           16 :   ReasoningPart,
+     361           11 :   FilePart,
+     362           11 :   ToolPart,
+     363           16 :   StepStartPart,
+     364           17 :   StepFinishPart,
+     365           15 :   SnapshotPart,
+     366           12 :   PatchPart,
+     367           12 :   AgentPart,
+     368           12 :   RetryPart,
+     369           15 :   CompactionPart,
+     370           59 : ]).annotate({ discriminator: "type", identifier: "Part" })
+     371              : export type Part =
+     372              :   | TextPart
+     373              :   | SubtaskPart
+     374              :   | ReasoningPart
+     375              :   | FilePart
+     376              :   | ToolPart
+     377              :   | StepStartPart
+     378              :   | StepFinishPart
+     379              :   | SnapshotPart
+     380              :   | PatchPart
+     381              :   | AgentPart
+     382              :   | RetryPart
+     383              :   | CompactionPart
+     384              : 
+     385           44 : const AssistantErrorSchema = Schema.Union([
+     386           25 :   AuthError.EffectSchema,
+     387          107 :   namedError("UnknownError", { message: Schema.String, ref: Schema.optional(Schema.String) }).EffectSchema,
+     388           33 :   OutputLengthError.EffectSchema,
+     389           28 :   AbortedError.EffectSchema,
+     390           37 :   StructuredOutputError.EffectSchema,
+     391           36 :   ContextOverflowError.EffectSchema,
+     392           34 :   ContentFilterError.EffectSchema,
+     393           22 :   APIError.EffectSchema,
+     394           39 : ]).annotate({ discriminator: "name" })
+     395              : type AssistantError = Schema.Schema.Type<typeof AssistantErrorSchema>
+     396              : 
+     397           45 : export const TextPartInput = Schema.Struct({
+     398           30 :   id: Schema.optional(PartID),
+     399           31 :   type: Schema.Literal("text"),
+     400           22 :   text: Schema.String,
+     401           45 :   synthetic: Schema.optional(Schema.Boolean),
+     402           43 :   ignored: Schema.optional(Schema.Boolean),
+     403           22 :   time: Schema.optional(
+     404           19 :     Schema.Struct({
+     405           26 :       start: NonNegativeInt,
+     406           38 :       end: Schema.optional(NonNegativeInt),
+     407            2 :     }),
+     408            4 :   ),
+     409           68 :   metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
+     410           45 : }).annotate({ identifier: "TextPartInput" })
+     411              : export type TextPartInput = Types.DeepMutable<Schema.Schema.Type<typeof TextPartInput>>
+     412              : 
+     413           45 : export const FilePartInput = Schema.Struct({
+     414           30 :   id: Schema.optional(PartID),
+     415           31 :   type: Schema.Literal("file"),
+     416           22 :   mime: Schema.String,
+     417           43 :   filename: Schema.optional(Schema.String),
+     418           21 :   url: Schema.String,
+     419           40 :   source: Schema.optional(FilePartSource),
+     420           45 : }).annotate({ identifier: "FilePartInput" })
+     421              : export type FilePartInput = Types.DeepMutable<Schema.Schema.Type<typeof FilePartInput>>
+     422              : 
+     423           46 : export const AgentPartInput = Schema.Struct({
+     424           30 :   id: Schema.optional(PartID),
+     425           32 :   type: Schema.Literal("agent"),
+     426           22 :   name: Schema.String,
+     427           24 :   source: Schema.optional(
+     428           19 :     Schema.Struct({
+     429           25 :       value: Schema.String,
+     430           26 :       start: NonNegativeInt,
+     431           21 :       end: NonNegativeInt,
+     432            2 :     }),
+     433            2 :   ),
+     434           46 : }).annotate({ identifier: "AgentPartInput" })
+     435              : export type AgentPartInput = Types.DeepMutable<Schema.Schema.Type<typeof AgentPartInput>>
+     436              : 
+     437           48 : export const SubtaskPartInput = Schema.Struct({
+     438           30 :   id: Schema.optional(PartID),
+     439           34 :   type: Schema.Literal("subtask"),
+     440           24 :   prompt: Schema.String,
+     441           29 :   description: Schema.String,
+     442           23 :   agent: Schema.String,
+     443           23 :   model: Schema.optional(
+     444           19 :     Schema.Struct({
+     445           28 :       providerID: Provider.ID,
+     446           19 :       modelID: Model.ID,
+     447            2 :     }),
+     448            4 :   ),
+     449           40 :   command: Schema.optional(Schema.String),
+     450           48 : }).annotate({ identifier: "SubtaskPartInput" })
+     451              : export type SubtaskPartInput = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPartInput>>
+     452              : 
+     453           44 : export const Assistant = Schema.Struct({
+     454           14 :   ...messageBase,
+     455           36 :   role: Schema.Literal("assistant"),
+     456           25 :   time: Schema.Struct({
+     457           28 :     created: NonNegativeInt,
+     458           44 :     completed: Schema.optional(NonNegativeInt),
+     459            5 :   }),
+     460           47 :   error: Schema.optional(AssistantErrorSchema),
+     461           22 :   parentID: MessageID,
+     462           20 :   modelID: Model.ID,
+     463           26 :   providerID: Provider.ID,
+     464           22 :   mode: Schema.String,
+     465           23 :   agent: Schema.String,
+     466           25 :   path: Schema.Struct({
+     467           23 :     cwd: Schema.String,
+     468           21 :     root: Schema.String,
+     469            5 :   }),
+     470           43 :   summary: Schema.optional(Schema.Boolean),
+     471           22 :   cost: Schema.Finite,
+     472           27 :   tokens: Schema.Struct({
+     473           42 :     total: Schema.optional(Schema.Finite),
+     474           25 :     input: Schema.Finite,
+     475           26 :     output: Schema.Finite,
+     476           29 :     reasoning: Schema.Finite,
+     477           28 :     cache: Schema.Struct({
+     478           26 :       read: Schema.Finite,
+     479           24 :       write: Schema.Finite,
+     480            4 :     }),
+     481            5 :   }),
+     482           42 :   structured: Schema.optional(Schema.Any),
+     483           42 :   variant: Schema.optional(Schema.String),
+     484           39 :   finish: Schema.optional(Schema.String),
+     485           48 : }).annotate({ identifier: "AssistantMessage" })
+     486              : export type Assistant = Omit<Types.DeepMutable<Schema.Schema.Type<typeof Assistant>>, "error"> & {
+     487              :   error?: AssistantError
+     488              : }
+     489              : 
+     490          111 : export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" })
+     491              : export type Info = User | Assistant
+     492              : 
+     493           41 : export const WithParts = Schema.Struct({
+     494           13 :   info: Info,
+     495           26 :   parts: Schema.Array(Part),
+     496            3 : })
+     497              : export type WithParts = {
+     498              :   info: Info
+     499              :   parts: Part[]
+     500              : }
+     501              : 
+     502           18 : const options = {
+     503           14 :   durable: {
+     504           27 :     aggregate: "sessionID",
+     505           12 :     version: 1,
+     506            2 :   },
+     507            2 : } as const
+     508              : 
+     509           39 : const SessionSummary = Schema.Struct({
+     510           27 :   additions: Schema.Finite,
+     511           27 :   deletions: Schema.Finite,
+     512           23 :   files: Schema.Finite,
+     513           45 :   diffs: optional(Schema.Array(FileDiff.Info)),
+     514            3 : })
+     515              : 
+     516           38 : const SessionTokens = Schema.Struct({
+     517           23 :   input: Schema.Finite,
+     518           24 :   output: Schema.Finite,
+     519           27 :   reasoning: Schema.Finite,
+     520           26 :   cache: Schema.Struct({
+     521           24 :     read: Schema.Finite,
+     522           22 :     write: Schema.Finite,
+     523            3 :   }),
+     524            3 : })
+     525              : 
+     526           37 : const SessionShare = Schema.Struct({
+     527           19 :   url: Schema.String,
+     528            3 : })
+     529              : 
+     530           38 : const SessionRevert = Schema.Struct({
+     531           23 :   messageID: MessageID,
+     532           27 :   partID: optional(PartID),
+     533           36 :   snapshot: optional(Schema.String),
+     534           30 :   diff: optional(Schema.String),
+     535            3 : })
+     536              : 
+     537           37 : const SessionModel = Schema.Struct({
+     538           15 :   id: Model.ID,
+     539           26 :   providerID: Provider.ID,
+     540           33 :   variant: optional(Schema.String),
+     541            3 : })
+     542              : 
+     543           43 : export const SessionInfo = Schema.Struct({
+     544           16 :   id: SessionID,
+     545           22 :   slug: Schema.String,
+     546           24 :   projectID: Project.ID,
+     547           37 :   workspaceID: optional(WorkspaceID),
+     548           27 :   directory: Schema.String,
+     549           32 :   path: optional(Schema.String),
+     550           32 :   parentID: optional(SessionID),
+     551           36 :   summary: optional(SessionSummary),
+     552           32 :   cost: optional(Schema.Finite),
+     553           34 :   tokens: optional(SessionTokens),
+     554           32 :   share: optional(SessionShare),
+     555           23 :   title: Schema.String,
+     556           33 :   agent: optional(Schema.String),
+     557           32 :   model: optional(SessionModel),
+     558           25 :   version: Schema.String,
+     559           63 :   metadata: optional(Schema.Record(Schema.String, Schema.Any)),
+     560           25 :   time: Schema.Struct({
+     561           28 :     created: NonNegativeInt,
+     562           28 :     updated: NonNegativeInt,
+     563           41 :     compacting: optional(NonNegativeInt),
+     564           35 :     archived: optional(Schema.Finite),
+     565            5 :   }),
+     566           45 :   permission: optional(PermissionV1.Ruleset),
+     567           32 :   revert: optional(SessionRevert),
+     568           39 : }).annotate({ identifier: "Session" })
+     569              : export type SessionInfo = typeof SessionInfo.Type
+     570              : 
+     571           17 : const events = {
+     572           21 :   Created: define({
+     573           31 :     type: "session.created",
+     574           12 :     ...options,
+     575           15 :     schema: {
+     576           27 :       sessionID: SessionID,
+     577           21 :       info: SessionInfo,
+     578            3 :     },
+     579            5 :   }),
+     580           21 :   Updated: define({
+     581           31 :     type: "session.updated",
+     582           12 :     ...options,
+     583           15 :     schema: {
+     584           27 :       sessionID: SessionID,
+     585           21 :       info: SessionInfo,
+     586            3 :     },
+     587            5 :   }),
+     588           21 :   Deleted: define({
+     589           31 :     type: "session.deleted",
+     590           12 :     ...options,
+     591           15 :     schema: {
+     592           27 :       sessionID: SessionID,
+     593           21 :       info: SessionInfo,
+     594            3 :     },
+     595            5 :   }),
+     596           28 :   MessageUpdated: define({
+     597           31 :     type: "message.updated",
+     598           12 :     ...options,
+     599           15 :     schema: {
+     600           27 :       sessionID: SessionID,
+     601           14 :       info: Info,
+     602            3 :     },
+     603            5 :   }),
+     604           28 :   MessageRemoved: define({
+     605           31 :     type: "message.removed",
+     606           12 :     ...options,
+     607           15 :     schema: {
+     608           27 :       sessionID: SessionID,
+     609           24 :       messageID: MessageID,
+     610            3 :     },
+     611            5 :   }),
+     612           25 :   PartUpdated: define({
+     613           36 :     type: "message.part.updated",
+     614           12 :     ...options,
+     615           15 :     schema: {
+     616           27 :       sessionID: SessionID,
+     617           17 :       part: Part,
+     618           23 :       time: Schema.Finite,
+     619            3 :     },
+     620            5 :   }),
+     621           25 :   PartRemoved: define({
+     622           36 :     type: "message.part.removed",
+     623           12 :     ...options,
+     624           15 :     schema: {
+     625           27 :       sessionID: SessionID,
+     626           27 :       messageID: MessageID,
+     627           18 :       partID: PartID,
+     628            3 :     },
+     629            3 :   }),
+     630            2 : }
+     631              : 
+     632           34 : export const PartDelta = define({
+     633           29 :   type: "message.part.delta",
+     634           13 :   schema: {
+     635           25 :     sessionID: SessionID,
+     636           25 :     messageID: MessageID,
+     637           19 :     partID: PartID,
+     638           25 :     field: Schema.String,
+     639           22 :     delta: Schema.String,
+     640            2 :   },
+     641            3 : })
+     642              : 
+     643           29 : export const Diff = define({
+     644           23 :   type: "session.diff",
+     645           13 :   schema: {
+     646           25 :     sessionID: SessionID,
+     647           35 :     diff: Schema.Array(FileDiff.Info),
+     648            2 :   },
+     649            3 : })
+     650              : 
+     651           30 : export const Error = define({
+     652           24 :   type: "session.error",
+     653           13 :   schema: {
+     654           42 :     sessionID: Schema.optional(SessionID),
+     655           31 :     error: Assistant.fields.error,
+     656            2 :   },
+     657            3 : })
+     658              : 
+     659           26 : export const Event = {
+     660            9 :   ...events,
+     661           12 :   PartDelta,
+     662            7 :   Diff,
+     663            8 :   Error,
+     664           23 :   Definitions: inventory(
+     665           16 :     events.Created,
+     666           16 :     events.Updated,
+     667           16 :     events.Deleted,
+     668           23 :     events.MessageUpdated,
+     669           23 :     events.MessageRemoved,
+     670           20 :     events.PartUpdated,
+     671           20 :     events.PartRemoved,
+     672           11 :     PartDelta,
+     673            6 :     Diff,
+     674            5 :     Error,
+     675            2 :   ),
+     676            1 : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/workspace-event.ts.gcov.html b/packages/core/schema/src/workspace-event.ts.gcov.html new file mode 100644 index 00000000..92003776 --- /dev/null +++ b/packages/core/schema/src/workspace-event.ts.gcov.html @@ -0,0 +1,108 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/workspace-event.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - workspace-event.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %2525
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           52 : export * as WorkspaceEvent from "./workspace-event"
+       2              : 
+       3           32 : import { Schema } from "effect"
+       4           32 : import { Event } from "./event"
+       5           45 : import { WorkspaceID } from "./workspace-id"
+       6              : 
+       7           48 : export const ConnectionStatus = Schema.Struct({
+       8           27 :   workspaceID: WorkspaceID,
+       9           78 :   status: Schema.Literals(["connected", "connecting", "disconnected", "error"]),
+      10           63 : }).annotate({ identifier: "WorkspaceEvent.ConnectionStatus" })
+      11              : export interface ConnectionStatus extends Schema.Schema.Type<typeof ConnectionStatus> {}
+      12              : 
+      13           36 : export const Ready = Event.define({
+      14           26 :   type: "workspace.ready",
+      15           13 :   schema: {
+      16           21 :     name: Schema.String,
+      17            2 :   },
+      18            3 : })
+      19              : 
+      20           37 : export const Failed = Event.define({
+      21           27 :   type: "workspace.failed",
+      22           13 :   schema: {
+      23           24 :     message: Schema.String,
+      24            2 :   },
+      25            3 : })
+      26              : 
+      27           37 : export const Status = Event.define({
+      28           27 :   type: "workspace.status",
+      29           32 :   schema: ConnectionStatus.fields,
+      30            3 : })
+      31              : 
+      32           65 : export const Definitions = Event.inventory(Ready, Failed, Status)
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/workspace-id.ts.gcov.html b/packages/core/schema/src/workspace-id.ts.gcov.html new file mode 100644 index 00000000..f25afc3e --- /dev/null +++ b/packages/core/schema/src/workspace-id.ts.gcov.html @@ -0,0 +1,95 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/workspace-id.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - workspace-id.tsCoverageTotalHit
Test:opencode-lcov.infoLines:93.8 %1615
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           32 : import { Schema } from "effect"
+       2           41 : import { ascending } from "./identifier"
+       3           35 : import { statics } from "./schema"
+       4              : 
+       5           79 : export const WorkspaceID = Schema.String.check(Schema.isStartsWith("wrk")).pipe(
+       6           32 :   Schema.brand("WorkspaceV2.ID"),
+       7           22 :   statics((schema) => {
+       8           55 :     const create = () => schema.make("wrk_" + ascending())
+       9           12 :     return {
+      10           25 :       ascending: (id?: string) => {
+      11           30 :         if (!id) return create()
+      12            0 :         if (!id.startsWith("wrk")) throw new Error(`ID ${id} does not start with wrk`)
+      13            5 :         return schema.make(id)
+      14              :       },
+      15            8 :       create,
+      16            1 :     }
+      17            1 :   }),
+      18            2 : )
+      19              : export type WorkspaceID = typeof WorkspaceID.Type
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/core/schema/src/workspace.ts.gcov.html b/packages/core/schema/src/workspace.ts.gcov.html new file mode 100644 index 00000000..654f3cae --- /dev/null +++ b/packages/core/schema/src/workspace.ts.gcov.html @@ -0,0 +1,85 @@ + + + + + + + LCOV - opencode-lcov.info - ../schema/src/workspace.ts + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../schema/src - workspace.tsCoverageTotalHit
Test:opencode-lcov.infoLines:100.0 %55
Test Date:2026-08-28 03:27:30Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           41 : export * as Workspace from "./workspace"
+       2              : 
+       3           51 : import { WorkspaceEvent } from "./workspace-event"
+       4           45 : import { WorkspaceID } from "./workspace-id"
+       5              : 
+       6           30 : export const ID = WorkspaceID
+       7              : export type ID = WorkspaceID
+       8              : 
+       9           35 : export const Event = WorkspaceEvent
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/packages/ui/src/i18n/it.ts b/packages/ui/src/i18n/it.ts index 73b0461e..8ab8ef80 100644 --- a/packages/ui/src/i18n/it.ts +++ b/packages/ui/src/i18n/it.ts @@ -1,4 +1,8 @@ -export const dict: Record = { +import { dict as en } from "./en" + +type Keys = keyof typeof en + +const sessionReview = { "ui.sessionReview.title": "Modifiche della sessione", "ui.sessionReview.title.git": "Modifiche Git", "ui.sessionReview.title.branch": "Modifiche del branch", @@ -34,7 +38,11 @@ export const dict: Record = { "ui.sessionReviewV2.empty.changes.description": "Le modifiche al progetto verranno visualizzate qui", "ui.sessionReview.openFile": "Apri file", "ui.sessionReview.selection.line": "riga {{line}}", - "ui.sessionReview.selection.lines": "righe {{start}}-{{end}}", + "ui.sessionReview.selection.lines": "righe {{start}}-{{end}}" +} + +export const dict = { + ...sessionReview, "ui.fileMedia.kind.image": "immagine", "ui.fileMedia.kind.audio": "audio", "ui.fileMedia.state.removed": "File {{kind}} rimosso.", @@ -199,4 +207,4 @@ export const dict: Record = { "ui.question.multiHint": "Seleziona tutte le risposte pertinenti", "ui.question.singleHint": "Seleziona una risposta", "ui.question.custom.placeholder": "Digita la tua risposta...", -} +} satisfies Partial>