chore: merge dev into v2 (#34317)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com>
This commit is contained in:
parent
11bf8d8a42
commit
41283933ff
208 changed files with 9815 additions and 6651 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
|||
import { AbsolutePath, type DeepMutable } from "@opencode-ai/core/schema"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
import { Reference } from "@opencode-ai/core/reference"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
|
|
@ -93,7 +93,7 @@ export const layer = Layer.effect(
|
|||
const plugin = yield* Plugin.Service
|
||||
const skill = yield* Skill.Service
|
||||
const provider = yield* Provider.Service
|
||||
const locations = yield* LocationServiceMap
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
|
||||
const state = yield* InstanceState.make<State>(
|
||||
Effect.fn("Agent.state")(function* (ctx) {
|
||||
|
|
@ -444,10 +444,14 @@ export const defaultLayer = layer.pipe(
|
|||
Layer.provide(Auth.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
)
|
||||
|
||||
const locationServiceMapNode = LayerNode.make({ service: Service, layer: LocationServiceMap.layer, deps: [] })
|
||||
const locationServiceMapNode = LayerNode.make({
|
||||
service: LocationServiceMap.Service,
|
||||
layer: locationServiceMapLayer,
|
||||
deps: [],
|
||||
})
|
||||
|
||||
export const node = LayerNode.make({
|
||||
service: Service,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { EOL } from "os"
|
||||
import { Effect } from "effect"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
|
||||
import { effectCmd } from "../../effect-cmd"
|
||||
|
|
@ -9,8 +9,8 @@ import { cmd } from "../cmd"
|
|||
|
||||
const filesystem = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||
effect.pipe(
|
||||
Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(process.cwd()) }))),
|
||||
Effect.provide(LocationServiceMap.layer),
|
||||
Effect.provide(LocationServiceMap.Service.get(Location.Ref.make({ directory: AbsolutePath.make(process.cwd()) }))),
|
||||
Effect.provide(locationServiceMapLayer),
|
||||
)
|
||||
|
||||
const FileSearchCommand = effectCmd({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { EOL } from "os"
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { effectCmd } from "../../effect-cmd"
|
||||
|
|
@ -31,12 +31,12 @@ export const V2Command = effectCmd({
|
|||
}).pipe(
|
||||
Effect.withSpan("Cli.debug.v2"),
|
||||
Effect.provide(
|
||||
LocationServiceMap.get(
|
||||
LocationServiceMap.Service.get(
|
||||
Location.Ref.make({
|
||||
directory: AbsolutePath.make(process.cwd()),
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.provide(LocationServiceMap.layer),
|
||||
Effect.provide(locationServiceMapLayer),
|
||||
),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { Context, Effect, FiberMap, Iterable, Layer, Schema, Stream } from "effect"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { FetchHttpClient, HttpBody, HttpClient, HttpClientError, HttpClientRequest } from "effect/unstable/http"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { Effect, Layer, Schema, Context, Stream } from "effect"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { McpAuth } from "./auth"
|
|||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { TuiEvent } from "@/server/tui-event"
|
||||
import open from "open"
|
||||
import { Cause, Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect"
|
||||
import { Cause, Effect, Exit, Layer, Context, Schema, Stream } from "effect"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
|
|
@ -928,16 +928,17 @@ export const layer = Layer.effect(
|
|||
const pending = pendingOAuthTransports.get(mcpName)
|
||||
if (!pending) throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`)
|
||||
|
||||
const result = yield* Effect.tryPromise({
|
||||
try: () => pending.transport.finishAuth(authorizationCode).then(() => true as const),
|
||||
catch: (error) => {
|
||||
return error
|
||||
},
|
||||
}).pipe(Effect.option)
|
||||
const error = yield* Effect.tryPromise({
|
||||
try: () => pending.transport.finishAuth(authorizationCode),
|
||||
catch: (error) => error,
|
||||
}).pipe(
|
||||
Effect.match({
|
||||
onFailure: (error) => (error instanceof Error ? error.message : String(error)),
|
||||
onSuccess: () => undefined,
|
||||
}),
|
||||
)
|
||||
|
||||
if (Option.isNone(result)) {
|
||||
return { status: "failed", error: "OAuth completion failed" } satisfies Status
|
||||
}
|
||||
if (error) return { status: "failed", error: `OAuth completion failed: ${error}` } satisfies Status
|
||||
|
||||
yield* Effect.promise(() => pending.provider?.commit() ?? Promise.resolve())
|
||||
yield* auth.clearCodeVerifier(mcpName)
|
||||
|
|
|
|||
|
|
@ -1934,7 +1934,8 @@ export const layer = Layer.effect(
|
|||
return { providerID: entry.providerID, modelID: entry.modelID }
|
||||
}
|
||||
|
||||
const provider = Object.values(s.providers).find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id))
|
||||
const configured = Object.keys(cfg.provider ?? {})
|
||||
const provider = Object.values(s.providers).find((p) => configured.length === 0 || configured.includes(p.id))
|
||||
if (!provider) return yield* new NoProvidersError()
|
||||
const [model] = sort(Object.values(provider.models))
|
||||
if (!model) return yield* new NoModelsError({ providerID: provider.id })
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as InstanceState from "@/effect/instance-state"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
|
|
@ -14,7 +14,7 @@ import { InstanceHttpApi } from "../api"
|
|||
export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const ripgrep = yield* Ripgrep.Service
|
||||
const locations = yield* LocationServiceMap
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
|
||||
const filesystem = Effect.fnUntraced(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
|
||||
return yield* effect.pipe(
|
||||
|
|
@ -136,4 +136,4 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
|
|||
.handle("content", content)
|
||||
.handle("status", status)
|
||||
}),
|
||||
).pipe(Layer.provide(LocationServiceMap.layer))
|
||||
).pipe(Layer.provide(locationServiceMapLayer))
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Pty } from "@opencode-ai/core/pty"
|
|||
import { PtyProtocol } from "@opencode-ai/core/pty/protocol"
|
||||
import { PtyID } from "@opencode-ai/core/pty/schema"
|
||||
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Shell } from "@opencode-ai/core/shell"
|
||||
|
|
@ -43,7 +43,7 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler
|
|||
const tickets = yield* PtyTicket.Service
|
||||
const cors = yield* CorsConfig
|
||||
const plugin = yield* Plugin.Service
|
||||
const locations = yield* LocationServiceMap
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
const unregister = registerDisposer((directory) =>
|
||||
Effect.runPromise(locations.invalidate(Location.Ref.make({ directory: AbsolutePath.make(directory) }))),
|
||||
)
|
||||
|
|
@ -158,13 +158,13 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler
|
|||
.handle("remove", remove)
|
||||
.handle("connectToken", connectToken)
|
||||
}),
|
||||
).pipe(Layer.provide(LocationServiceMap.layer))
|
||||
).pipe(Layer.provide(locationServiceMapLayer))
|
||||
|
||||
export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-connect", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const tickets = yield* PtyTicket.Service
|
||||
const cors = yield* CorsConfig
|
||||
const locations = yield* LocationServiceMap
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
const unregister = registerDisposer((directory) =>
|
||||
Effect.runPromise(locations.invalidate(Location.Ref.make({ directory: AbsolutePath.make(directory) }))),
|
||||
)
|
||||
|
|
@ -270,4 +270,4 @@ export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-conne
|
|||
}),
|
||||
)
|
||||
}),
|
||||
).pipe(Layer.provide(LocationServiceMap.layer))
|
||||
).pipe(Layer.provide(locationServiceMapLayer))
|
||||
|
|
|
|||
|
|
@ -52,15 +52,18 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
|
|||
import { MoveSession } from "@opencode-ai/core/control-plane/move-session"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { ProjectCopy } from "@opencode-ai/core/project/copy"
|
||||
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
||||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import * as SessionExecutionLocal from "@opencode-ai/core/session/execution/local"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@opencode-ai/server/cors"
|
||||
import { serveUIEffect } from "@/server/shared/ui"
|
||||
|
|
@ -95,6 +98,10 @@ import { sessionHandlers } from "./handlers/session"
|
|||
import { syncHandlers } from "./handlers/sync"
|
||||
import { tuiHandlers } from "./handlers/tui"
|
||||
import { handlers } from "@opencode-ai/server/handlers"
|
||||
import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
import { layer as locationLayer } from "@opencode-ai/server/location"
|
||||
import { sessionLocationLayer } from "@opencode-ai/server/middleware/session-location"
|
||||
import { PtyEnvironment } from "@opencode-ai/server/pty-environment"
|
||||
import { schemaErrorLayer as v2SchemaErrorLayer } from "@opencode-ai/server/middleware/schema-error"
|
||||
import { workspaceHandlers } from "./handlers/workspace"
|
||||
import { instanceContextLayer } from "./middleware/instance-context"
|
||||
|
|
@ -221,6 +228,7 @@ const app = LayerNode.group([
|
|||
Discovery.node,
|
||||
Question.node,
|
||||
Permission.node,
|
||||
PermissionSaved.node,
|
||||
Todo.node,
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
|
|
@ -279,9 +287,21 @@ export function createRoutes(
|
|||
MoveSession.defaultLayer,
|
||||
HttpServer.layerServices,
|
||||
]),
|
||||
Layer.provide(LayerNode.buildLayer(app)),
|
||||
Layer.provide(Layer.succeed(CorsConfig)(corsOptions)),
|
||||
Layer.provideMerge(Observability.layer),
|
||||
|
||||
Layer.provide(sessionLocationLayer),
|
||||
Layer.provide(locationLayer),
|
||||
Layer.provide(PtyEnvironment.layer),
|
||||
Layer.provide(
|
||||
SessionV2.defaultLayer.pipe(
|
||||
Layer.provide(SessionExecutionLocal.defaultLayer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
),
|
||||
),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
|
||||
Layer.provide(LayerNode.compile(app)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import path from "path"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { llmClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { llmClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
|
|
|
|||
|
|
@ -36,8 +36,6 @@ import type { SystemError } from "bun"
|
|||
import type { Provider } from "@/provider/provider"
|
||||
import { Effect, Schema } from "effect"
|
||||
|
||||
export const node = LayerNode.group([Database.node])
|
||||
|
||||
/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
|
||||
interface FetchDecompressionError extends Error {
|
||||
code: "ZlibError"
|
||||
|
|
@ -733,3 +731,4 @@ export function fromError(
|
|||
}
|
||||
|
||||
export * as MessageV2 from "./message-v2"
|
||||
export const node = LayerNode.group([Database.node])
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { makeRuntime } from "@opencode-ai/core/effect/runtime"
|
|||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import * as SessionExecutionLocal from "@opencode-ai/core/session/execution/local"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
|
||||
import { NotFoundError } from "@/storage/storage"
|
||||
import { eq } from "drizzle-orm"
|
||||
|
|
@ -947,7 +947,7 @@ export const defaultLayer = layer.pipe(
|
|||
Layer.provide(
|
||||
SessionV2.defaultLayer.pipe(
|
||||
Layer.provide(SessionExecutionLocal.defaultLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
),
|
||||
),
|
||||
Layer.provide(RuntimeFlags.defaultLayer),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { Permission } from "@/permission"
|
|||
import { Skill } from "@/skill"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
import { Reference } from "@opencode-ai/core/reference"
|
||||
import { MCP } from "@/mcp"
|
||||
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
||||
|
|
@ -52,7 +52,7 @@ export const layer = Layer.effect(
|
|||
Effect.gen(function* () {
|
||||
const skill = yield* Skill.Service
|
||||
const mcp = yield* MCP.Service
|
||||
const locations = yield* LocationServiceMap
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
|
||||
return Service.of({
|
||||
environment: Effect.fn("SystemPrompt.environment")(function* (model: Provider.Model) {
|
||||
|
|
@ -131,10 +131,14 @@ export const layer = Layer.effect(
|
|||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
Layer.provide(MCP.defaultLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
)
|
||||
|
||||
const locationServiceMapNode = LayerNode.make({ service: Service, layer: LocationServiceMap.layer, deps: [] })
|
||||
const locationServiceMapNode = LayerNode.make({
|
||||
service: LocationServiceMap.Service,
|
||||
layer: locationServiceMapLayer,
|
||||
deps: [],
|
||||
})
|
||||
|
||||
export const node = LayerNode.make({
|
||||
service: Service,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import type * as SDK from "@opencode-ai/sdk/v2"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { Effect, Exit, Layer, Option, Schema, Scope, Context, Stream } from "effect"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient, path } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { httpClient, path } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { NodePath } from "@effect/platform-node"
|
||||
import { Effect, Layer, Path, Schema, Context } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||
import { PlanExitTool } from "./plan"
|
||||
import { Session } from "@/session/session"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { path } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { path } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { InstanceLayer } from "@/project/instance-layer"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { Plugin } from "../../src/plugin"
|
|||
import { Provider } from "../../src/provider/provider"
|
||||
import { Skill } from "../../src/skill"
|
||||
import { Truncate } from "../../src/tool/truncate"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
|
||||
const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||
Agent.layer.pipe(
|
||||
|
|
@ -23,7 +23,7 @@ const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
|||
Layer.provide(Auth.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
Layer.provide(RuntimeFlags.layer(flags)),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { expect } from "bun:test"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import path from "path"
|
||||
|
|
@ -44,7 +44,7 @@ const agentLayer = Agent.layer.pipe(
|
|||
Layer.provide(SkillTest.empty),
|
||||
Layer.provide(provider.layer),
|
||||
Layer.provide(pluginLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
import { test } from "bun:test"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
|
||||
class A extends Context.Service<A, {}>()("test/TierA") {}
|
||||
class B extends Context.Service<B, {}>()("test/TierB") {}
|
||||
|
||||
const tiers = LayerNode.tiers(["request", "global"])
|
||||
const request = tiers.make("request")
|
||||
const global = tiers.make("global")
|
||||
const globalA = global({ service: A, layer: Layer.succeed(A, A.of({})), deps: [] })
|
||||
const bLayer = Layer.effect(B, Effect.as(A, B.of({})))
|
||||
|
||||
request({ service: B, layer: bLayer, deps: [globalA] })
|
||||
|
||||
// @ts-expect-error Global cannot depend on request
|
||||
global({ service: B, layer: bLayer, deps: [request({ service: A, layer: Layer.succeed(A, A.of({})), deps: [] })] })
|
||||
|
||||
test("type exploration compiles", () => {})
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
|
||||
class Value extends Context.Service<Value, { readonly value: string }>()("test/TierValue") {}
|
||||
class Result extends Context.Service<Result, { readonly value: string }>()("test/TierResult") {}
|
||||
class Left extends Context.Service<Left, { readonly value: string }>()("test/TierLeft") {}
|
||||
class Right extends Context.Service<Right, { readonly value: string }>()("test/TierRight") {}
|
||||
class Last extends Context.Service<Last, { readonly value: string }>()("test/TierLast") {}
|
||||
|
||||
test("builds tiers with a custom builder", async () => {
|
||||
let locationBuilds = 0
|
||||
const tiers = LayerNode.tiers(["location", "global"])
|
||||
const global = tiers.make("global")
|
||||
const location = tiers.make("location")
|
||||
const value = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "value" })), deps: [] })
|
||||
const result = location({
|
||||
service: Result,
|
||||
layer: Layer.effect(
|
||||
Result,
|
||||
Effect.gen(function* () {
|
||||
return Result.of({ value: (yield* Value).value })
|
||||
}),
|
||||
),
|
||||
deps: [value],
|
||||
})
|
||||
const layer = LayerNode.buildLayer(LayerNode.group([result]), {
|
||||
tiers,
|
||||
buildTier: (tier, layers) => {
|
||||
if (tier !== "location") return LayerNode.combine(layers)
|
||||
locationBuilds++
|
||||
return LayerNode.combine(layers).pipe(Layer.fresh)
|
||||
},
|
||||
})
|
||||
const program = Effect.gen(function* () {
|
||||
return (yield* Result).value
|
||||
}).pipe(Effect.provide(layer))
|
||||
|
||||
expect(await Effect.runPromise(program)).toBe("value")
|
||||
expect(locationBuilds).toBe(1)
|
||||
})
|
||||
|
||||
test("rejects conflicting higher-tier service implementations", () => {
|
||||
const tiers = LayerNode.tiers(["location", "global"])
|
||||
const global = tiers.make("global")
|
||||
const location = tiers.make("location")
|
||||
const first = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "first" })), deps: [] })
|
||||
const second = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "second" })), deps: [] })
|
||||
const left = location({
|
||||
service: Left,
|
||||
layer: Layer.effect(Left, Effect.as(Value, Left.of({ value: "left" }))),
|
||||
deps: [first],
|
||||
})
|
||||
const right = location({
|
||||
service: Right,
|
||||
layer: Layer.effect(Right, Effect.as(Value, Right.of({ value: "right" }))),
|
||||
deps: [second],
|
||||
})
|
||||
|
||||
expect(() => LayerNode.buildLayer(LayerNode.group([left, right]), { tiers })).toThrow(
|
||||
"conflicting implementations for test/TierValue",
|
||||
)
|
||||
})
|
||||
|
||||
test("validates tier dependencies through groups", () => {
|
||||
const tiers = LayerNode.tiers(["location", "global"])
|
||||
const global = tiers.make("global")
|
||||
const location = tiers.make("location")
|
||||
const local = location({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "local" })), deps: [] })
|
||||
const invalid = global({
|
||||
service: Result,
|
||||
layer: Layer.effect(
|
||||
Result,
|
||||
Effect.map(Value, (value) => Result.of({ value: value.value })),
|
||||
),
|
||||
deps: [LayerNode.group([local])],
|
||||
})
|
||||
|
||||
expect(() => LayerNode.buildLayer(invalid, { tiers })).toThrow("Tier global cannot depend on lower tier location")
|
||||
})
|
||||
|
||||
test("validates shared groups in each consumer tier", () => {
|
||||
const tiers = LayerNode.tiers(["location", "global"])
|
||||
const global = tiers.make("global")
|
||||
const location = tiers.make("location")
|
||||
const local = location({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "local" })), deps: [] })
|
||||
const shared = LayerNode.group([local])
|
||||
const valid = location({
|
||||
service: Left,
|
||||
layer: Layer.effect(
|
||||
Left,
|
||||
Effect.map(Value, (value) => Left.of({ value: value.value })),
|
||||
),
|
||||
deps: [shared],
|
||||
})
|
||||
const invalid = global({
|
||||
service: Result,
|
||||
layer: Layer.effect(
|
||||
Result,
|
||||
Effect.map(Value, (value) => Result.of({ value: value.value })),
|
||||
),
|
||||
deps: [shared],
|
||||
})
|
||||
|
||||
expect(() => LayerNode.buildLayer(LayerNode.group([valid, invalid]), { tiers })).toThrow(
|
||||
"Tier global cannot depend on lower tier location",
|
||||
)
|
||||
})
|
||||
|
||||
test("rejects a service assigned to multiple tiers", () => {
|
||||
const tiers = LayerNode.tiers(["location", "global"])
|
||||
const global = tiers.make("global")
|
||||
const location = tiers.make("location")
|
||||
const local = location({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "local" })), deps: [] })
|
||||
const shared = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "global" })), deps: [] })
|
||||
|
||||
expect(() => LayerNode.buildLayer(LayerNode.group([local, shared]), { tiers })).toThrow(
|
||||
"Service test/TierValue belongs to both tier location and tier global",
|
||||
)
|
||||
})
|
||||
|
||||
test("rebinds same-tier providers without reacquiring them", async () => {
|
||||
let firstAcquisitions = 0
|
||||
const tiers = LayerNode.tiers(["global"])
|
||||
const global = tiers.make("global")
|
||||
const first = global({
|
||||
service: Value,
|
||||
layer: Layer.effect(
|
||||
Value,
|
||||
Effect.sync(() => {
|
||||
firstAcquisitions++
|
||||
return Value.of({ value: "first" })
|
||||
}),
|
||||
),
|
||||
deps: [],
|
||||
})
|
||||
const second = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "second" })), deps: [] })
|
||||
const left = global({
|
||||
service: Left,
|
||||
layer: Layer.effect(
|
||||
Left,
|
||||
Effect.map(Value, (value) => Left.of({ value: value.value })),
|
||||
),
|
||||
deps: [first],
|
||||
})
|
||||
const right = global({
|
||||
service: Right,
|
||||
layer: Layer.effect(
|
||||
Right,
|
||||
Effect.map(Value, (value) => Right.of({ value: value.value })),
|
||||
),
|
||||
deps: [second],
|
||||
})
|
||||
const last = global({
|
||||
service: Last,
|
||||
layer: Layer.effect(
|
||||
Last,
|
||||
Effect.map(Value, (value) => Last.of({ value: value.value })),
|
||||
),
|
||||
deps: [first],
|
||||
})
|
||||
const layer = LayerNode.buildLayer(LayerNode.group([left, right, last]), { tiers })
|
||||
const values = Effect.gen(function* () {
|
||||
return [(yield* Left).value, (yield* Right).value, (yield* Last).value]
|
||||
}).pipe(Effect.provide(layer))
|
||||
|
||||
expect(await Effect.runPromise(values)).toEqual(["first", "second", "first"])
|
||||
expect(firstAcquisitions).toBe(1)
|
||||
})
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import { test } from "bun:test"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
|
||||
class A extends Context.Service<A, {}>()("test/LayerNodeA") {}
|
||||
class B extends Context.Service<B, {}>()("test/LayerNodeB") {}
|
||||
class C extends Context.Service<C, {}>()("test/LayerNodeC") {}
|
||||
class LayerError {
|
||||
readonly _tag = "LayerError"
|
||||
}
|
||||
class OtherError {
|
||||
readonly _tag = "OtherError"
|
||||
}
|
||||
|
||||
const tiers = LayerNode.tiers(["app"])
|
||||
const make = tiers.make("app")
|
||||
const aLayer = Layer.succeed(A, A.of({}))
|
||||
const bLayer = Layer.effect(B, Effect.as(A, B.of({})))
|
||||
const cLayer = Layer.effect(
|
||||
C,
|
||||
Effect.gen(function* () {
|
||||
yield* A
|
||||
yield* B
|
||||
return C.of({})
|
||||
}),
|
||||
)
|
||||
const failingA = Layer.effect(A, Effect.fail(new LayerError()))
|
||||
const a = make({ service: A, layer: aLayer, deps: [] })
|
||||
const b = make({ service: B, layer: bLayer, deps: [a] })
|
||||
const c = make({ service: C, layer: cLayer, deps: [a, b] })
|
||||
const failing = make({ service: A, layer: failingA, deps: [] })
|
||||
const dependent = make({ service: B, layer: bLayer, deps: [failing] })
|
||||
|
||||
make({ name: "manual-a", layer: aLayer, deps: [] })
|
||||
|
||||
// @ts-expect-error A node must have a service or name
|
||||
make({ layer: aLayer, deps: [] })
|
||||
|
||||
// @ts-expect-error Service and name are mutually exclusive
|
||||
make({ service: A, name: "a", layer: aLayer, deps: [] })
|
||||
|
||||
// @ts-expect-error B requires A
|
||||
make({ service: B, layer: bLayer, deps: [] })
|
||||
|
||||
// @ts-expect-error C requires A and B
|
||||
make({ service: C, layer: cLayer, deps: [a] })
|
||||
|
||||
const closed = LayerNode.buildLayer(c, { tiers })
|
||||
const closedWithError = LayerNode.buildLayer(dependent, { tiers })
|
||||
const checkClosed: Layer.Layer<C, never, never> = closed
|
||||
const checkError: Layer.Layer<B, LayerError, never> = closedWithError
|
||||
void checkClosed
|
||||
void checkError
|
||||
|
||||
LayerNode.replace(aLayer, Layer.succeed(A, A.of({})))
|
||||
|
||||
// @ts-expect-error Replacement must provide A
|
||||
LayerNode.replace(aLayer, Layer.succeed(B, B.of({})))
|
||||
|
||||
// @ts-expect-error Replacement cannot introduce a new error
|
||||
LayerNode.replace(aLayer, Layer.effect(A, Effect.fail(new OtherError())))
|
||||
|
||||
// @ts-expect-error Replacement must be closed
|
||||
LayerNode.replace(bLayer, bLayer)
|
||||
|
||||
test("type exploration compiles", () => {})
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
|
||||
class Value extends Context.Service<Value, { readonly value: string }>()("test/LayerNodeValue") {}
|
||||
class Greeting extends Context.Service<Greeting, { readonly value: string }>()("test/LayerNodeGreeting") {}
|
||||
class Left extends Context.Service<Left, { readonly value: string }>()("test/LayerNodeLeft") {}
|
||||
class Right extends Context.Service<Right, { readonly value: string }>()("test/LayerNodeRight") {}
|
||||
|
||||
const tiers = LayerNode.tiers(["app"])
|
||||
const make = tiers.make("app")
|
||||
const valueLayer = Layer.succeed(Value, Value.of({ value: "production" }))
|
||||
const greetingLayer = Layer.effect(
|
||||
Greeting,
|
||||
Effect.map(Value, (value) => Greeting.of({ value: `hello ${value.value}` })),
|
||||
)
|
||||
const value = make({ service: Value, layer: valueLayer, deps: [] })
|
||||
const greeting = make({ service: Greeting, layer: greetingLayer, deps: [value] })
|
||||
|
||||
describe("layer node", () => {
|
||||
test("builds an untiered graph", async () => {
|
||||
const value = LayerNode.make({ service: Value, layer: valueLayer, deps: [] })
|
||||
const greeting = LayerNode.make({ service: Greeting, layer: greetingLayer, deps: [value] })
|
||||
const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(LayerNode.buildLayer(greeting)))
|
||||
expect(await Effect.runPromise(program)).toBe("hello production")
|
||||
})
|
||||
|
||||
test("builds a dependency graph", async () => {
|
||||
const program = Effect.map(Greeting, (item) => item.value).pipe(
|
||||
Effect.provide(LayerNode.buildLayer(greeting, { tiers })),
|
||||
)
|
||||
expect(await Effect.runPromise(program)).toBe("hello production")
|
||||
})
|
||||
|
||||
test("replaces a layer by identity", async () => {
|
||||
const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
|
||||
const program = Effect.map(Greeting, (item) => item.value).pipe(
|
||||
Effect.provide(
|
||||
LayerNode.buildLayer(greeting, { tiers, replacements: [LayerNode.replace(valueLayer, replacement)] }),
|
||||
),
|
||||
)
|
||||
expect(await Effect.runPromise(program)).toBe("hello simulation")
|
||||
})
|
||||
|
||||
test("replaces every use of the same layer", async () => {
|
||||
const leftLayer = Layer.effect(
|
||||
Left,
|
||||
Effect.map(Value, (item) => Left.of({ value: item.value })),
|
||||
)
|
||||
const rightLayer = Layer.effect(
|
||||
Right,
|
||||
Effect.map(Value, (item) => Right.of({ value: item.value })),
|
||||
)
|
||||
const left = make({ service: Left, layer: leftLayer, deps: [value] })
|
||||
const right = make({ service: Right, layer: rightLayer, deps: [value] })
|
||||
const replacement = Layer.succeed(Value, Value.of({ value: "replaced" }))
|
||||
const layer = LayerNode.buildLayer(LayerNode.group([left, right]), {
|
||||
tiers,
|
||||
replacements: [LayerNode.replace(valueLayer, replacement)],
|
||||
})
|
||||
const program = Effect.gen(function* () {
|
||||
return [(yield* Left).value, (yield* Right).value]
|
||||
}).pipe(Effect.provide(layer))
|
||||
expect(await Effect.runPromise(program)).toEqual(["replaced", "replaced"])
|
||||
})
|
||||
|
||||
test("does not acquire an unused replacement", async () => {
|
||||
let acquisitions = 0
|
||||
const other = Layer.succeed(Value, Value.of({ value: "other" }))
|
||||
const replacement = Layer.effect(
|
||||
Value,
|
||||
Effect.sync(() => {
|
||||
acquisitions++
|
||||
return Value.of({ value: "replacement" })
|
||||
}),
|
||||
)
|
||||
await Effect.runPromise(
|
||||
Effect.map(Greeting, (item) => item.value).pipe(
|
||||
Effect.provide(
|
||||
LayerNode.buildLayer(greeting, { tiers, replacements: [LayerNode.replace(other, replacement)] }),
|
||||
),
|
||||
),
|
||||
)
|
||||
expect(acquisitions).toBe(0)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { test } from "bun:test"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { makeGlobalNode, makeLocationNode } from "@opencode-ai/core/effect/scoped-node"
|
||||
|
||||
class A extends Context.Service<A, {}>()("test/ScopedA") {}
|
||||
class B extends Context.Service<B, {}>()("test/ScopedB") {}
|
||||
|
||||
const a = Layer.succeed(A, A.of({}))
|
||||
const b = Layer.effect(B, Effect.as(A, B.of({})))
|
||||
const globalA = makeGlobalNode({ service: A, layer: a, deps: [] })
|
||||
const locationA = makeLocationNode({ service: A, layer: a, deps: [] })
|
||||
|
||||
makeGlobalNode({ service: B, layer: b, deps: [globalA] })
|
||||
makeLocationNode({ service: B, layer: b, deps: [globalA] })
|
||||
makeLocationNode({ service: B, layer: b, deps: [locationA] })
|
||||
|
||||
// @ts-expect-error Global nodes cannot depend on location nodes
|
||||
makeGlobalNode({ service: B, layer: b, deps: [locationA] })
|
||||
|
||||
// @ts-expect-error B requires A
|
||||
makeLocationNode({ service: B, layer: b, deps: [] })
|
||||
|
||||
test("type exploration compiles", () => {})
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { $ } from "bun"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { describe, expect } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Git } from "../../src/git"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const weird = process.platform === "win32" ? "space file.txt" : "tab\tfile.txt"
|
||||
const it = testEffect(LayerNode.buildLayer(Git.node))
|
||||
const it = testEffect(LayerNode.compile(LayerNode.group([Git.node])))
|
||||
|
||||
const scopedTmpdir = (options?: Parameters<typeof tmpdir>[0]) =>
|
||||
Effect.acquireRelease(
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ mcpTest.instance(
|
|||
|
||||
expect(yield* mcp.finishAuth(name, "invalid-code")).toEqual({
|
||||
status: "failed",
|
||||
error: "OAuth completion failed",
|
||||
error: "OAuth completion failed: Token exchange failed",
|
||||
})
|
||||
const entry = yield* auth.get(name)
|
||||
expect(entry?.tokens?.accessToken).toBe("working-token")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { test, expect, describe } from "bun:test"
|
||||
import { determineScope } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||
import { McpOAuthProvider, OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "../../src/mcp/oauth-provider"
|
||||
import type { McpAuth } from "../../src/mcp/auth"
|
||||
|
||||
|
|
@ -59,3 +60,43 @@ describe("McpOAuthProvider.clientMetadata", () => {
|
|||
expect(provider.clientMetadata.token_endpoint_auth_method).toBe("none")
|
||||
})
|
||||
})
|
||||
|
||||
describe("MCP OAuth scope selection", () => {
|
||||
test("adds offline_access when the authorization server and client support refresh tokens", () => {
|
||||
expect(
|
||||
determineScope({
|
||||
resourceMetadata: {
|
||||
resource: "https://mcp.example.com/mcp",
|
||||
scopes_supported: ["resource.read"],
|
||||
},
|
||||
authServerMetadata: {
|
||||
issuer: "https://auth.example.com",
|
||||
authorization_endpoint: "https://auth.example.com/authorize",
|
||||
token_endpoint: "https://auth.example.com/token",
|
||||
response_types_supported: ["code"],
|
||||
scopes_supported: ["resource.read", "offline_access"],
|
||||
},
|
||||
clientMetadata: makeProvider({}).clientMetadata,
|
||||
}),
|
||||
).toBe("resource.read offline_access")
|
||||
})
|
||||
|
||||
test("does not add unsupported authorization server scopes", () => {
|
||||
expect(
|
||||
determineScope({
|
||||
resourceMetadata: {
|
||||
resource: "https://mcp.example.com/mcp",
|
||||
scopes_supported: ["resource.read"],
|
||||
},
|
||||
authServerMetadata: {
|
||||
issuer: "https://auth.example.com",
|
||||
authorization_endpoint: "https://auth.example.com/authorize",
|
||||
token_endpoint: "https://auth.example.com/token",
|
||||
response_types_supported: ["code"],
|
||||
scopes_supported: ["resource.read"],
|
||||
},
|
||||
clientMetadata: makeProvider({}).clientMetadata,
|
||||
}),
|
||||
).toBe("resource.read")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -356,6 +356,17 @@ it.instance(
|
|||
{ config: { model: "anthropic/claude-sonnet-4-20250514" } },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"defaultModel treats empty provider config as no allowlist",
|
||||
Effect.gen(function* () {
|
||||
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = yield* Provider.use.defaultModel()
|
||||
expect(model.providerID).toBeDefined()
|
||||
expect(model.modelID).toBeDefined()
|
||||
}),
|
||||
{ config: { provider: {} } },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"defaultModel returns a typed error when config excludes every provider",
|
||||
Effect.gen(function* () {
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,40 @@ const scenarios: Scenario[] = [
|
|||
headers: ctx.headers(),
|
||||
}))
|
||||
.status(400, undefined, "none"),
|
||||
http.protected
|
||||
.get("/api/session/{sessionID}/history", "v2.session.history")
|
||||
.seeded((ctx) => ctx.session({ title: "Session history" }))
|
||||
.at((ctx) => ({
|
||||
path: `${route("/api/session/{sessionID}/history", { sessionID: ctx.state.id })}?${new URLSearchParams({
|
||||
after: "0",
|
||||
limit: "2",
|
||||
})}`,
|
||||
headers: ctx.headers(),
|
||||
}))
|
||||
.json(
|
||||
200,
|
||||
(body) => {
|
||||
object(body)
|
||||
array(body.data)
|
||||
check(typeof body.hasMore === "boolean", "Expected a history exhaustion signal")
|
||||
},
|
||||
"none",
|
||||
),
|
||||
http.protected
|
||||
.get("/api/session/{sessionID}/history", "v2.session.history.missing")
|
||||
.at((ctx) => ({
|
||||
path: route("/api/session/{sessionID}/history", { sessionID: "ses_httpapi_missing" }),
|
||||
headers: ctx.headers(),
|
||||
}))
|
||||
.json(404, object, "status"),
|
||||
http.protected
|
||||
.get("/api/session/{sessionID}/history", "v2.session.history.invalid")
|
||||
.seeded((ctx) => ctx.session({ title: "Invalid history sequence" }))
|
||||
.at((ctx) => ({
|
||||
path: `${route("/api/session/{sessionID}/history", { sessionID: ctx.state.id })}?after=-1`,
|
||||
headers: ctx.headers(),
|
||||
}))
|
||||
.json(400, object, "status"),
|
||||
http.protected
|
||||
.get("/api/session/{sessionID}/event", "v2.session.events.missing")
|
||||
.at((ctx) => ({
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ type OpenApiSchema = {
|
|||
readonly enum?: readonly unknown[]
|
||||
readonly properties?: Record<string, OpenApiSchema>
|
||||
readonly required?: readonly string[]
|
||||
readonly contentSchema?: OpenApiSchema
|
||||
readonly contentMediaType?: string
|
||||
}
|
||||
type OpenApiResponse = {
|
||||
readonly description?: string
|
||||
|
|
@ -99,6 +101,21 @@ describe("PublicApi OpenAPI v2 errors", () => {
|
|||
})
|
||||
})
|
||||
|
||||
test("names the v2 event union without the SSE string wrapper collision", () => {
|
||||
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
|
||||
|
||||
expect(spec.components.schemas.V2Event1).toBeUndefined()
|
||||
expect(spec.components.schemas.V2Event?.anyOf?.length).toBeGreaterThan(0)
|
||||
expect(spec.components.schemas.V2EventStream).toMatchObject({
|
||||
type: "string",
|
||||
contentMediaType: "application/json",
|
||||
contentSchema: { $ref: "#/components/schemas/V2Event" },
|
||||
})
|
||||
expect(spec.paths["/api/event"]?.get?.responses?.["200"]?.content?.["text/event-stream"]?.schema).toEqual({
|
||||
$ref: "#/components/schemas/V2Event",
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves /api auth responses", () => {
|
||||
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
|
||||
|
||||
|
|
|
|||
|
|
@ -27,13 +27,44 @@ const Event = Schema.Struct({
|
|||
data: Schema.Unknown,
|
||||
})
|
||||
|
||||
async function readEvent(reader: ReadableStreamDefaultReader<Uint8Array>) {
|
||||
const value = await reader.read()
|
||||
if (value.done) throw new Error("event stream closed")
|
||||
return Schema.decodeUnknownSync(Event)(JSON.parse(new TextDecoder().decode(value.value).replace(/^data: /, "")))
|
||||
async function* eventStream(body: ReadableStream<Uint8Array>) {
|
||||
const reader = body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ""
|
||||
try {
|
||||
while (true) {
|
||||
const boundary = buffer.match(/(?:\r\n|\r|\n){2}/)
|
||||
if (!boundary || boundary.index === undefined) {
|
||||
const value = await reader.read()
|
||||
if (value.done) return
|
||||
buffer += decoder.decode(value.value, { stream: true })
|
||||
continue
|
||||
}
|
||||
|
||||
const record = buffer.slice(0, boundary.index)
|
||||
buffer = buffer.slice(boundary.index + boundary[0].length)
|
||||
const data = record
|
||||
.split(/\r\n|\r|\n/)
|
||||
.filter((line) => line.startsWith("data:"))
|
||||
.map((line) => line.slice(5).replace(/^ /, ""))
|
||||
if (data.length) yield Schema.decodeUnknownSync(Event)(JSON.parse(data.join("\n")))
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
await reader.cancel()
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readEventType(reader: ReadableStreamDefaultReader<Uint8Array>, type: string) {
|
||||
async function readEvent(reader: AsyncIterator<typeof Event.Type>) {
|
||||
const value = await reader.next()
|
||||
if (value.done) throw new Error("event stream closed")
|
||||
return value.value
|
||||
}
|
||||
|
||||
async function readEventType(reader: AsyncIterator<typeof Event.Type>, type: string) {
|
||||
for (let index = 0; index < 20; index++) {
|
||||
const event = await readEvent(reader)
|
||||
if (event.type === type) return event
|
||||
|
|
@ -78,7 +109,7 @@ describe("v2 location HttpApi", () => {
|
|||
await using subscriber = await tmpdir({ git: true })
|
||||
await using publisher = await tmpdir({ git: true })
|
||||
const response = await request("/api/event", subscriber.path)
|
||||
const reader = response.body!.getReader()
|
||||
const reader = eventStream(response.body!)
|
||||
const connected = await readEvent(reader)
|
||||
expect(connected.type).toBe("server.connected")
|
||||
expect(connected.location).toBeUndefined()
|
||||
|
|
@ -90,6 +121,6 @@ describe("v2 location HttpApi", () => {
|
|||
location: { directory: publisher.path },
|
||||
data: { sessionID: expect.any(String) },
|
||||
})
|
||||
await reader.cancel()
|
||||
await reader.return(undefined)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
|||
import { SessionStatus } from "../../src/session/status"
|
||||
import { SessionSummary } from "../../src/session/summary"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
|
||||
import type { Provider } from "@/provider/provider"
|
||||
|
|
@ -613,7 +614,9 @@ describe("session.compaction.create", () => {
|
|||
})
|
||||
|
||||
const v2 = yield* SessionV2.Service.use((svc) => svc.messages({ sessionID: info.id })).pipe(
|
||||
Effect.provide(SessionV2.defaultLayer.pipe(Layer.provide(SessionExecution.noopLayer))),
|
||||
Effect.provide(SessionV2.defaultLayer),
|
||||
Effect.provide(SessionExecution.noopLayer),
|
||||
Effect.provide(locationServiceMapLayer),
|
||||
)
|
||||
expect(v2.at(-1)).toMatchObject({
|
||||
type: "compaction",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { Effect, Option } from "effect"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
|
|
@ -12,7 +12,7 @@ import { testEffect } from "../lib/effect"
|
|||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
const it = testEffect(LayerNode.buildLayer(LayerNode.group([SessionNs.node, MessageV2.node, SessionProjector.node])))
|
||||
const it = testEffect(LayerNode.compile(LayerNode.group([SessionNs.node, MessageV2.node, SessionProjector.node])))
|
||||
|
||||
const withSession = <A, E, R>(
|
||||
fn: (input: { session: SessionNs.Interface; sessionID: SessionID }) => Effect.Effect<A, E, R>,
|
||||
|
|
|
|||
|
|
@ -179,9 +179,9 @@ const replacements = [
|
|||
LayerNode.replace(SessionSummary.layer, summary),
|
||||
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer({ experimentalEventSystem: true })),
|
||||
]
|
||||
const env = LayerNode.buildLayer(
|
||||
const env = LayerNode.compile(
|
||||
LayerNode.group([root, LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })]),
|
||||
{ replacements },
|
||||
new Map(replacements.map((item) => [item.source, item.replacement])),
|
||||
)
|
||||
|
||||
const it = testEffect(env)
|
||||
|
|
@ -206,9 +206,12 @@ const providerErrorLLM = Layer.succeed(
|
|||
),
|
||||
}),
|
||||
)
|
||||
const providerErrorEnv = LayerNode.buildLayer(root, {
|
||||
replacements: [...replacements, LayerNode.replace(LLM.layer, providerErrorLLM)],
|
||||
})
|
||||
const providerErrorEnv = LayerNode.compile(
|
||||
root,
|
||||
new Map(
|
||||
[...replacements, LayerNode.replace(LLM.layer, providerErrorLLM)].map((item) => [item.source, item.replacement]),
|
||||
),
|
||||
)
|
||||
const itProviderError = testEffect(providerErrorEnv)
|
||||
|
||||
const fragmentFailureLLM = Layer.succeed(
|
||||
|
|
@ -225,9 +228,12 @@ const fragmentFailureLLM = Layer.succeed(
|
|||
),
|
||||
}),
|
||||
)
|
||||
const fragmentFailureEnv = LayerNode.buildLayer(root, {
|
||||
replacements: [...replacements, LayerNode.replace(LLM.layer, fragmentFailureLLM)],
|
||||
})
|
||||
const fragmentFailureEnv = LayerNode.compile(
|
||||
root,
|
||||
new Map(
|
||||
[...replacements, LayerNode.replace(LLM.layer, fragmentFailureLLM)].map((item) => [item.source, item.replacement]),
|
||||
),
|
||||
)
|
||||
const itFragmentFailure = testEffect(fragmentFailureEnv)
|
||||
|
||||
const boot = Effect.fn("test.boot")(function* () {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ import { reply, TestLLMServer } from "../lib/llm-server"
|
|||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
|
||||
const summary = Layer.succeed(
|
||||
SessionSummary.Service,
|
||||
|
|
@ -230,7 +230,7 @@ function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; proces
|
|||
Layer.provide(
|
||||
SystemPrompt.layer.pipe(
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
Layer.provide(deps),
|
||||
),
|
||||
),
|
||||
|
|
@ -697,7 +697,9 @@ noLLMServer.instance.skip(
|
|||
})
|
||||
|
||||
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
|
||||
Effect.provide(SessionV2.defaultLayer.pipe(Layer.provide(SessionExecution.noopLayer))),
|
||||
Effect.provide(SessionV2.defaultLayer),
|
||||
Effect.provide(SessionExecution.noopLayer),
|
||||
Effect.provide(locationServiceMapLayer),
|
||||
)
|
||||
const { db } = yield* Database.Service
|
||||
const row = yield* db
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import type { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { APICallError } from "ai"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { Effect, Schedule, Schema } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { SessionRetry } from "../../src/session/retry"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
|
|
@ -16,7 +16,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
|
|||
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
const retryProvider = "test"
|
||||
const it = testEffect(LayerNode.buildLayer(LayerNode.group([SessionStatus.node, CrossSpawnSpawner.node])))
|
||||
const it = testEffect(LayerNode.compile(LayerNode.group([SessionStatus.node, CrossSpawnSpawner.node])))
|
||||
|
||||
function apiError(headers?: Record<string, string>): SessionV1.APIError {
|
||||
return Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
|
||||
|
|
|
|||
|
|
@ -87,13 +87,16 @@ const root = LayerNode.group([
|
|||
LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] }),
|
||||
])
|
||||
const it = testEffect(
|
||||
LayerNode.buildLayer(root, {
|
||||
replacements: [
|
||||
LayerNode.replace(MCP.layer, mcp),
|
||||
LayerNode.replace(LSP.layer, lsp),
|
||||
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer({ experimentalEventSystem: true })),
|
||||
],
|
||||
}),
|
||||
LayerNode.compile(
|
||||
root,
|
||||
new Map(
|
||||
[
|
||||
LayerNode.replace(MCP.layer, mcp),
|
||||
LayerNode.replace(LSP.layer, lsp),
|
||||
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer({ experimentalEventSystem: true })),
|
||||
].map((item) => [item.source, item.replacement]),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const providerCfg = (url: string) => ({
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Skill } from "../../src/skill"
|
|||
import { Permission } from "../../src/permission"
|
||||
import { SystemPrompt } from "../../src/session/system"
|
||||
import { MCP } from "../../src/mcp"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const skills: Skill.Info[] = [
|
||||
|
|
@ -44,7 +44,7 @@ const build: Agent.Info = {
|
|||
|
||||
const it = testEffect(
|
||||
SystemPrompt.layer.pipe(
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
Layer.provide(
|
||||
Layer.mock(MCP.Service, {
|
||||
instructions: () =>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { beforeEach, describe, expect } from "bun:test"
|
|||
import { Effect, Exit, Layer, Option } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ import { provideTmpdirInstance } from "../fixture/fixture"
|
|||
import { resetDatabase } from "../fixture/db"
|
||||
import { pollWithTimeout, testEffect } from "../lib/effect"
|
||||
|
||||
const env = LayerNode.buildLayer(CrossSpawnSpawner.node)
|
||||
const env = LayerNode.compile(LayerNode.group([CrossSpawnSpawner.node]))
|
||||
const it = testEffect(env)
|
||||
|
||||
const json = (req: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unknown, status = 200) =>
|
||||
|
|
@ -34,13 +34,16 @@ const json = (req: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unkno
|
|||
const none = HttpClient.make(() => Effect.die("unexpected http call"))
|
||||
|
||||
function requestLayer(client: HttpClient.HttpClient) {
|
||||
return LayerNode.buildLayer(LayerNode.group([ShareNext.node, AccountRepo.node]), {
|
||||
replacements: [LayerNode.replace(FetchHttpClient.layer, Layer.succeed(HttpClient.HttpClient, client))],
|
||||
})
|
||||
const replacement = LayerNode.replace(FetchHttpClient.layer, Layer.succeed(HttpClient.HttpClient, client))
|
||||
return LayerNode.compile(
|
||||
LayerNode.group([ShareNext.node, AccountRepo.node]),
|
||||
new Map([[replacement.source, replacement.replacement]]),
|
||||
)
|
||||
}
|
||||
|
||||
function integrationLayer(client: HttpClient.HttpClient) {
|
||||
return LayerNode.buildLayer(
|
||||
const replacement = LayerNode.replace(FetchHttpClient.layer, Layer.succeed(HttpClient.HttpClient, client))
|
||||
return LayerNode.compile(
|
||||
LayerNode.group([
|
||||
ShareNext.node,
|
||||
EventV2Bridge.node,
|
||||
|
|
@ -49,7 +52,7 @@ function integrationLayer(client: HttpClient.HttpClient) {
|
|||
AccountRepo.node,
|
||||
Database.node,
|
||||
]),
|
||||
{ replacements: [LayerNode.replace(FetchHttpClient.layer, Layer.succeed(HttpClient.HttpClient, client))] },
|
||||
new Map([[replacement.source, replacement.replacement]]),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,11 +54,17 @@ const replacements = [
|
|||
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer()),
|
||||
]
|
||||
|
||||
const it = testEffect(LayerNode.buildLayer(root, { replacements }))
|
||||
const it = testEffect(LayerNode.compile(root, new Map(replacements.map((item) => [item.source, item.replacement]))))
|
||||
const withBrokenPlugin = testEffect(
|
||||
LayerNode.buildLayer(root, {
|
||||
replacements: [...replacements, LayerNode.replace(Plugin.layer, brokenPluginLayer)],
|
||||
}),
|
||||
LayerNode.compile(
|
||||
root,
|
||||
new Map(
|
||||
[...replacements, LayerNode.replace(Plugin.layer, brokenPluginLayer)].map((item) => [
|
||||
item.source,
|
||||
item.replacement,
|
||||
]),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue