Compare commits
1 commit
dev
...
jlongster/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f0343ca24 |
106 changed files with 1377 additions and 748 deletions
1
packages/core/src/.#location-layer.ts
Symbolic link
1
packages/core/src/.#location-layer.ts
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
james@james-6.local.2380:1780672839
|
||||||
1
packages/core/src/.#location-mutation.ts
Symbolic link
1
packages/core/src/.#location-mutation.ts
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
james@james-6.local.2380:1780672839
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as AgentV2 from "./agent"
|
export * as AgentV2 from "./agent"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import { Array, Context, Effect, Layer, Types } from "effect"
|
import { Array, Context, Effect, Layer, Types } from "effect"
|
||||||
import { Agent } from "@opencode-ai/schema/agent"
|
import { Agent } from "@opencode-ai/schema/agent"
|
||||||
import { State } from "./state"
|
import { State } from "./state"
|
||||||
|
|
@ -106,3 +107,5 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer
|
export const locationLayer = layer
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [] })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as AISDK from "./aisdk"
|
export * as AISDK from "./aisdk"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||||
import { Cause, Context, Effect, Layer, Schema, Scope } from "effect"
|
import { Cause, Context, Effect, Layer, Schema, Scope } from "effect"
|
||||||
import { ModelV2 } from "./model"
|
import { ModelV2 } from "./model"
|
||||||
|
|
@ -231,4 +232,6 @@ export const locationLayer = Layer.effect(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [] })
|
||||||
|
|
||||||
export const defaultLayer = locationLayer
|
export const defaultLayer = locationLayer
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ export * as BackgroundJob from "./background-job"
|
||||||
|
|
||||||
import { Cause, Clock, Context, Deferred, Effect, Exit, Layer, Scope, SynchronizedRef } from "effect"
|
import { Cause, Clock, Context, Deferred, Effect, Exit, Layer, Scope, SynchronizedRef } from "effect"
|
||||||
import { Identifier } from "./id/id"
|
import { Identifier } from "./id/id"
|
||||||
|
import { makeGlobalNode } from "./effect/scoped-node"
|
||||||
|
|
||||||
export type Status = "running" | "completed" | "error" | "cancelled"
|
export type Status = "running" | "completed" | "error" | "cancelled"
|
||||||
|
|
||||||
|
|
@ -362,3 +363,5 @@ export const make = Effect.gen(function* () {
|
||||||
export const layer = Layer.effect(Service, make)
|
export const layer = Layer.effect(Service, make)
|
||||||
|
|
||||||
export const defaultLayer = layer
|
export const defaultLayer = layer
|
||||||
|
|
||||||
|
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as Catalog from "./catalog"
|
export * as Catalog from "./catalog"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
|
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/schema/catalog"
|
import { Catalog } from "@opencode-ai/schema/catalog"
|
||||||
import { ModelV2 } from "./model"
|
import { ModelV2 } from "./model"
|
||||||
|
|
@ -291,3 +292,5 @@ export const locationLayer = layer.pipe(
|
||||||
Layer.provideMerge(Integration.locationLayer),
|
Layer.provideMerge(Integration.locationLayer),
|
||||||
Layer.provideMerge(Policy.locationLayer),
|
Layer.provideMerge(Policy.locationLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Policy.node, Integration.node] })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as CommandV2 from "./command"
|
export * as CommandV2 from "./command"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import { Context, Effect, Layer, Types } from "effect"
|
import { Context, Effect, Layer, Types } from "effect"
|
||||||
import { Command } from "@opencode-ai/schema/command"
|
import { Command } from "@opencode-ai/schema/command"
|
||||||
import { State } from "./state"
|
import { State } from "./state"
|
||||||
|
|
@ -59,3 +60,5 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer
|
export const locationLayer = layer
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [] })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as Config from "./config"
|
export * as Config from "./config"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { type ParseError, parse } from "jsonc-parser"
|
import { type ParseError, parse } from "jsonc-parser"
|
||||||
import { Context, Effect, Layer, Option, Schema } from "effect"
|
import { Context, Effect, Layer, Option, Schema } from "effect"
|
||||||
|
|
@ -218,3 +219,9 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer.pipe(Layer.provideMerge(Policy.locationLayer))
|
export const locationLayer = layer.pipe(Layer.provideMerge(Policy.locationLayer))
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer,
|
||||||
|
deps: [FSUtil.node, Global.node, Location.node, Policy.node],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { Credential } from "@opencode-ai/schema/credential"
|
import { Credential } from "@opencode-ai/schema/credential"
|
||||||
import { Integration } from "@opencode-ai/schema/integration"
|
import { Integration } from "@opencode-ai/schema/integration"
|
||||||
import { Database } from "./database/database"
|
import { Database } from "./database/database"
|
||||||
|
import { makeGlobalNode } from "./effect/scoped-node"
|
||||||
import { CredentialTable } from "./credential/sql"
|
import { CredentialTable } from "./credential/sql"
|
||||||
|
|
||||||
export const ID = Credential.ID
|
export const ID = Credential.ID
|
||||||
|
|
@ -135,3 +136,5 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
|
||||||
|
|
||||||
|
export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] })
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import {
|
||||||
import * as NodeChildProcess from "node:child_process"
|
import * as NodeChildProcess from "node:child_process"
|
||||||
import { PassThrough } from "node:stream"
|
import { PassThrough } from "node:stream"
|
||||||
import launch from "cross-spawn"
|
import launch from "cross-spawn"
|
||||||
import { LayerNode } from "./effect/layer-node"
|
import { makeGlobalNode } from "./effect/scoped-node"
|
||||||
import { filesystem, path } from "./effect/layer-node-platform"
|
import { filesystem, path } from "./effect/layer-node-platform"
|
||||||
|
|
||||||
const toError = (err: unknown): Error => (err instanceof globalThis.Error ? err : new globalThis.Error(String(err)))
|
const toError = (err: unknown): Error => (err instanceof globalThis.Error ? err : new globalThis.Error(String(err)))
|
||||||
|
|
@ -503,6 +503,6 @@ export const layer: Layer.Layer<ChildProcessSpawner, never, FileSystem.FileSyste
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer), Layer.provide(NodePath.layer))
|
export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer), Layer.provide(NodePath.layer))
|
||||||
export const node = LayerNode.make({ service: ChildProcessSpawner, layer, deps: [filesystem, path] })
|
export const node = makeGlobalNode({ service: ChildProcessSpawner, layer, deps: [filesystem, path] })
|
||||||
|
|
||||||
export * as CrossSpawnSpawner from "./cross-spawn-spawner"
|
export * as CrossSpawnSpawner from "./cross-spawn-spawner"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Flag } from "../flag/flag"
|
||||||
import { isAbsolute, join } from "path"
|
import { isAbsolute, join } from "path"
|
||||||
import { DatabaseMigration } from "./migration"
|
import { DatabaseMigration } from "./migration"
|
||||||
import { InstallationChannel } from "../installation/version"
|
import { InstallationChannel } from "../installation/version"
|
||||||
import { LayerNode } from "../effect/layer-node"
|
import { makeGlobalNode } from "../effect/scoped-node"
|
||||||
|
|
||||||
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
|
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
|
||||||
type DatabaseShape = Effect.Success<typeof makeDatabase>
|
type DatabaseShape = Effect.Success<typeof makeDatabase>
|
||||||
|
|
@ -60,4 +60,4 @@ export const defaultLayer = Layer.unwrap(
|
||||||
}),
|
}),
|
||||||
).pipe(Layer.provide(Global.defaultLayer))
|
).pipe(Layer.provide(Global.defaultLayer))
|
||||||
|
|
||||||
export const node = LayerNode.make({ service: Service, layer: layerFromPath(path()), deps: [] })
|
export const node = makeGlobalNode({ service: Service, layer: layerFromPath(path()), deps: [] })
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,16 @@ import { LLMClient, RequestExecutor } from "@opencode-ai/llm/route"
|
||||||
import { FileSystem, Path } from "effect"
|
import { FileSystem, Path } from "effect"
|
||||||
import { FetchHttpClient } from "effect/unstable/http"
|
import { FetchHttpClient } from "effect/unstable/http"
|
||||||
import { HttpClient } from "effect/unstable/http"
|
import { HttpClient } from "effect/unstable/http"
|
||||||
import { LayerNode } from "./layer-node"
|
import { makeGlobalNode } from "./scoped-node"
|
||||||
|
|
||||||
export const filesystem = LayerNode.make({ service: FileSystem.FileSystem, layer: NodeFileSystem.layer, deps: [] })
|
export const filesystem = makeGlobalNode({ service: FileSystem.FileSystem, layer: NodeFileSystem.layer, deps: [] })
|
||||||
export const path = LayerNode.make({ service: Path.Path, layer: NodePath.layer, deps: [] })
|
export const path = makeGlobalNode({ service: Path.Path, layer: NodePath.layer, deps: [] })
|
||||||
export const httpClient = LayerNode.make({ service: HttpClient.HttpClient, layer: FetchHttpClient.layer, deps: [] })
|
export const httpClient = makeGlobalNode({ service: HttpClient.HttpClient, layer: FetchHttpClient.layer, deps: [] })
|
||||||
export const requestExecutor = LayerNode.make({
|
export const requestExecutor = makeGlobalNode({
|
||||||
service: RequestExecutor.Service,
|
service: RequestExecutor.Service,
|
||||||
layer: RequestExecutor.layer,
|
layer: RequestExecutor.layer,
|
||||||
deps: [httpClient],
|
deps: [httpClient],
|
||||||
})
|
})
|
||||||
export const llmClient = LayerNode.make({ service: LLMClient.Service, layer: LLMClient.layer, deps: [requestExecutor] })
|
export const llmClient = makeGlobalNode({ service: LLMClient.Service, layer: LLMClient.layer, deps: [requestExecutor] })
|
||||||
|
|
||||||
export * as LayerNodePlatform from "./layer-node-platform"
|
export * as LayerNodePlatform from "./layer-node-platform"
|
||||||
|
|
|
||||||
182
packages/core/src/effect/layer-node-tree.ts
Normal file
182
packages/core/src/effect/layer-node-tree.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
import { Layer } from "effect"
|
||||||
|
import { LayerNode } from "./layer-node"
|
||||||
|
|
||||||
|
type AnyNode = LayerNode.Node<unknown, unknown, any>
|
||||||
|
type RuntimeLayer = Layer.Layer<never, unknown, unknown>
|
||||||
|
|
||||||
|
type Separated<N, Names extends readonly [string, ...string[]]> = {
|
||||||
|
readonly [Name in Names[number]]: N extends LayerNode.Node<infer A, infer E, any>
|
||||||
|
? LayerNode.Node<A, E>
|
||||||
|
: never
|
||||||
|
}
|
||||||
|
|
||||||
|
export function separate<const Root extends AnyNode, const Names extends readonly [string, ...string[]]>(
|
||||||
|
root: Root,
|
||||||
|
tiers: LayerNode.Tiers<Names>,
|
||||||
|
): Separated<Root, Names> {
|
||||||
|
const roots = new Map<LayerNode.Tier, AnyNode[]>()
|
||||||
|
const serviceTiers = new Map<string, LayerNode.Tier>()
|
||||||
|
const visited = new Set<AnyNode>()
|
||||||
|
|
||||||
|
const validate = (node: AnyNode) => {
|
||||||
|
if (node.kind === "group") {
|
||||||
|
node.dependencies.forEach(validate)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (visited.has(node)) return
|
||||||
|
visited.add(node)
|
||||||
|
const tier = requireTier(node, tiers)
|
||||||
|
const existing = serviceTiers.get(node.name)
|
||||||
|
if (existing && existing !== tier) {
|
||||||
|
throw new Error(`Service ${node.name} belongs to both tier ${existing} and tier ${tier}`)
|
||||||
|
}
|
||||||
|
serviceTiers.set(node.name, tier)
|
||||||
|
node.dependencies.forEach(validate)
|
||||||
|
}
|
||||||
|
validate(root)
|
||||||
|
|
||||||
|
for (const node of flatten(root)) {
|
||||||
|
const tier = requireTier(node, tiers)
|
||||||
|
const current = roots.get(tier) ?? []
|
||||||
|
roots.set(tier, current)
|
||||||
|
current.push(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
tiers.names.map((name) => [name, LayerNode.group(roots.get(tiers.values[name as Names[number]]) ?? [])]),
|
||||||
|
) as Separated<Root, Names>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hoist<A, E, T extends LayerNode.Tier>(
|
||||||
|
root: LayerNode.Node<A, E, any>,
|
||||||
|
tier: T,
|
||||||
|
tiers: LayerNode.Tiers<readonly [string, ...string[]]>,
|
||||||
|
): {
|
||||||
|
readonly node: LayerNode.Node<A, E>
|
||||||
|
readonly hoisted: LayerNode.Node<unknown, unknown>
|
||||||
|
} {
|
||||||
|
const indexes = new Map(tiers.names.map((name, index) => [tiers.values[name], index]))
|
||||||
|
const current = indexes.get(tier)
|
||||||
|
if (current === undefined) throw new Error(`Tier ${tier} is not in the tier configuration`)
|
||||||
|
const visited = new Map<AnyNode, AnyNode>()
|
||||||
|
const hoisted = new Map<string, AnyNode>()
|
||||||
|
let hoistedTier: LayerNode.Tier | undefined
|
||||||
|
const visiting = new Set<AnyNode>()
|
||||||
|
const stack: AnyNode[] = []
|
||||||
|
|
||||||
|
const visit = (node: AnyNode): AnyNode => {
|
||||||
|
if (node.kind === "group") {
|
||||||
|
return { ...node, dependencies: node.dependencies.map(visit) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingNode = visited.get(node)
|
||||||
|
if (existingNode) return existingNode
|
||||||
|
|
||||||
|
const dependencyTier = requireTier(node, tiers)
|
||||||
|
const index = indexes.get(dependencyTier)!
|
||||||
|
if (index < current) throw new Error(`Tier ${tier} cannot depend on lower tier ${dependencyTier}`)
|
||||||
|
if (index > current) {
|
||||||
|
if (hoistedTier && hoistedTier !== dependencyTier) {
|
||||||
|
throw new Error(`Tree ${tier} hoists dependencies into multiple tiers`)
|
||||||
|
}
|
||||||
|
hoistedTier = dependencyTier
|
||||||
|
const existing = hoisted.get(node.name)
|
||||||
|
if (existing && existing !== node) {
|
||||||
|
throw new Error(`Tier ${tier} has conflicting implementations for ${node.name}`)
|
||||||
|
}
|
||||||
|
hoisted.set(node.name, node)
|
||||||
|
const empty = LayerNode.group([])
|
||||||
|
visited.set(node, empty)
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
if (node.kind === "unbound") {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visiting.has(node)) {
|
||||||
|
const start = stack.indexOf(node)
|
||||||
|
throw new Error(
|
||||||
|
`Cycle detected in layer tree: ${[...stack.slice(start), node].map((item) => item.name).join(" -> ")}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
visiting.add(node)
|
||||||
|
stack.push(node)
|
||||||
|
try {
|
||||||
|
const dependencies = node.dependencies.map(visit)
|
||||||
|
const clone = { ...node, dependencies }
|
||||||
|
visited.set(node, clone)
|
||||||
|
return clone
|
||||||
|
} finally {
|
||||||
|
stack.pop()
|
||||||
|
visiting.delete(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
node: visit(root) as LayerNode.Node<A, E>,
|
||||||
|
hoisted: LayerNode.group(Array.from(hoisted.values())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compile(
|
||||||
|
root: LayerNode.Node<unknown, unknown, any>,
|
||||||
|
replacements?: ReadonlyMap<Layer.Any, Layer.Any>,
|
||||||
|
): Layer.Layer<never, unknown> {
|
||||||
|
const cache = new Map<AnyNode, RuntimeLayer>()
|
||||||
|
const compileNode = (node: AnyNode): RuntimeLayer => {
|
||||||
|
if (node.kind === "unbound") throw new Error(`Unbound layer node: ${node.name}`)
|
||||||
|
const cached = cache.get(node)
|
||||||
|
if (cached) return cached
|
||||||
|
const dependencies = node.dependencies.flatMap(flatten).map(compileNode)
|
||||||
|
const implementation = (replacements?.get(node.implementation!) ?? node.implementation!) as RuntimeLayer
|
||||||
|
const layer =
|
||||||
|
dependencies.length === 0
|
||||||
|
? implementation
|
||||||
|
: implementation.pipe(Layer.provide(dependencies as [RuntimeLayer, ...RuntimeLayer[]]))
|
||||||
|
cache.set(node, layer)
|
||||||
|
return layer
|
||||||
|
}
|
||||||
|
const layers = flatten(root).map((node) => compileNode(node))
|
||||||
|
const layer = layers.reduce<RuntimeLayer>((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty)
|
||||||
|
return layer as Layer.Layer<never, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bind<A, E, T extends LayerNode.Tier | undefined>(
|
||||||
|
root: LayerNode.Node<A, E, T>,
|
||||||
|
source: AnyNode,
|
||||||
|
replacement: AnyNode,
|
||||||
|
): LayerNode.Node<A, E, T> {
|
||||||
|
if (source.kind !== "unbound") throw new Error(`Cannot bind non-unbound layer node: ${source.name}`)
|
||||||
|
if (source.name !== replacement.name) {
|
||||||
|
throw new Error(`Cannot bind ${source.name} to ${replacement.name}`)
|
||||||
|
}
|
||||||
|
if (source.tier !== replacement.tier) {
|
||||||
|
throw new Error(`Cannot bind ${source.name} across tiers`)
|
||||||
|
}
|
||||||
|
const visited = new Map<AnyNode, AnyNode>()
|
||||||
|
const visit = (node: AnyNode): AnyNode => {
|
||||||
|
if (node === source) return replacement
|
||||||
|
const existing = visited.get(node)
|
||||||
|
if (existing) return existing
|
||||||
|
if (node.kind === "unbound") return node
|
||||||
|
const clone = { ...node, dependencies: node.dependencies.map(visit) }
|
||||||
|
visited.set(node, clone)
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
return visit(root) as LayerNode.Node<A, E, T>
|
||||||
|
}
|
||||||
|
|
||||||
|
function flatten(node: AnyNode): readonly AnyNode[] {
|
||||||
|
return node.kind === "group" ? node.dependencies.flatMap(flatten) : [node]
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireTier(node: AnyNode, tiers: LayerNode.Tiers<readonly [string, ...string[]]>): LayerNode.Tier {
|
||||||
|
const tier =
|
||||||
|
node.tier ?? (tiers.names.length === 1 && tiers.names[0] === "untiered" ? tiers.values.untiered : undefined)
|
||||||
|
if (!tier || !tiers.names.some((name) => tiers.values[name] === tier)) {
|
||||||
|
throw new Error(`Node ${node.name} is not in the tier configuration`)
|
||||||
|
}
|
||||||
|
return tier
|
||||||
|
}
|
||||||
|
|
||||||
|
export * as LayerNodeTree from "./layer-node-tree"
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Brand, Context, Layer } from "effect"
|
import { Brand, Context, Layer } from "effect"
|
||||||
|
|
||||||
type RuntimeLayer = Layer.Layer<never, unknown, unknown>
|
|
||||||
type AnyNode = Node<unknown, unknown, any>
|
type AnyNode = Node<unknown, unknown, any>
|
||||||
type NodeList<Item extends AnyNode = AnyNode> = readonly [] | readonly [Item, ...Item[]]
|
type NodeList<Item extends AnyNode = AnyNode> = readonly [] | readonly [Item, ...Item[]]
|
||||||
type Output<Item> = [Item] extends [never] ? never : Item extends Node<infer A, unknown, any> ? A : never
|
type Output<Item> = [Item] extends [never] ? never : Item extends Node<infer A, unknown, any> ? A : never
|
||||||
|
|
@ -18,8 +17,8 @@ export type Tier<Name extends string = string> = Name & Brand.Brand<"LayerNode.T
|
||||||
|
|
||||||
const makeTier = Brand.nominal<Tier>()
|
const makeTier = Brand.nominal<Tier>()
|
||||||
|
|
||||||
export type Node<A, E = never, T extends Tier | undefined = undefined> = {
|
export interface Node<A, E = never, T extends Tier | undefined = undefined> {
|
||||||
readonly kind: "layer" | "group"
|
readonly kind: "layer" | "unbound" | "group"
|
||||||
readonly name: string
|
readonly name: string
|
||||||
readonly service?: Context.Service.Any
|
readonly service?: Context.Service.Any
|
||||||
readonly implementation?: Layer.Any
|
readonly implementation?: Layer.Any
|
||||||
|
|
@ -34,7 +33,7 @@ type NodeIdentity =
|
||||||
| { readonly name: string; readonly service?: never }
|
| { readonly name: string; readonly service?: never }
|
||||||
type DistributiveOmit<A, K extends PropertyKey> = A extends unknown ? Omit<A, K> : never
|
type DistributiveOmit<A, K extends PropertyKey> = A extends unknown ? Omit<A, K> : never
|
||||||
|
|
||||||
type NodeInput<
|
type MakeInput<
|
||||||
Implementation extends Layer.Any,
|
Implementation extends Layer.Any,
|
||||||
Items extends NodeList,
|
Items extends NodeList,
|
||||||
T extends Tier | undefined = undefined,
|
T extends Tier | undefined = undefined,
|
||||||
|
|
@ -49,8 +48,12 @@ export function make<
|
||||||
const Items extends NodeList,
|
const Items extends NodeList,
|
||||||
const T extends Tier | undefined = undefined,
|
const T extends Tier | undefined = undefined,
|
||||||
>(
|
>(
|
||||||
input: NodeInput<Implementation, Items, T>,
|
input: MakeInput<Implementation, Items, T>,
|
||||||
): Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>, T> {
|
): Node<
|
||||||
|
Layer.Success<Implementation>,
|
||||||
|
Layer.Error<Implementation> | Error<Items[number]>,
|
||||||
|
T
|
||||||
|
> {
|
||||||
return {
|
return {
|
||||||
kind: "layer",
|
kind: "layer",
|
||||||
name: input.service !== undefined ? input.service.key : input.name,
|
name: input.service !== undefined ? input.service.key : input.name,
|
||||||
|
|
@ -61,7 +64,20 @@ export function make<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function group<const Items extends NodeList>(
|
export function unbound<R, Shape, const T extends Tier>(
|
||||||
|
service: Context.Key<R, Shape>,
|
||||||
|
tier: T,
|
||||||
|
): Node<R, never, T> {
|
||||||
|
return {
|
||||||
|
kind: "unbound",
|
||||||
|
name: service.key,
|
||||||
|
service,
|
||||||
|
dependencies: [],
|
||||||
|
tier,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function group<const Items extends readonly AnyNode[]>(
|
||||||
dependencies: Items,
|
dependencies: Items,
|
||||||
): Node<Output<Items[number]>, Error<Items[number]>> {
|
): Node<Output<Items[number]>, Error<Items[number]>> {
|
||||||
return { kind: "group", name: "group", dependencies }
|
return { kind: "group", name: "group", dependencies }
|
||||||
|
|
@ -76,19 +92,26 @@ type AllowedTierNames<Names extends readonly string[], Name extends Names[number
|
||||||
: AllowedTierNames<Tail, Name>
|
: AllowedTierNames<Tail, Name>
|
||||||
: never
|
: never
|
||||||
|
|
||||||
type NodeInTiers<Names extends string> = Node<unknown, unknown, Tier<Names>>
|
type NodeInTiers<Names extends string> = Node<unknown, unknown, Tier<Names> | undefined>
|
||||||
|
type CheckTiers<Items extends NodeList, Names extends string> = [Exclude<Items[number], NodeInTiers<Names>>] extends [
|
||||||
|
never,
|
||||||
|
]
|
||||||
|
? unknown
|
||||||
|
: { readonly "Invalid tier dependencies": Exclude<Items[number], NodeInTiers<Names>> }
|
||||||
|
|
||||||
export interface Tiers<Names extends readonly [string, ...string[]]> {
|
export interface Tiers<Names extends readonly [string, ...string[]]> {
|
||||||
readonly names: Names
|
readonly names: Names
|
||||||
readonly values: { readonly [K in Names[number]]: Tier<K> }
|
readonly values: { readonly [K in Names[number]]: Tier<K> }
|
||||||
readonly make: <Name extends Names[number]>(
|
readonly make: <Name extends Names[number]>(
|
||||||
name: Name,
|
name: Name,
|
||||||
) => <
|
) => <const Implementation extends Layer.Any, const Items extends NodeList>(
|
||||||
const Implementation extends Layer.Any,
|
input: DistributiveOmit<MakeInput<Implementation, Items, Tier<Name>>, "tier"> &
|
||||||
const Items extends NodeList<NodeInTiers<AllowedTierNames<Names, Name>>>,
|
CheckTiers<Items, AllowedTierNames<Names, Name>>,
|
||||||
>(
|
) => Node<
|
||||||
input: DistributiveOmit<NodeInput<Implementation, Items, Tier<Name>>, "tier">,
|
Layer.Success<Implementation>,
|
||||||
) => Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>, Tier<Name>>
|
Layer.Error<Implementation> | Error<Items[number]>,
|
||||||
|
Tier<Name>
|
||||||
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tiers<const Names extends readonly [string, ...string[]]>(names: Names): Tiers<Names> {
|
export function tiers<const Names extends readonly [string, ...string[]]>(names: Names): Tiers<Names> {
|
||||||
|
|
@ -96,14 +119,11 @@ export function tiers<const Names extends readonly [string, ...string[]]>(names:
|
||||||
return {
|
return {
|
||||||
names,
|
names,
|
||||||
values,
|
values,
|
||||||
make: ((name: Names[number]) => (input: DistributiveOmit<NodeInput<Layer.Any, NodeList, Tier>, "tier">) =>
|
make: ((name: Names[number]) => (input: DistributiveOmit<MakeInput<Layer.Any, NodeList, Tier>, "tier">) =>
|
||||||
make({ ...input, tier: values[name] })) as Tiers<Names>["make"],
|
make({ ...input, tier: values[name] })) as Tiers<Names>["make"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultTiers = tiers(["untiered"])
|
|
||||||
const untiered = defaultTiers.values.untiered
|
|
||||||
|
|
||||||
export type Replacement = {
|
export type Replacement = {
|
||||||
readonly source: Layer.Any
|
readonly source: Layer.Any
|
||||||
readonly replacement: Layer.Any
|
readonly replacement: Layer.Any
|
||||||
|
|
@ -120,129 +140,4 @@ export function replace<A, E, R, E2>(
|
||||||
return { source, replacement }
|
return { source, replacement }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildLayer<
|
|
||||||
A,
|
|
||||||
E,
|
|
||||||
const Names extends readonly [string, ...string[]] = readonly ["untiered"],
|
|
||||||
const Built extends Layer.Any = Layer.Layer<never, never, never>,
|
|
||||||
>(
|
|
||||||
node: Node<A, E, any>,
|
|
||||||
options?: {
|
|
||||||
readonly tiers?: Tiers<Names>
|
|
||||||
readonly buildTier?: (tier: Names[number], layers: readonly Layer.Any[]) => Built
|
|
||||||
readonly replacements?: readonly Replacement[]
|
|
||||||
},
|
|
||||||
): Layer.Layer<A | Layer.Success<Built>, E | Layer.Error<Built>, never> {
|
|
||||||
const tiers = options?.tiers ?? (defaultTiers as unknown as Tiers<Names>)
|
|
||||||
const replacementMap = new Map(options?.replacements?.map((item) => [item.source, item.replacement]))
|
|
||||||
const plans = plan(node, tiers, replacementMap)
|
|
||||||
const layers: RuntimeLayer[] = tiers.names.map((name) => {
|
|
||||||
const tier = tiers.values[name as Names[number]]
|
|
||||||
const layers = plans.get(tier) ?? []
|
|
||||||
return (options?.buildTier?.(name, layers) ?? combine(layers)) as RuntimeLayer
|
|
||||||
})
|
|
||||||
if (layers.length === 0) return Layer.empty as never
|
|
||||||
return layers.slice(1).reduce((result, layer) => result.pipe(Layer.provideMerge(layer)), layers[0]) as never
|
|
||||||
}
|
|
||||||
|
|
||||||
export function combine(layers: readonly Layer.Any[]): RuntimeLayer {
|
|
||||||
return layers.reduce<RuntimeLayer>(
|
|
||||||
(result, layer) => (layer as RuntimeLayer).pipe(Layer.provideMerge(result)),
|
|
||||||
Layer.empty as RuntimeLayer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function plan(
|
|
||||||
root: AnyNode,
|
|
||||||
tiers: Tiers<readonly [string, ...string[]]>,
|
|
||||||
replacements: ReadonlyMap<Layer.Any, Layer.Any>,
|
|
||||||
) {
|
|
||||||
const indexes = new Map(tiers.names.map((name, index) => [tiers.values[name], index]))
|
|
||||||
const plans = new Map<Tier, Layer.Any[]>()
|
|
||||||
const activeImplementations = new Map<Tier, Map<string, AnyNode>>()
|
|
||||||
const serviceTiers = new Map<string, Tier>()
|
|
||||||
const visiting = new Set<AnyNode>()
|
|
||||||
const stack: AnyNode[] = []
|
|
||||||
const boundaryVisited = new Map<AnyNode, Set<Tier>>()
|
|
||||||
const boundaryServices = new Map<Tier, Map<string, AnyNode>>()
|
|
||||||
|
|
||||||
const validateBoundary = (node: AnyNode, origin: Tier) => {
|
|
||||||
const checked = boundaryVisited.get(node) ?? new Set<Tier>()
|
|
||||||
boundaryVisited.set(node, checked)
|
|
||||||
if (checked.has(origin)) return false
|
|
||||||
checked.add(origin)
|
|
||||||
const services = boundaryServices.get(origin) ?? new Map<string, AnyNode>()
|
|
||||||
boundaryServices.set(origin, services)
|
|
||||||
const key = node.name
|
|
||||||
const existing = services.get(key)
|
|
||||||
if (existing && existing !== node) {
|
|
||||||
throw new Error(`Tier ${origin} has conflicting implementations for ${key}`)
|
|
||||||
}
|
|
||||||
services.set(key, node)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const visit = (node: AnyNode, currentTier?: Tier, origins: readonly Tier[] = []) => {
|
|
||||||
if (node.kind === "group") {
|
|
||||||
node.dependencies.forEach((dependency) => visit(dependency, currentTier, origins))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const tier = node.tier ?? untiered
|
|
||||||
if (!indexes.has(tier)) throw new Error(`Node ${node.name} is not in the tier configuration`)
|
|
||||||
const key = node.name
|
|
||||||
const serviceTier = serviceTiers.get(key)
|
|
||||||
if (serviceTier && serviceTier !== tier) {
|
|
||||||
throw new Error(`Service ${key} belongs to both tier ${serviceTier} and tier ${tier}`)
|
|
||||||
}
|
|
||||||
serviceTiers.set(key, tier)
|
|
||||||
const nextOrigins = [...origins]
|
|
||||||
if (currentTier) {
|
|
||||||
const current = indexes.get(currentTier)!
|
|
||||||
const required = indexes.get(tier)!
|
|
||||||
if (required < current) {
|
|
||||||
throw new Error(`Tier ${currentTier} cannot depend on lower tier ${tier}`)
|
|
||||||
}
|
|
||||||
if (required > current) nextOrigins.push(currentTier)
|
|
||||||
}
|
|
||||||
const unseenOrigins = nextOrigins.filter((origin) => validateBoundary(node, origin))
|
|
||||||
|
|
||||||
// A node may need to be emitted more than once because the final output is a
|
|
||||||
// flat list of layers applied with Layer.provideMerge. If another node for
|
|
||||||
// the same service was emitted afterward, this node is no longer the active
|
|
||||||
// implementation for subsequent consumers. Re-emitting restores the intended
|
|
||||||
// implementation ordering while Effect memoization avoids reacquiring the layer.
|
|
||||||
const implementations = activeImplementations.get(tier) ?? new Map<string, AnyNode>()
|
|
||||||
activeImplementations.set(tier, implementations)
|
|
||||||
if (implementations.get(key) === node && unseenOrigins.length === 0) return
|
|
||||||
|
|
||||||
if (visiting.has(node)) {
|
|
||||||
const start = stack.indexOf(node)
|
|
||||||
throw new Error(
|
|
||||||
`Cycle detected in layer graph: ${[...stack.slice(start), node].map((item) => item.name).join(" -> ")}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
visiting.add(node)
|
|
||||||
stack.push(node)
|
|
||||||
try {
|
|
||||||
node.dependencies.forEach((dependency) => visit(dependency, tier, unseenOrigins))
|
|
||||||
const layers = plans.get(tier) ?? []
|
|
||||||
plans.set(tier, layers)
|
|
||||||
layers.push(replacements.get(node.implementation!) ?? node.implementation!)
|
|
||||||
implementations.set(key, node)
|
|
||||||
} finally {
|
|
||||||
stack.pop()
|
|
||||||
visiting.delete(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visit(root)
|
|
||||||
return plans
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireTier(node: AnyNode, indexes: ReadonlyMap<Tier, number>) {
|
|
||||||
if (!node.tier || !indexes.has(node.tier)) throw new Error(`Node ${node.name} is not in the tier configuration`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export * as LayerNode from "./layer-node"
|
export * as LayerNode from "./layer-node"
|
||||||
|
|
|
||||||
32
packages/core/src/effect/scoped-node-build.ts
Normal file
32
packages/core/src/effect/scoped-node-build.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Layer } from "effect"
|
||||||
|
import { buildLocationServiceMap, type LocationError, type LocationServices } from "../location-layer"
|
||||||
|
import { LocationServiceMap, node as locationServiceMapNode } from "../location-service-map"
|
||||||
|
import { LayerNode } from "./layer-node"
|
||||||
|
import { LayerNodeTree } from "./layer-node-tree"
|
||||||
|
import { makeGlobalNode, tiers } from "./scoped-node"
|
||||||
|
|
||||||
|
export function build<A, E>(
|
||||||
|
root: LayerNode.Node<A, E, any>,
|
||||||
|
replacements?: readonly LayerNode.Replacement[],
|
||||||
|
): Layer.Layer<LocationServiceMap | A, LocationError | E> {
|
||||||
|
const separated = LayerNodeTree.separate(root, tiers)
|
||||||
|
|
||||||
|
const location = LayerNodeTree.hoist(separated.location, tiers.values.location, tiers)
|
||||||
|
|
||||||
|
const replacementMap = new Map(replacements?.map((item) => [item.source, item.replacement]))
|
||||||
|
|
||||||
|
const locationMap = buildLocationServiceMap(
|
||||||
|
location.node as unknown as LayerNode.Node<LocationServices, LocationError, any>,
|
||||||
|
replacementMap,
|
||||||
|
).pipe(Layer.provide(LayerNodeTree.compile(location.hoisted, replacementMap)))
|
||||||
|
|
||||||
|
const locationMapNode = makeGlobalNode({ service: LocationServiceMap, layer: locationMap, deps: [] })
|
||||||
|
const globalNode = LayerNodeTree.bind(separated.global, locationServiceMapNode, locationMapNode)
|
||||||
|
|
||||||
|
return LayerNodeTree.compile(globalNode, replacementMap).pipe(Layer.provideMerge(locationMap)) as Layer.Layer<
|
||||||
|
LocationServiceMap | A,
|
||||||
|
LocationError | E
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
|
export * as ScopedNodeBuild from "./scoped-node-build"
|
||||||
|
|
@ -7,7 +7,7 @@ import { and, asc, eq, gt } from "drizzle-orm"
|
||||||
import { Database } from "./database/database"
|
import { Database } from "./database/database"
|
||||||
import { EventSequenceTable, EventTable } from "./event/sql"
|
import { EventSequenceTable, EventTable } from "./event/sql"
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
import { LayerNode } from "./effect/layer-node"
|
import { makeGlobalNode } from "./effect/scoped-node"
|
||||||
import { isDeepStrictEqual } from "node:util"
|
import { isDeepStrictEqual } from "node:util"
|
||||||
import { Durable } from "@opencode-ai/schema/durable-event-manifest"
|
import { Durable } from "@opencode-ai/schema/durable-event-manifest"
|
||||||
|
|
||||||
|
|
@ -569,6 +569,6 @@ export const layerWith = (options?: LayerOptions) =>
|
||||||
)
|
)
|
||||||
|
|
||||||
export const layer = layerWith()
|
export const layer = layerWith()
|
||||||
export const node = LayerNode.make({ service: Service, layer: layer, deps: [Database.node] })
|
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Database.node] })
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as FileMutation from "./file-mutation"
|
export * as FileMutation from "./file-mutation"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { dirname } from "path"
|
import { dirname } from "path"
|
||||||
import { KeyedMutex } from "./effect/keyed-mutex"
|
import { KeyedMutex } from "./effect/keyed-mutex"
|
||||||
|
|
@ -192,6 +193,8 @@ function sameBytes(left: Uint8Array, right: Uint8Array) {
|
||||||
|
|
||||||
export const locationLayer = layer
|
export const locationLayer = layer
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deferred until the corresponding V2 integrations exist.
|
* Deferred until the corresponding V2 integrations exist.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as FileSystem from "./filesystem"
|
export * as FileSystem from "./filesystem"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { FSUtil } from "./fs-util"
|
import { FSUtil } from "./fs-util"
|
||||||
|
|
@ -113,3 +114,9 @@ const baseLayer = Layer.effect(
|
||||||
export const layer = baseLayer.pipe(Layer.provide(FileSystemSearch.locationLayer), Layer.provide(FSUtil.defaultLayer))
|
export const layer = baseLayer.pipe(Layer.provide(FileSystemSearch.locationLayer), Layer.provide(FSUtil.defaultLayer))
|
||||||
|
|
||||||
export const locationLayer = layer
|
export const locationLayer = layer
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer: baseLayer,
|
||||||
|
deps: [FSUtil.node, Location.node, FileSystemSearch.node],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as FileSystemSearch from "./search"
|
export * as FileSystemSearch from "./search"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "../effect/scoped-node"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Context, Effect, Layer, Scope } from "effect"
|
import { Context, Effect, Layer, Scope } from "effect"
|
||||||
import { Fff } from "#fff"
|
import { Fff } from "#fff"
|
||||||
|
|
@ -229,6 +230,8 @@ export const fffLayer = Layer.effect(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = Layer.unwrap(
|
const layer = Layer.unwrap(Effect.sync(() => (Flag.OPENCODE_DISABLE_FFF || !Fff.available() ? ripgrepLayer : fffLayer)))
|
||||||
Effect.sync(() => (Flag.OPENCODE_DISABLE_FFF || !Fff.available() ? ripgrepLayer : fffLayer)),
|
|
||||||
)
|
export const locationLayer = layer
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Location.node, Ripgrep.node] })
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export * as Watcher from "./watcher"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { createWrapper } from "@parcel/watcher/wrapper"
|
import { createWrapper } from "@parcel/watcher/wrapper"
|
||||||
import type ParcelWatcher from "@parcel/watcher"
|
import type ParcelWatcher from "@parcel/watcher"
|
||||||
|
import { makeLocationNode } from "../effect/scoped-node"
|
||||||
import { Cause, Context, Effect, Layer } from "effect"
|
import { Cause, Context, Effect, Layer } from "effect"
|
||||||
import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher"
|
import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
@ -133,3 +134,9 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer), Layer.provide(Git.defaultLayer))
|
export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer), Layer.provide(Git.defaultLayer))
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer,
|
||||||
|
deps: [FSUtil.node, Location.node, Config.node, Git.node, EventV2.node],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Context, Effect, FileSystem, Layer, Schema } from "effect"
|
||||||
import type { PlatformError } from "effect/PlatformError"
|
import type { PlatformError } from "effect/PlatformError"
|
||||||
import { Glob } from "./util/glob"
|
import { Glob } from "./util/glob"
|
||||||
import { serviceUse } from "./effect/service-use"
|
import { serviceUse } from "./effect/service-use"
|
||||||
import { LayerNode } from "./effect/layer-node"
|
import { makeGlobalNode } from "./effect/scoped-node"
|
||||||
import { filesystem } from "./effect/layer-node-platform"
|
import { filesystem } from "./effect/layer-node-platform"
|
||||||
|
|
||||||
export namespace FSUtil {
|
export namespace FSUtil {
|
||||||
|
|
@ -201,7 +201,7 @@ export namespace FSUtil {
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer))
|
export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer))
|
||||||
export const node = LayerNode.make({ service: Service, layer: layer, deps: [filesystem] })
|
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [filesystem] })
|
||||||
|
|
||||||
// Pure helpers that don't need Effect (path manipulation, sync operations)
|
// Pure helpers that don't need Effect (path manipulation, sync operations)
|
||||||
export function mimeType(p: string): string {
|
export function mimeType(p: string): string {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { ChildProcess } from "effect/unstable/process"
|
||||||
import { AbsolutePath, RelativePath } from "./schema"
|
import { AbsolutePath, RelativePath } from "./schema"
|
||||||
import { FSUtil } from "./fs-util"
|
import { FSUtil } from "./fs-util"
|
||||||
import { AppProcess } from "./process"
|
import { AppProcess } from "./process"
|
||||||
import { LayerNode } from "./effect/layer-node"
|
import { makeGlobalNode } from "./effect/scoped-node"
|
||||||
import { File } from "./file"
|
import { File } from "./file"
|
||||||
import { KeyedMutex } from "./effect/keyed-mutex"
|
import { KeyedMutex } from "./effect/keyed-mutex"
|
||||||
|
|
||||||
|
|
@ -944,7 +944,7 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(AppProcess.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(AppProcess.defaultLayer))
|
||||||
export const node = LayerNode.make({ service: Service, layer: layer, deps: [FSUtil.node, AppProcess.node] })
|
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [FSUtil.node, AppProcess.node] })
|
||||||
|
|
||||||
interface Result {
|
interface Result {
|
||||||
readonly exitCode: number
|
readonly exitCode: number
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import os from "os"
|
||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { Flock } from "./util/flock"
|
import { Flock } from "./util/flock"
|
||||||
import { Flag } from "./flag/flag"
|
import { Flag } from "./flag/flag"
|
||||||
import { LayerNode } from "./effect/layer-node"
|
import { makeGlobalNode } from "./effect/scoped-node"
|
||||||
|
|
||||||
const app = "opencode"
|
const app = "opencode"
|
||||||
const data = path.join(xdgData!, app)
|
const data = path.join(xdgData!, app)
|
||||||
|
|
@ -77,7 +77,7 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer
|
export const defaultLayer = layer
|
||||||
export const node = LayerNode.make({ service: Service, layer: layer, deps: [] })
|
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [] })
|
||||||
|
|
||||||
export const layerWith = (input: Partial<Interface>) =>
|
export const layerWith = (input: Partial<Interface>) =>
|
||||||
Layer.effect(
|
Layer.effect(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as Image from "./image"
|
export * as Image from "./image"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { Config } from "./config"
|
import { Config } from "./config"
|
||||||
import { FileSystem } from "./filesystem"
|
import { FileSystem } from "./filesystem"
|
||||||
|
|
@ -76,3 +77,5 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer))
|
export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer))
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [Config.node] })
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { Location } from "./location"
|
||||||
import { AbsolutePath } from "./schema"
|
import { AbsolutePath } from "./schema"
|
||||||
import { SystemContext } from "./system-context/index"
|
import { SystemContext } from "./system-context/index"
|
||||||
import { SystemContextRegistry } from "./system-context/registry"
|
import { SystemContextRegistry } from "./system-context/registry"
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
|
|
||||||
class File extends Schema.Class<File>("InstructionContext.File")({
|
class File extends Schema.Class<File>("InstructionContext.File")({
|
||||||
path: AbsolutePath,
|
path: AbsolutePath,
|
||||||
|
|
@ -87,6 +88,12 @@ export const layer = Layer.effectDiscard(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
name: "instruction-context",
|
||||||
|
layer,
|
||||||
|
deps: [FSUtil.node, Global.node, Location.node, SystemContextRegistry.node],
|
||||||
|
})
|
||||||
|
|
||||||
function render(files: ReadonlyArray<File>) {
|
function render(files: ReadonlyArray<File>) {
|
||||||
return files.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n")
|
return files.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as Integration from "./integration"
|
export * as Integration from "./integration"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import {
|
import {
|
||||||
Cause,
|
Cause,
|
||||||
Clock,
|
Clock,
|
||||||
|
|
@ -515,3 +516,5 @@ export const locationLayer = Layer.effect(
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [Credential.node, EventV2.node] })
|
||||||
|
|
|
||||||
|
|
@ -1,151 +1,125 @@
|
||||||
import { Effect, Layer, LayerMap } from "effect"
|
import { Effect, Layer, LayerMap } from "effect"
|
||||||
import { Location } from "./location"
|
|
||||||
import { Policy } from "./policy"
|
|
||||||
import { Config } from "./config"
|
|
||||||
import { PluginV2 } from "./plugin"
|
|
||||||
import { Catalog } from "./catalog"
|
|
||||||
import { Integration } from "./integration"
|
|
||||||
import { CommandV2 } from "./command"
|
|
||||||
import { AgentV2 } from "./agent"
|
import { AgentV2 } from "./agent"
|
||||||
import { PluginInternal } from "./plugin/internal"
|
import { AISDK } from "./aisdk"
|
||||||
import { Project } from "./project"
|
import { Catalog } from "./catalog"
|
||||||
import { ProjectCopy } from "./project/copy"
|
import { CommandV2 } from "./command"
|
||||||
import { ProjectDirectories } from "./project/directories"
|
import { Config } from "./config"
|
||||||
import { EventV2 } from "./event"
|
import { LayerNode } from "./effect/layer-node"
|
||||||
import { Credential } from "./credential"
|
import { LayerNodeTree } from "./effect/layer-node-tree"
|
||||||
import { Npm } from "./npm"
|
import { ScopedNode } from "./effect/scoped-node"
|
||||||
import { ModelsDev } from "./models-dev"
|
|
||||||
import { FSUtil } from "./fs-util"
|
|
||||||
import { Git } from "./git"
|
|
||||||
import { Global } from "./global"
|
|
||||||
import { Database } from "./database/database"
|
|
||||||
import { PermissionV2 } from "./permission"
|
|
||||||
import { PermissionSaved } from "./permission/saved"
|
|
||||||
import { FileSystem } from "./filesystem"
|
|
||||||
import { Ripgrep } from "./ripgrep"
|
|
||||||
import { Watcher } from "./filesystem/watcher"
|
|
||||||
import { LocationMutation } from "./location-mutation"
|
|
||||||
import { FileMutation } from "./file-mutation"
|
import { FileMutation } from "./file-mutation"
|
||||||
|
import { FileSystem } from "./filesystem"
|
||||||
|
import { FileSystemSearch } from "./filesystem/search"
|
||||||
|
import { Watcher } from "./filesystem/watcher"
|
||||||
|
import { Image } from "./image"
|
||||||
|
import { Integration } from "./integration"
|
||||||
|
import { Location } from "./location"
|
||||||
|
import { LocationMutation } from "./location-mutation"
|
||||||
|
import { LocationServiceMap } from "./location-service-map"
|
||||||
|
import { PermissionV2 } from "./permission"
|
||||||
|
import { PluginV2 } from "./plugin"
|
||||||
|
import { PluginInternal } from "./plugin/internal"
|
||||||
|
import { Policy } from "./policy"
|
||||||
|
import { ProjectCopy } from "./project/copy"
|
||||||
|
import { Pty } from "./pty"
|
||||||
|
import { QuestionV2 } from "./question"
|
||||||
import { Reference } from "./reference"
|
import { Reference } from "./reference"
|
||||||
import { ReferenceGuidance } from "./reference/guidance"
|
import { ReferenceGuidance } from "./reference/guidance"
|
||||||
import { RepositoryCache } from "./repository-cache"
|
|
||||||
import { Pty } from "./pty"
|
|
||||||
import { SkillV2 } from "./skill"
|
|
||||||
import { SkillGuidance } from "./skill/guidance"
|
|
||||||
import { BuiltInTools } from "./tool/builtins"
|
|
||||||
import { Image } from "./image"
|
|
||||||
import { ToolRegistry } from "./tool/registry"
|
|
||||||
import { ApplicationTools } from "./tool/application-tools"
|
|
||||||
import { ToolOutputStore } from "./tool-output-store"
|
|
||||||
import { AppProcess } from "./process"
|
|
||||||
import { SessionStore } from "./session/store"
|
|
||||||
import { SessionTodo } from "./session/todo"
|
|
||||||
import { QuestionV2 } from "./question"
|
|
||||||
import { LLMClient } from "@opencode-ai/llm"
|
|
||||||
import { RequestExecutor } from "@opencode-ai/llm/route"
|
|
||||||
import * as SessionRunnerLLM from "./session/runner/llm"
|
import * as SessionRunnerLLM from "./session/runner/llm"
|
||||||
import { SessionRunnerModel } from "./session/runner/model"
|
import { SessionRunnerModel } from "./session/runner/model"
|
||||||
import { SystemContextBuiltIns } from "./system-context/builtins"
|
import { SessionTodo } from "./session/todo"
|
||||||
import { FetchHttpClient } from "effect/unstable/http"
|
import { SkillV2 } from "./skill"
|
||||||
|
import { SkillGuidance } from "./skill/guidance"
|
||||||
import { Snapshot } from "./snapshot"
|
import { Snapshot } from "./snapshot"
|
||||||
|
import { SystemContextBuiltIns } from "./system-context/builtins"
|
||||||
|
import { SystemContextRegistry } from "./system-context/registry"
|
||||||
|
import { BuiltInTools } from "./tool/builtins"
|
||||||
|
import { ReadToolFileSystem } from "./tool/read-filesystem"
|
||||||
|
import { ToolRegistry } from "./tool/registry"
|
||||||
|
import { ToolOutputStore } from "./tool-output-store"
|
||||||
|
|
||||||
export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("@opencode/example/LocationServiceMap", {
|
export { LocationServiceMap, node } from "./location-service-map"
|
||||||
lookup: (ref: Location.Ref) => {
|
|
||||||
const boot = Layer.effectDiscard(
|
|
||||||
Effect.logInfo("booting location services", { directory: ref.directory, workspaceID: ref.workspaceID }),
|
|
||||||
)
|
|
||||||
const location = Location.layer(ref)
|
|
||||||
const systemContext = SystemContextBuiltIns.locationLayer
|
|
||||||
const base = Layer.mergeAll(
|
|
||||||
location,
|
|
||||||
Policy.locationLayer,
|
|
||||||
Config.locationLayer,
|
|
||||||
Reference.locationLayer,
|
|
||||||
PluginV2.locationLayer,
|
|
||||||
Catalog.locationLayer,
|
|
||||||
Integration.locationLayer,
|
|
||||||
CommandV2.locationLayer,
|
|
||||||
AgentV2.locationLayer,
|
|
||||||
PluginInternal.locationLayer,
|
|
||||||
ProjectCopy.locationLayer,
|
|
||||||
FileSystem.locationLayer,
|
|
||||||
Watcher.locationLayer,
|
|
||||||
Pty.locationLayer,
|
|
||||||
SkillV2.locationLayer,
|
|
||||||
systemContext,
|
|
||||||
LocationMutation.locationLayer.pipe(Layer.orDie),
|
|
||||||
).pipe(Layer.provideMerge(location))
|
|
||||||
const resources = ToolOutputStore.layer.pipe(Layer.provide(base))
|
|
||||||
const permissionsAndTools = ToolRegistry.layer.pipe(
|
|
||||||
Layer.provideMerge(PermissionV2.locationLayer),
|
|
||||||
Layer.provide(resources),
|
|
||||||
Layer.provide(base),
|
|
||||||
)
|
|
||||||
const services = Layer.mergeAll(base, resources, permissionsAndTools)
|
|
||||||
const image = Image.layer.pipe(Layer.provide(services))
|
|
||||||
const mutation = FileMutation.locationLayer.pipe(Layer.provide(services))
|
|
||||||
const skillGuidance = SkillGuidance.locationLayer.pipe(Layer.provide(services))
|
|
||||||
const referenceGuidance = ReferenceGuidance.locationLayer.pipe(Layer.provide(services))
|
|
||||||
const todos = SessionTodo.layer.pipe(Layer.provide(services))
|
|
||||||
const questions = QuestionV2.locationLayer.pipe(Layer.provide(services))
|
|
||||||
const builtInTools = BuiltInTools.locationLayer.pipe(
|
|
||||||
Layer.provide(services),
|
|
||||||
Layer.provide(mutation),
|
|
||||||
Layer.provide(resources),
|
|
||||||
Layer.provide(todos),
|
|
||||||
Layer.provide(questions),
|
|
||||||
Layer.provide(image),
|
|
||||||
)
|
|
||||||
const model = SessionRunnerModel.locationLayer.pipe(Layer.provide(services))
|
|
||||||
const snapshot = Snapshot.locationLayer.pipe(Layer.provide(services))
|
|
||||||
const runner = SessionRunnerLLM.defaultLayer.pipe(
|
|
||||||
Layer.provide(services),
|
|
||||||
Layer.provide(model),
|
|
||||||
Layer.provide(skillGuidance),
|
|
||||||
Layer.provide(referenceGuidance),
|
|
||||||
Layer.provide(snapshot),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Kick off a background project copy refresh to update locations now that we
|
export const locationServices = LayerNode.group([
|
||||||
// have a location
|
Location.node,
|
||||||
const projectCopyRefresh = Layer.effectDiscard(ProjectCopy.refreshAfterBoot).pipe(Layer.provide(services))
|
Policy.node,
|
||||||
|
Config.node,
|
||||||
|
AgentV2.node,
|
||||||
|
CommandV2.node,
|
||||||
|
Reference.node,
|
||||||
|
Integration.node,
|
||||||
|
Catalog.node,
|
||||||
|
AISDK.node,
|
||||||
|
PluginV2.node,
|
||||||
|
PluginInternal.node,
|
||||||
|
ProjectCopy.node,
|
||||||
|
ProjectCopy.refreshNode,
|
||||||
|
FileSystemSearch.node,
|
||||||
|
FileSystem.node,
|
||||||
|
Watcher.node,
|
||||||
|
Pty.node,
|
||||||
|
SkillV2.node,
|
||||||
|
SystemContextRegistry.node,
|
||||||
|
SystemContextBuiltIns.node,
|
||||||
|
LocationMutation.node,
|
||||||
|
FileMutation.node,
|
||||||
|
PermissionV2.node,
|
||||||
|
ToolOutputStore.node,
|
||||||
|
ToolRegistry.node,
|
||||||
|
ToolRegistry.toolsNode,
|
||||||
|
Image.node,
|
||||||
|
SkillGuidance.node,
|
||||||
|
ReferenceGuidance.node,
|
||||||
|
SessionTodo.node,
|
||||||
|
QuestionV2.node,
|
||||||
|
ReadToolFileSystem.node,
|
||||||
|
BuiltInTools.node,
|
||||||
|
SessionRunnerModel.node,
|
||||||
|
Snapshot.node,
|
||||||
|
SessionRunnerLLM.node,
|
||||||
|
])
|
||||||
|
|
||||||
return Layer.mergeAll(
|
type NodeOutput<N> = N extends LayerNode.Node<infer A, unknown, any> ? A : never
|
||||||
boot,
|
type NodeError<N> = N extends LayerNode.Node<unknown, infer E, any> ? E : never
|
||||||
services,
|
export type LocationServices = NodeOutput<typeof locationServices>
|
||||||
image,
|
export type LocationError = NodeError<typeof locationServices>
|
||||||
mutation,
|
export type LocationTierServices = Exclude<LocationServices, Location.Service>
|
||||||
resources,
|
|
||||||
todos,
|
export function buildLocationServiceMap(
|
||||||
questions,
|
location: LayerNode.Node<LocationServices, LocationError, any>,
|
||||||
model,
|
replacements?: ReadonlyMap<Layer.Any, Layer.Any>,
|
||||||
snapshot,
|
): Layer.Layer<LocationServiceMap, LocationError> {
|
||||||
runner,
|
return Layer.effect(
|
||||||
builtInTools,
|
LocationServiceMap,
|
||||||
referenceGuidance,
|
LayerMap.make(
|
||||||
projectCopyRefresh,
|
(ref: Location.Ref) => {
|
||||||
).pipe(Layer.fresh)
|
const layer = LayerNodeTree.compile(
|
||||||
},
|
LayerNodeTree.bind(location, Location.node, Location.boundNode(ref)),
|
||||||
idleTimeToLive: "60 minutes",
|
replacements,
|
||||||
dependencies: [
|
) as Layer.Layer<LocationServices, LocationError>
|
||||||
Project.defaultLayer,
|
return layer.pipe(
|
||||||
EventV2.defaultLayer,
|
Layer.fresh,
|
||||||
Credential.defaultLayer,
|
Layer.tap(() =>
|
||||||
Npm.defaultLayer,
|
Effect.logInfo("booting location services", {
|
||||||
ModelsDev.defaultLayer,
|
directory: ref.directory,
|
||||||
FSUtil.defaultLayer,
|
workspaceID: ref.workspaceID,
|
||||||
Git.defaultLayer,
|
}),
|
||||||
AppProcess.defaultLayer,
|
),
|
||||||
Global.defaultLayer,
|
)
|
||||||
Ripgrep.defaultLayer,
|
},
|
||||||
Database.defaultLayer,
|
{ idleTimeToLive: "60 minutes" },
|
||||||
ProjectDirectories.defaultLayer,
|
),
|
||||||
SessionStore.layer.pipe(Layer.provide(Database.defaultLayer)),
|
)
|
||||||
PermissionSaved.defaultLayer,
|
}
|
||||||
RepositoryCache.defaultLayer,
|
|
||||||
LLMClient.layer.pipe(Layer.provide(RequestExecutor.defaultLayer)),
|
// This is temporary for backwards compatibility
|
||||||
FetchHttpClient.layer,
|
const separatedLocationServices = LayerNodeTree.separate(locationServices, ScopedNode.tiers)
|
||||||
ToolOutputStore.defaultCleanupLayer,
|
const hoistedLocationServices = LayerNodeTree.hoist(
|
||||||
ApplicationTools.layer,
|
separatedLocationServices.location,
|
||||||
],
|
ScopedNode.tiers.values.location,
|
||||||
}) {}
|
ScopedNode.tiers,
|
||||||
|
)
|
||||||
|
|
||||||
|
export const locationServiceMapLayer = buildLocationServiceMap(
|
||||||
|
hoistedLocationServices.node as LayerNode.Node<LocationServices, LocationError, any>,
|
||||||
|
).pipe(Layer.provide(LayerNodeTree.compile(hoistedLocationServices.hoisted))) as Layer.Layer<LocationServiceMap>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as LocationMutation from "./location-mutation"
|
export * as LocationMutation from "./location-mutation"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { FSUtil } from "./fs-util"
|
import { FSUtil } from "./fs-util"
|
||||||
|
|
@ -153,3 +154,9 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer
|
export const locationLayer = layer
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer: layer.pipe(Layer.orDie),
|
||||||
|
deps: [FSUtil.node, Location.node],
|
||||||
|
})
|
||||||
|
|
|
||||||
16
packages/core/src/location-service-map.ts
Normal file
16
packages/core/src/location-service-map.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Context, Effect, Layer, LayerMap } from "effect"
|
||||||
|
import { LayerNode } from "./effect/layer-node"
|
||||||
|
import { ScopedNode } from "./effect/scoped-node"
|
||||||
|
import { Location } from "./location"
|
||||||
|
import type { LocationError, LocationServices } from "./location-layer"
|
||||||
|
|
||||||
|
export class LocationServiceMap extends Context.Service<
|
||||||
|
LocationServiceMap,
|
||||||
|
LayerMap.LayerMap<Location.Ref, LocationServices, LocationError>
|
||||||
|
>()("@opencode/example/LocationServiceMap") {
|
||||||
|
static get(ref: Location.Ref) {
|
||||||
|
return Layer.unwrap(Effect.map(LocationServiceMap, (locations) => locations.get(ref)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const node = LayerNode.unbound(LocationServiceMap, ScopedNode.tiers.values.global)
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { Info, Ref, response } from "@opencode-ai/schema/location"
|
import { Info, Ref, response } from "@opencode-ai/schema/location"
|
||||||
import { Project } from "./project"
|
import { Project } from "./project"
|
||||||
|
import { LayerNode } from "./effect/layer-node"
|
||||||
|
import { makeLocationNode, tiers } from "./effect/scoped-node"
|
||||||
|
|
||||||
export * as Location from "./location"
|
export * as Location from "./location"
|
||||||
|
|
||||||
|
|
@ -12,6 +14,8 @@ export interface Interface extends Info {
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Location") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Location") {}
|
||||||
|
|
||||||
|
export const node = LayerNode.unbound(Service, tiers.values.location)
|
||||||
|
|
||||||
export const layer = (ref: Ref) =>
|
export const layer = (ref: Ref) =>
|
||||||
Layer.effect(
|
Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
|
|
@ -26,3 +30,10 @@ export const layer = (ref: Ref) =>
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const boundNode = (ref: Ref) =>
|
||||||
|
makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer: layer(ref),
|
||||||
|
deps: [Project.node],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { Hash } from "./util/hash"
|
||||||
import { FSUtil } from "./fs-util"
|
import { FSUtil } from "./fs-util"
|
||||||
import { InstallationChannel, InstallationVersion } from "./installation/version"
|
import { InstallationChannel, InstallationVersion } from "./installation/version"
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
import { LayerNode } from "./effect/layer-node"
|
import { makeGlobalNode } from "./effect/scoped-node"
|
||||||
import { httpClient } from "./effect/layer-node-platform"
|
import { httpClient } from "./effect/layer-node-platform"
|
||||||
|
|
||||||
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
|
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
|
||||||
|
|
@ -244,6 +244,6 @@ export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(FSUtil.defaultLayer),
|
Layer.provide(FSUtil.defaultLayer),
|
||||||
Layer.provide(EventV2.defaultLayer),
|
Layer.provide(EventV2.defaultLayer),
|
||||||
)
|
)
|
||||||
export const node = LayerNode.make({ service: Service, layer: layer, deps: [FSUtil.node, EventV2.node, httpClient] })
|
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [FSUtil.node, EventV2.node, httpClient] })
|
||||||
|
|
||||||
export * as ModelsDev from "./models-dev"
|
export * as ModelsDev from "./models-dev"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { NodeFileSystem } from "@effect/platform-node"
|
||||||
import { FSUtil } from "./fs-util"
|
import { FSUtil } from "./fs-util"
|
||||||
import { Global } from "./global"
|
import { Global } from "./global"
|
||||||
import { EffectFlock } from "./util/effect-flock"
|
import { EffectFlock } from "./util/effect-flock"
|
||||||
import { LayerNode } from "./effect/layer-node"
|
import { makeGlobalNode } from "./effect/scoped-node"
|
||||||
import { filesystem } from "./effect/layer-node-platform"
|
import { filesystem } from "./effect/layer-node-platform"
|
||||||
import { makeRuntime } from "./effect/runtime"
|
import { makeRuntime } from "./effect/runtime"
|
||||||
import { NpmConfig } from "./npm-config"
|
import { NpmConfig } from "./npm-config"
|
||||||
|
|
@ -253,7 +253,7 @@ export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(Global.layer),
|
Layer.provide(Global.layer),
|
||||||
Layer.provide(NodeFileSystem.layer),
|
Layer.provide(NodeFileSystem.layer),
|
||||||
)
|
)
|
||||||
export const node = LayerNode.make({
|
export const node = makeGlobalNode({
|
||||||
service: Service,
|
service: Service,
|
||||||
layer: layer,
|
layer: layer,
|
||||||
deps: [FSUtil.node, Global.node, filesystem, EffectFlock.node],
|
deps: [FSUtil.node, Global.node, filesystem, EffectFlock.node],
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as PermissionV2 from "./permission"
|
export * as PermissionV2 from "./permission"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import { Context, Deferred, Effect as EffectRuntime, Layer, Schema } from "effect"
|
import { Context, Deferred, Effect as EffectRuntime, Layer, Schema } from "effect"
|
||||||
import { Permission } from "@opencode-ai/schema/permission"
|
import { Permission } from "@opencode-ai/schema/permission"
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
|
|
@ -300,3 +301,9 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer.pipe(Layer.provideMerge(AgentV2.locationLayer))
|
export const locationLayer = layer.pipe(Layer.provideMerge(AgentV2.locationLayer))
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer,
|
||||||
|
deps: [EventV2.node, Location.node, AgentV2.node, SessionStore.node, PermissionSaved.node],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export * as PermissionSaved from "./saved"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { Database } from "../database/database"
|
import { Database } from "../database/database"
|
||||||
|
import { makeGlobalNode } from "../effect/scoped-node"
|
||||||
import { ProjectV2 } from "../project"
|
import { ProjectV2 } from "../project"
|
||||||
import { PermissionTable } from "./sql"
|
import { PermissionTable } from "./sql"
|
||||||
import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
|
import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
|
||||||
|
|
@ -76,3 +77,5 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
|
||||||
|
|
||||||
|
export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as PluginV2 from "./plugin"
|
export * as PluginV2 from "./plugin"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect"
|
import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect"
|
||||||
import type { Plugin as PluginRuntime } from "@opencode-ai/plugin/v2/effect"
|
import type { Plugin as PluginRuntime } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { Plugin } from "@opencode-ai/schema/plugin"
|
import { Plugin } from "@opencode-ai/schema/plugin"
|
||||||
|
|
@ -150,3 +151,18 @@ export const locationLayer = layer.pipe(
|
||||||
Layer.provideMerge(Reference.locationLayer),
|
Layer.provideMerge(Reference.locationLayer),
|
||||||
Layer.provideMerge(SkillV2.locationLayer),
|
Layer.provideMerge(SkillV2.locationLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer,
|
||||||
|
deps: [
|
||||||
|
EventV2.node,
|
||||||
|
AgentV2.node,
|
||||||
|
AISDK.node,
|
||||||
|
Catalog.node,
|
||||||
|
CommandV2.node,
|
||||||
|
Integration.node,
|
||||||
|
Reference.node,
|
||||||
|
SkillV2.node,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
export * as PluginInternal from "./internal"
|
export * as PluginInternal from "./internal"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "../effect/scoped-node"
|
||||||
|
import { httpClient } from "../effect/layer-node-platform"
|
||||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { Effect, Layer, Scope } from "effect"
|
import { Effect, Layer, Scope } from "effect"
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
|
|
@ -57,7 +59,7 @@ export function define<R>(plugin: Plugin<R>) {
|
||||||
return plugin
|
return plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
export const locationLayer = Layer.effectDiscard(
|
const layer = Layer.effectDiscard(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const commands = yield* CommandV2.Service
|
const commands = yield* CommandV2.Service
|
||||||
|
|
@ -117,9 +119,34 @@ export const locationLayer = Layer.effectDiscard(
|
||||||
yield* add(VariantPlugin.Plugin)
|
yield* add(VariantPlugin.Plugin)
|
||||||
}).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
|
}).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
|
||||||
}),
|
}),
|
||||||
).pipe(
|
)
|
||||||
|
|
||||||
|
export const locationLayer = layer.pipe(
|
||||||
Layer.provideMerge(PluginV2.locationLayer),
|
Layer.provideMerge(PluginV2.locationLayer),
|
||||||
Layer.provideMerge(Config.locationLayer),
|
Layer.provideMerge(Config.locationLayer),
|
||||||
Layer.provideMerge(FileSystem.locationLayer),
|
Layer.provideMerge(FileSystem.locationLayer),
|
||||||
Layer.provideMerge(FetchHttpClient.layer),
|
Layer.provideMerge(FetchHttpClient.layer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
name: "plugin-internal",
|
||||||
|
layer,
|
||||||
|
deps: [
|
||||||
|
Catalog.node,
|
||||||
|
CommandV2.node,
|
||||||
|
PluginV2.node,
|
||||||
|
Integration.node,
|
||||||
|
AgentV2.node,
|
||||||
|
Config.node,
|
||||||
|
Location.node,
|
||||||
|
ModelsDev.node,
|
||||||
|
Npm.node,
|
||||||
|
EventV2.node,
|
||||||
|
FSUtil.node,
|
||||||
|
FileSystem.node,
|
||||||
|
Global.node,
|
||||||
|
httpClient,
|
||||||
|
SkillV2.node,
|
||||||
|
Reference.node,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as Policy from "./policy"
|
export * as Policy from "./policy"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import { Context, Effect as EffectRuntime, Layer, Schema } from "effect"
|
import { Context, Effect as EffectRuntime, Layer, Schema } from "effect"
|
||||||
import { Wildcard } from "./util/wildcard"
|
import { Wildcard } from "./util/wildcard"
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
|
|
@ -44,3 +45,5 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer
|
export const locationLayer = layer
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [Location.node] })
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import type { PlatformError } from "effect/PlatformError"
|
||||||
import { ChildProcess } from "effect/unstable/process"
|
import { ChildProcess } from "effect/unstable/process"
|
||||||
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
||||||
import { CrossSpawnSpawner } from "./cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "./cross-spawn-spawner"
|
||||||
import { LayerNode } from "./effect/layer-node"
|
import { makeGlobalNode } from "./effect/scoped-node"
|
||||||
|
|
||||||
export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()("AppProcessError", {
|
export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()("AppProcessError", {
|
||||||
command: Schema.String,
|
command: Schema.String,
|
||||||
|
|
@ -238,6 +238,6 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
|
||||||
export const node = LayerNode.make({ service: Service, layer: layer, deps: [CrossSpawnSpawner.node] })
|
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [CrossSpawnSpawner.node] })
|
||||||
|
|
||||||
export * as AppProcess from "./process"
|
export * as AppProcess from "./process"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import path from "path"
|
||||||
import { AbsolutePath } from "./schema"
|
import { AbsolutePath } from "./schema"
|
||||||
import { FSUtil } from "./fs-util"
|
import { FSUtil } from "./fs-util"
|
||||||
import { Git } from "./git"
|
import { Git } from "./git"
|
||||||
import { LayerNode } from "./effect/layer-node"
|
import { makeGlobalNode } from "./effect/scoped-node"
|
||||||
import { Hash } from "./util/hash"
|
import { Hash } from "./util/hash"
|
||||||
import { ProjectDirectories } from "./project/directories"
|
import { ProjectDirectories } from "./project/directories"
|
||||||
import { ProjectSchema } from "./project/schema"
|
import { ProjectSchema } from "./project/schema"
|
||||||
|
|
@ -134,7 +134,7 @@ export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(Git.defaultLayer),
|
Layer.provide(Git.defaultLayer),
|
||||||
Layer.provideMerge(ProjectDirectories.defaultLayer),
|
Layer.provideMerge(ProjectDirectories.defaultLayer),
|
||||||
)
|
)
|
||||||
export const node = LayerNode.make({
|
export const node = makeGlobalNode({
|
||||||
service: Service,
|
service: Service,
|
||||||
layer: layer,
|
layer: layer,
|
||||||
deps: [FSUtil.node, Git.node, ProjectDirectories.node],
|
deps: [FSUtil.node, Git.node, ProjectDirectories.node],
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import path from "path"
|
||||||
import { AbsolutePath } from "../schema"
|
import { AbsolutePath } from "../schema"
|
||||||
import { FSUtil } from "../fs-util"
|
import { FSUtil } from "../fs-util"
|
||||||
import { Git } from "../git"
|
import { Git } from "../git"
|
||||||
import { LayerNode } from "../effect/layer-node"
|
import { makeLocationNode } from "../effect/scoped-node"
|
||||||
import { Project } from "../project"
|
import { Project } from "../project"
|
||||||
import { ProjectDirectories } from "./directories"
|
import { ProjectDirectories } from "./directories"
|
||||||
import { makeGitWorktreeStrategy } from "./copy-strategies"
|
import { makeGitWorktreeStrategy } from "./copy-strategies"
|
||||||
|
|
@ -279,8 +279,14 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer
|
export const locationLayer = layer
|
||||||
export const node = LayerNode.make({
|
export const node = makeLocationNode({
|
||||||
service: Service,
|
service: Service,
|
||||||
layer: layer,
|
layer: layer,
|
||||||
deps: [FSUtil.node, Git.node, ProjectDirectories.node, EventV2.node, Database.node],
|
deps: [FSUtil.node, Git.node, ProjectDirectories.node, EventV2.node, Database.node],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const refreshNode = makeLocationNode({
|
||||||
|
name: "project-copy-refresh",
|
||||||
|
layer: Layer.effectDiscard(refreshAfterBoot),
|
||||||
|
deps: [node, Location.node],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ export * as ProjectDirectories from "./directories"
|
||||||
import { and, asc, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm"
|
import { and, asc, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { Database } from "../database/database"
|
import { Database } from "../database/database"
|
||||||
import { LayerNode } from "../effect/layer-node"
|
import { makeGlobalNode } from "../effect/scoped-node"
|
||||||
import { AbsolutePath, optional } from "../schema"
|
import { AbsolutePath, optional } from "../schema"
|
||||||
import { ProjectSchema } from "./schema"
|
import { ProjectSchema } from "./schema"
|
||||||
import { ProjectDirectoryTable } from "./sql"
|
import { ProjectDirectoryTable } from "./sql"
|
||||||
|
|
@ -156,4 +156,4 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
|
||||||
export const node = LayerNode.make({ service: Service, layer: layer, deps: [Database.node] })
|
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Database.node] })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as Pty from "./pty"
|
export * as Pty from "./pty"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import type { Disp, Proc } from "#pty"
|
import type { Disp, Proc } from "#pty"
|
||||||
import { Context, Effect, Layer, Schema, Types } from "effect"
|
import { Context, Effect, Layer, Schema, Types } from "effect"
|
||||||
import { Pty } from "@opencode-ai/schema/pty"
|
import { Pty } from "@opencode-ai/schema/pty"
|
||||||
|
|
@ -313,3 +314,5 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer))
|
export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer))
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Location.node, Config.node] })
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { WorkspaceV2 } from "../workspace"
|
||||||
import { PtyTicket } from "@opencode-ai/schema/pty-ticket"
|
import { PtyTicket } from "@opencode-ai/schema/pty-ticket"
|
||||||
import { PtyID } from "./schema"
|
import { PtyID } from "./schema"
|
||||||
import { Cache, Context, Duration, Effect, Layer } from "effect"
|
import { Cache, Context, Duration, Effect, Layer } from "effect"
|
||||||
import { LayerNode } from "../effect/layer-node"
|
import { makeGlobalNode } from "../effect/scoped-node"
|
||||||
|
|
||||||
const DEFAULT_TTL = Duration.seconds(60)
|
const DEFAULT_TTL = Duration.seconds(60)
|
||||||
const CAPACITY = 10_000
|
const CAPACITY = 10_000
|
||||||
|
|
@ -54,4 +54,4 @@ export const make = (ttl: Duration.Input = DEFAULT_TTL) =>
|
||||||
export const layer = Layer.effect(Service, make())
|
export const layer = Layer.effect(Service, make())
|
||||||
|
|
||||||
export const defaultLayer = layer
|
export const defaultLayer = layer
|
||||||
export const node = LayerNode.make({ service: Service, layer: layer, deps: [] })
|
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [] })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as QuestionV2 from "./question"
|
export * as QuestionV2 from "./question"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import { Context, Deferred, Effect, Layer, Schema } from "effect"
|
import { Context, Deferred, Effect, Layer, Schema } from "effect"
|
||||||
import { Question } from "@opencode-ai/schema/question"
|
import { Question } from "@opencode-ai/schema/question"
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
|
|
@ -148,3 +149,5 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer
|
export const locationLayer = layer
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as Reference from "./reference"
|
export * as Reference from "./reference"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import { Context, Effect, Layer, Scope, Types } from "effect"
|
import { Context, Effect, Layer, Scope, Types } from "effect"
|
||||||
import { Reference } from "@opencode-ai/schema/reference"
|
import { Reference } from "@opencode-ai/schema/reference"
|
||||||
import { Global } from "./global"
|
import { Global } from "./global"
|
||||||
|
|
@ -120,3 +121,9 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer
|
export const locationLayer = layer
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer,
|
||||||
|
deps: [Global.node, EventV2.node, RepositoryCache.node],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as ReferenceGuidance from "./guidance"
|
export * as ReferenceGuidance from "./guidance"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "../effect/scoped-node"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { Reference } from "../reference"
|
import { Reference } from "../reference"
|
||||||
import { SystemContext } from "../system-context/index"
|
import { SystemContext } from "../system-context/index"
|
||||||
|
|
@ -64,3 +65,5 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer
|
export const locationLayer = layer
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [Reference.node] })
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Git } from "./git"
|
||||||
import { Global } from "./global"
|
import { Global } from "./global"
|
||||||
import { Repository } from "./repository"
|
import { Repository } from "./repository"
|
||||||
import { AbsolutePath } from "./schema"
|
import { AbsolutePath } from "./schema"
|
||||||
|
import { makeGlobalNode } from "./effect/scoped-node"
|
||||||
import { EffectFlock } from "./util/effect-flock"
|
import { EffectFlock } from "./util/effect-flock"
|
||||||
|
|
||||||
export type Result = {
|
export type Result = {
|
||||||
|
|
@ -229,6 +230,12 @@ export const defaultLayer: Layer.Layer<Service> = layer.pipe(
|
||||||
Layer.provide(Global.defaultLayer),
|
Layer.provide(Global.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeGlobalNode({
|
||||||
|
service: Service,
|
||||||
|
layer,
|
||||||
|
deps: [EffectFlock.node, FSUtil.node, Git.node, Global.node],
|
||||||
|
})
|
||||||
|
|
||||||
function statusForRepository(input: { reuse: boolean; refresh?: boolean; branchMatches?: boolean }) {
|
function statusForRepository(input: { reuse: boolean; refresh?: boolean; branchMatches?: boolean }) {
|
||||||
if (!input.reuse) return "cloned" as const
|
if (!input.reuse) return "cloned" as const
|
||||||
if (input.branchMatches === false || input.refresh) return "refreshed" as const
|
if (input.branchMatches === false || input.refresh) return "refreshed" as const
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ export * as Ripgrep from "./ripgrep"
|
||||||
import { Context, Effect, Fiber, Layer, Schema, Stream } from "effect"
|
import { Context, Effect, Fiber, Layer, Schema, Stream } from "effect"
|
||||||
import { ChildProcess } from "effect/unstable/process"
|
import { ChildProcess } from "effect/unstable/process"
|
||||||
import { Entry, Match } from "@opencode-ai/schema/filesystem"
|
import { Entry, Match } from "@opencode-ai/schema/filesystem"
|
||||||
import { LayerNode } from "./effect/layer-node"
|
import { makeGlobalNode } from "./effect/scoped-node"
|
||||||
import { AppProcess, collectStream, waitForAbort } from "./process"
|
import { AppProcess, collectStream, waitForAbort } from "./process"
|
||||||
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
|
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
|
||||||
import { RipgrepBinary } from "./ripgrep/binary"
|
import { RipgrepBinary } from "./ripgrep/binary"
|
||||||
|
|
@ -279,4 +279,4 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(Layer.merge(RipgrepBinary.defaultLayer, AppProcess.defaultLayer)))
|
export const defaultLayer = layer.pipe(Layer.provide(Layer.merge(RipgrepBinary.defaultLayer, AppProcess.defaultLayer)))
|
||||||
export const node = LayerNode.make({ service: Service, layer: layer, deps: [RipgrepBinary.node, AppProcess.node] })
|
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [RipgrepBinary.node, AppProcess.node] })
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/
|
||||||
import { ChildProcess } from "effect/unstable/process"
|
import { ChildProcess } from "effect/unstable/process"
|
||||||
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
||||||
import { CrossSpawnSpawner } from "../cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "../cross-spawn-spawner"
|
||||||
import { LayerNode } from "../effect/layer-node"
|
import { makeGlobalNode } from "../effect/scoped-node"
|
||||||
import { httpClient } from "../effect/layer-node-platform"
|
import { httpClient } from "../effect/layer-node-platform"
|
||||||
import { FSUtil } from "../fs-util"
|
import { FSUtil } from "../fs-util"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
|
|
@ -130,7 +130,7 @@ export namespace RipgrepBinary {
|
||||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const node = LayerNode.make({
|
export const node = makeGlobalNode({
|
||||||
service: Service,
|
service: Service,
|
||||||
layer: layer,
|
layer: layer,
|
||||||
deps: [FSUtil.node, httpClient, CrossSpawnSpawner.node],
|
deps: [FSUtil.node, httpClient, CrossSpawnSpawner.node],
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ import { fromRow } from "./session/info"
|
||||||
import { SessionRunner } from "./session/runner/index"
|
import { SessionRunner } from "./session/runner/index"
|
||||||
import { SessionStore } from "./session/store"
|
import { SessionStore } from "./session/store"
|
||||||
import { SessionExecution } from "./session/execution"
|
import { SessionExecution } from "./session/execution"
|
||||||
|
import { SessionExecutionLocal } from "./session/execution/local"
|
||||||
|
import { makeGlobalNode } from "./effect/scoped-node"
|
||||||
|
import { LocationServiceMap, node as locationServiceMapNode } from "./location-service-map"
|
||||||
import { MessageDecodeError } from "./session/error"
|
import { MessageDecodeError } from "./session/error"
|
||||||
import { SessionEvent } from "./session/event"
|
import { SessionEvent } from "./session/event"
|
||||||
import { SessionInput } from "./session/input"
|
import { SessionInput } from "./session/input"
|
||||||
|
|
@ -170,279 +173,267 @@ export interface Interface {
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Session") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Session") {}
|
||||||
|
|
||||||
export const layer = Layer.unwrap(
|
export const layer = Layer.effect(
|
||||||
Effect.promise(() => import("./location-layer")).pipe(
|
Service,
|
||||||
Effect.map(({ LocationServiceMap }) =>
|
Effect.gen(function* () {
|
||||||
Layer.effect(
|
const database = yield* Database.Service
|
||||||
Service,
|
const db = database.db
|
||||||
Effect.gen(function* () {
|
const events = yield* EventV2.Service
|
||||||
const database = yield* Database.Service
|
const projects = yield* ProjectV2.Service
|
||||||
const db = database.db
|
const execution = yield* SessionExecution.Service
|
||||||
const events = yield* EventV2.Service
|
const store = yield* SessionStore.Service
|
||||||
const projects = yield* ProjectV2.Service
|
const locations = yield* LocationServiceMap
|
||||||
const execution = yield* SessionExecution.Service
|
const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message)
|
||||||
const store = yield* SessionStore.Service
|
const isDurableSessionEvent = Schema.is(SessionEvent.Durable)
|
||||||
const locations = yield* LocationServiceMap
|
const decode = (row: typeof SessionMessageTable.$inferSelect) =>
|
||||||
const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message)
|
decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe(
|
||||||
const isDurableSessionEvent = Schema.is(SessionEvent.Durable)
|
Effect.mapError(
|
||||||
const decode = (row: typeof SessionMessageTable.$inferSelect) =>
|
() =>
|
||||||
decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe(
|
new MessageDecodeError({
|
||||||
Effect.mapError(
|
sessionID: SessionSchema.ID.make(row.session_id),
|
||||||
() =>
|
messageID: SessionMessage.ID.make(row.id),
|
||||||
new MessageDecodeError({
|
}),
|
||||||
sessionID: SessionSchema.ID.make(row.session_id),
|
),
|
||||||
messageID: SessionMessage.ID.make(row.id),
|
)
|
||||||
}),
|
|
||||||
|
const result = Service.of({
|
||||||
|
create: Effect.fn("V2Session.create")(function* (input) {
|
||||||
|
const sessionID = input.id ?? SessionSchema.ID.create()
|
||||||
|
const recorded = yield* store.get(sessionID)
|
||||||
|
if (recorded) return recorded
|
||||||
|
const project = yield* projects.resolve(input.location.directory)
|
||||||
|
yield* db
|
||||||
|
.insert(ProjectTable)
|
||||||
|
.values({ id: project.id, worktree: project.directory, vcs: project.vcs?.type, sandboxes: [] })
|
||||||
|
.onConflictDoNothing()
|
||||||
|
.run()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
const now = Date.now()
|
||||||
|
const info = SessionV1.SessionInfo.make({
|
||||||
|
id: sessionID,
|
||||||
|
slug: Slug.create(),
|
||||||
|
version: InstallationVersion,
|
||||||
|
projectID: project.id,
|
||||||
|
directory: input.location.directory,
|
||||||
|
path: path.relative(project.directory, input.location.directory).replaceAll("\\", "/"),
|
||||||
|
workspaceID: input.location.workspaceID ? WorkspaceV2.ID.make(input.location.workspaceID) : undefined,
|
||||||
|
title: `New session - ${new Date(now).toISOString()}`,
|
||||||
|
agent: input.agent,
|
||||||
|
model: input.model
|
||||||
|
? {
|
||||||
|
id: ModelV2.ID.make(input.model.id),
|
||||||
|
providerID: input.model.providerID,
|
||||||
|
variant: input.model.variant,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
cost: 0,
|
||||||
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||||
|
time: { created: now, updated: now },
|
||||||
|
})
|
||||||
|
const projected = yield* events
|
||||||
|
.publish(SessionV1.Event.Created, { sessionID, info }, { location: input.location })
|
||||||
|
.pipe(
|
||||||
|
Effect.as({ type: "created" } as const),
|
||||||
|
Effect.catchDefect((defect) => {
|
||||||
|
if (!(defect instanceof SessionProjector.SessionAlreadyProjected)) {
|
||||||
|
return Effect.die(defect)
|
||||||
|
}
|
||||||
|
// Concurrent creation lost the projection race. The existing Session identity wins.
|
||||||
|
return store
|
||||||
|
.get(sessionID)
|
||||||
|
.pipe(
|
||||||
|
Effect.flatMap((session) =>
|
||||||
|
session ? Effect.succeed({ type: "existing", session } as const) : Effect.die(defect),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if (projected.type === "existing") return projected.session
|
||||||
|
// TODO: Restore recorded sessions onto replacement synchronized workspaces in a future API slice.
|
||||||
|
return yield* result.get(sessionID).pipe(Effect.orDie)
|
||||||
|
}),
|
||||||
|
get: Effect.fn("V2Session.get")(function* (sessionID) {
|
||||||
|
const session = yield* store.get(sessionID)
|
||||||
|
if (!session) return yield* new NotFoundError({ sessionID })
|
||||||
|
return session
|
||||||
|
}),
|
||||||
|
list: Effect.fn("V2Session.list")(function* (input = {}) {
|
||||||
|
const direction = input.anchor?.direction ?? "next"
|
||||||
|
const requestedOrder = input.order ?? "desc"
|
||||||
|
const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder
|
||||||
|
const sortColumn = SessionTable.time_created
|
||||||
|
const conditions: SQL[] = []
|
||||||
|
if ("directory" in input) conditions.push(eq(SessionTable.directory, input.directory))
|
||||||
|
if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
|
||||||
|
if ("project" in input) conditions.push(eq(SessionTable.project_id, input.project))
|
||||||
|
if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`))
|
||||||
|
if (input.anchor) {
|
||||||
|
conditions.push(
|
||||||
|
order === "asc"
|
||||||
|
? or(
|
||||||
|
gt(sortColumn, input.anchor.time),
|
||||||
|
and(eq(sortColumn, input.anchor.time), gt(SessionTable.id, input.anchor.id)),
|
||||||
|
)!
|
||||||
|
: or(
|
||||||
|
lt(sortColumn, input.anchor.time),
|
||||||
|
and(eq(sortColumn, input.anchor.time), lt(SessionTable.id, input.anchor.id)),
|
||||||
|
)!,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const query = db
|
||||||
|
.select()
|
||||||
|
.from(SessionTable)
|
||||||
|
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||||
|
.orderBy(
|
||||||
|
order === "asc" ? asc(sortColumn) : desc(sortColumn),
|
||||||
|
order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id),
|
||||||
|
)
|
||||||
|
const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe(
|
||||||
|
Effect.orDie,
|
||||||
|
)
|
||||||
|
return (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row))
|
||||||
|
}),
|
||||||
|
messages: Effect.fn("V2Session.messages")(function* (input) {
|
||||||
|
yield* result.get(input.sessionID)
|
||||||
|
const direction = input.cursor?.direction ?? "next"
|
||||||
|
const requestedOrder = input.order ?? "desc"
|
||||||
|
const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder
|
||||||
|
const anchor = input.cursor
|
||||||
|
? yield* db
|
||||||
|
.select({ seq: SessionMessageTable.seq })
|
||||||
|
.from(SessionMessageTable)
|
||||||
|
.where(
|
||||||
|
and(eq(SessionMessageTable.session_id, input.sessionID), eq(SessionMessageTable.id, input.cursor.id)),
|
||||||
|
)
|
||||||
|
.get()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
: undefined
|
||||||
|
if (input.cursor && !anchor) return []
|
||||||
|
const boundary = anchor
|
||||||
|
? order === "asc"
|
||||||
|
? gt(SessionMessageTable.seq, anchor.seq)
|
||||||
|
: lt(SessionMessageTable.seq, anchor.seq)
|
||||||
|
: undefined
|
||||||
|
const where = boundary
|
||||||
|
? and(eq(SessionMessageTable.session_id, input.sessionID), boundary)
|
||||||
|
: eq(SessionMessageTable.session_id, input.sessionID)
|
||||||
|
const query = db
|
||||||
|
.select()
|
||||||
|
.from(SessionMessageTable)
|
||||||
|
.where(where)
|
||||||
|
.orderBy(order === "asc" ? asc(SessionMessageTable.seq) : desc(SessionMessageTable.seq))
|
||||||
|
const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe(
|
||||||
|
Effect.orDie,
|
||||||
|
)
|
||||||
|
return yield* Effect.forEach(direction === "previous" ? rows.toReversed() : rows, decode)
|
||||||
|
}),
|
||||||
|
message: Effect.fn("V2Session.message")(function* (input) {
|
||||||
|
const stored = yield* store.message(input.messageID)
|
||||||
|
return stored?.sessionID === input.sessionID ? stored.message : undefined
|
||||||
|
}),
|
||||||
|
context: Effect.fn("V2Session.context")(function* (sessionID) {
|
||||||
|
yield* result.get(sessionID)
|
||||||
|
return yield* store.context(sessionID)
|
||||||
|
}),
|
||||||
|
events: (input) =>
|
||||||
|
Stream.unwrap(
|
||||||
|
result
|
||||||
|
.get(input.sessionID)
|
||||||
|
.pipe(Effect.as(events.durable({ aggregateID: input.sessionID, after: input.after }))),
|
||||||
|
).pipe(Stream.filter((event): event is SessionEvent.DurableEvent => isDurableSessionEvent(event))),
|
||||||
|
prompt: Effect.fn("V2Session.prompt")((input) =>
|
||||||
|
Effect.uninterruptible(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* result.get(input.sessionID)
|
||||||
|
const messageID = input.id ?? SessionMessage.ID.create()
|
||||||
|
const delivery = input.delivery ?? "steer"
|
||||||
|
const expected = { sessionID: input.sessionID, messageID, prompt: input.prompt, delivery }
|
||||||
|
const admitted = yield* SessionInput.admit(db, events, {
|
||||||
|
id: messageID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
prompt: input.prompt,
|
||||||
|
delivery,
|
||||||
|
}).pipe(
|
||||||
|
Effect.catchDefect((defect) =>
|
||||||
|
defect instanceof SessionInput.LifecycleConflict
|
||||||
|
? new PromptConflictError({ sessionID: input.sessionID, messageID })
|
||||||
|
: Effect.die(defect),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
if (!SessionInput.equivalent(admitted, expected))
|
||||||
const result = Service.of({
|
return yield* new PromptConflictError({ sessionID: input.sessionID, messageID })
|
||||||
create: Effect.fn("V2Session.create")(function* (input) {
|
if (input.resume !== false) yield* execution.wake(admitted.sessionID)
|
||||||
const sessionID = input.id ?? SessionSchema.ID.create()
|
return admitted
|
||||||
const recorded = yield* store.get(sessionID)
|
}),
|
||||||
if (recorded) return recorded
|
),
|
||||||
const project = yield* projects.resolve(input.location.directory)
|
|
||||||
yield* db
|
|
||||||
.insert(ProjectTable)
|
|
||||||
.values({ id: project.id, worktree: project.directory, vcs: project.vcs?.type, sandboxes: [] })
|
|
||||||
.onConflictDoNothing()
|
|
||||||
.run()
|
|
||||||
.pipe(Effect.orDie)
|
|
||||||
const now = Date.now()
|
|
||||||
const info = SessionV1.SessionInfo.make({
|
|
||||||
id: sessionID,
|
|
||||||
slug: Slug.create(),
|
|
||||||
version: InstallationVersion,
|
|
||||||
projectID: project.id,
|
|
||||||
directory: input.location.directory,
|
|
||||||
path: path.relative(project.directory, input.location.directory).replaceAll("\\", "/"),
|
|
||||||
workspaceID: input.location.workspaceID ? WorkspaceV2.ID.make(input.location.workspaceID) : undefined,
|
|
||||||
title: `New session - ${new Date(now).toISOString()}`,
|
|
||||||
agent: input.agent,
|
|
||||||
model: input.model
|
|
||||||
? {
|
|
||||||
id: ModelV2.ID.make(input.model.id),
|
|
||||||
providerID: input.model.providerID,
|
|
||||||
variant: input.model.variant,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
cost: 0,
|
|
||||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
||||||
time: { created: now, updated: now },
|
|
||||||
})
|
|
||||||
const projected = yield* events
|
|
||||||
.publish(SessionV1.Event.Created, { sessionID, info }, { location: input.location })
|
|
||||||
.pipe(
|
|
||||||
Effect.as({ type: "created" } as const),
|
|
||||||
Effect.catchDefect((defect) => {
|
|
||||||
if (!(defect instanceof SessionProjector.SessionAlreadyProjected)) {
|
|
||||||
return Effect.die(defect)
|
|
||||||
}
|
|
||||||
// Concurrent creation lost the projection race. The existing Session identity wins.
|
|
||||||
return store
|
|
||||||
.get(sessionID)
|
|
||||||
.pipe(
|
|
||||||
Effect.flatMap((session) =>
|
|
||||||
session ? Effect.succeed({ type: "existing", session } as const) : Effect.die(defect),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
if (projected.type === "existing") return projected.session
|
|
||||||
// TODO: Restore recorded sessions onto replacement synchronized workspaces in a future API slice.
|
|
||||||
return yield* result.get(sessionID).pipe(Effect.orDie)
|
|
||||||
}),
|
|
||||||
get: Effect.fn("V2Session.get")(function* (sessionID) {
|
|
||||||
const session = yield* store.get(sessionID)
|
|
||||||
if (!session) return yield* new NotFoundError({ sessionID })
|
|
||||||
return session
|
|
||||||
}),
|
|
||||||
list: Effect.fn("V2Session.list")(function* (input = {}) {
|
|
||||||
const direction = input.anchor?.direction ?? "next"
|
|
||||||
const requestedOrder = input.order ?? "desc"
|
|
||||||
const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder
|
|
||||||
const sortColumn = SessionTable.time_created
|
|
||||||
const conditions: SQL[] = []
|
|
||||||
if ("directory" in input) conditions.push(eq(SessionTable.directory, input.directory))
|
|
||||||
if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
|
|
||||||
if ("project" in input) conditions.push(eq(SessionTable.project_id, input.project))
|
|
||||||
if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`))
|
|
||||||
if (input.anchor) {
|
|
||||||
conditions.push(
|
|
||||||
order === "asc"
|
|
||||||
? or(
|
|
||||||
gt(sortColumn, input.anchor.time),
|
|
||||||
and(eq(sortColumn, input.anchor.time), gt(SessionTable.id, input.anchor.id)),
|
|
||||||
)!
|
|
||||||
: or(
|
|
||||||
lt(sortColumn, input.anchor.time),
|
|
||||||
and(eq(sortColumn, input.anchor.time), lt(SessionTable.id, input.anchor.id)),
|
|
||||||
)!,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const query = db
|
|
||||||
.select()
|
|
||||||
.from(SessionTable)
|
|
||||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
|
||||||
.orderBy(
|
|
||||||
order === "asc" ? asc(sortColumn) : desc(sortColumn),
|
|
||||||
order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id),
|
|
||||||
)
|
|
||||||
const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe(
|
|
||||||
Effect.orDie,
|
|
||||||
)
|
|
||||||
return (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row))
|
|
||||||
}),
|
|
||||||
messages: Effect.fn("V2Session.messages")(function* (input) {
|
|
||||||
yield* result.get(input.sessionID)
|
|
||||||
const direction = input.cursor?.direction ?? "next"
|
|
||||||
const requestedOrder = input.order ?? "desc"
|
|
||||||
const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder
|
|
||||||
const anchor = input.cursor
|
|
||||||
? yield* db
|
|
||||||
.select({ seq: SessionMessageTable.seq })
|
|
||||||
.from(SessionMessageTable)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(SessionMessageTable.session_id, input.sessionID),
|
|
||||||
eq(SessionMessageTable.id, input.cursor.id),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.get()
|
|
||||||
.pipe(Effect.orDie)
|
|
||||||
: undefined
|
|
||||||
if (input.cursor && !anchor) return []
|
|
||||||
const boundary = anchor
|
|
||||||
? order === "asc"
|
|
||||||
? gt(SessionMessageTable.seq, anchor.seq)
|
|
||||||
: lt(SessionMessageTable.seq, anchor.seq)
|
|
||||||
: undefined
|
|
||||||
const where = boundary
|
|
||||||
? and(eq(SessionMessageTable.session_id, input.sessionID), boundary)
|
|
||||||
: eq(SessionMessageTable.session_id, input.sessionID)
|
|
||||||
const query = db
|
|
||||||
.select()
|
|
||||||
.from(SessionMessageTable)
|
|
||||||
.where(where)
|
|
||||||
.orderBy(order === "asc" ? asc(SessionMessageTable.seq) : desc(SessionMessageTable.seq))
|
|
||||||
const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe(
|
|
||||||
Effect.orDie,
|
|
||||||
)
|
|
||||||
return yield* Effect.forEach(direction === "previous" ? rows.toReversed() : rows, decode)
|
|
||||||
}),
|
|
||||||
message: Effect.fn("V2Session.message")(function* (input) {
|
|
||||||
const stored = yield* store.message(input.messageID)
|
|
||||||
return stored?.sessionID === input.sessionID ? stored.message : undefined
|
|
||||||
}),
|
|
||||||
context: Effect.fn("V2Session.context")(function* (sessionID) {
|
|
||||||
yield* result.get(sessionID)
|
|
||||||
return yield* store.context(sessionID)
|
|
||||||
}),
|
|
||||||
events: (input) =>
|
|
||||||
Stream.unwrap(
|
|
||||||
result
|
|
||||||
.get(input.sessionID)
|
|
||||||
.pipe(Effect.as(events.durable({ aggregateID: input.sessionID, after: input.after }))),
|
|
||||||
).pipe(Stream.filter((event): event is SessionEvent.DurableEvent => isDurableSessionEvent(event))),
|
|
||||||
prompt: Effect.fn("V2Session.prompt")((input) =>
|
|
||||||
Effect.uninterruptible(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
yield* result.get(input.sessionID)
|
|
||||||
const messageID = input.id ?? SessionMessage.ID.create()
|
|
||||||
const delivery = input.delivery ?? "steer"
|
|
||||||
const expected = { sessionID: input.sessionID, messageID, prompt: input.prompt, delivery }
|
|
||||||
const admitted = yield* SessionInput.admit(db, events, {
|
|
||||||
id: messageID,
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
prompt: input.prompt,
|
|
||||||
delivery,
|
|
||||||
}).pipe(
|
|
||||||
Effect.catchDefect((defect) =>
|
|
||||||
defect instanceof SessionInput.LifecycleConflict
|
|
||||||
? new PromptConflictError({ sessionID: input.sessionID, messageID })
|
|
||||||
: Effect.die(defect),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if (!SessionInput.equivalent(admitted, expected))
|
|
||||||
return yield* new PromptConflictError({ sessionID: input.sessionID, messageID })
|
|
||||||
if (input.resume !== false) yield* execution.wake(admitted.sessionID)
|
|
||||||
return admitted
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
shell: Effect.fn("V2Session.shell")(function* () {
|
|
||||||
return yield* new OperationUnavailableError({ operation: "shell" })
|
|
||||||
}),
|
|
||||||
skill: Effect.fn("V2Session.skill")(function* () {
|
|
||||||
return yield* new OperationUnavailableError({ operation: "skill" })
|
|
||||||
}),
|
|
||||||
switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) {
|
|
||||||
yield* result.get(input.sessionID)
|
|
||||||
yield* events.publish(SessionEvent.AgentSwitched, {
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
messageID: SessionMessage.ID.create(),
|
|
||||||
timestamp: yield* DateTime.now,
|
|
||||||
agent: input.agent,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
switchModel: Effect.fn("V2Session.switchModel")(function* (input) {
|
|
||||||
yield* result.get(input.sessionID)
|
|
||||||
yield* events.publish(SessionEvent.ModelSwitched, {
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
messageID: SessionMessage.ID.create(),
|
|
||||||
timestamp: yield* DateTime.now,
|
|
||||||
model: input.model,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
compact: Effect.fn("V2Session.compact")(function* (input) {
|
|
||||||
yield* result.get(input.sessionID)
|
|
||||||
return yield* new OperationUnavailableError({ operation: "compact" })
|
|
||||||
}),
|
|
||||||
wait: Effect.fn("V2Session.wait")(function* (sessionID) {
|
|
||||||
yield* result.get(sessionID)
|
|
||||||
return yield* new OperationUnavailableError({ operation: "wait" })
|
|
||||||
}),
|
|
||||||
resume: Effect.fn("V2Session.resume")(function* (sessionID) {
|
|
||||||
yield* result.get(sessionID)
|
|
||||||
yield* execution.resume(sessionID)
|
|
||||||
}),
|
|
||||||
interrupt: Effect.fn("V2Session.interrupt")((sessionID) =>
|
|
||||||
Effect.uninterruptible(execution.interrupt(sessionID)),
|
|
||||||
),
|
|
||||||
revert: {
|
|
||||||
stage: Effect.fn("V2Session.revert.stage")(function* (input) {
|
|
||||||
const session = yield* result.get(input.sessionID)
|
|
||||||
return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe(
|
|
||||||
Effect.provideService(Database.Service, database),
|
|
||||||
Effect.provideService(EventV2.Service, events),
|
|
||||||
Effect.provide(locations.get(session.location)),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) {
|
|
||||||
const session = yield* result.get(sessionID)
|
|
||||||
yield* SessionRevert.clear(session).pipe(
|
|
||||||
Effect.provideService(EventV2.Service, events),
|
|
||||||
Effect.provide(locations.get(session.location)),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) {
|
|
||||||
const session = yield* result.get(sessionID)
|
|
||||||
yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events))
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
),
|
shell: Effect.fn("V2Session.shell")(function* () {
|
||||||
),
|
return yield* new OperationUnavailableError({ operation: "shell" })
|
||||||
|
}),
|
||||||
|
skill: Effect.fn("V2Session.skill")(function* () {
|
||||||
|
return yield* new OperationUnavailableError({ operation: "skill" })
|
||||||
|
}),
|
||||||
|
switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) {
|
||||||
|
yield* result.get(input.sessionID)
|
||||||
|
yield* events.publish(SessionEvent.AgentSwitched, {
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
messageID: SessionMessage.ID.create(),
|
||||||
|
timestamp: yield* DateTime.now,
|
||||||
|
agent: input.agent,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
switchModel: Effect.fn("V2Session.switchModel")(function* (input) {
|
||||||
|
yield* result.get(input.sessionID)
|
||||||
|
yield* events.publish(SessionEvent.ModelSwitched, {
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
messageID: SessionMessage.ID.create(),
|
||||||
|
timestamp: yield* DateTime.now,
|
||||||
|
model: input.model,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
compact: Effect.fn("V2Session.compact")(function* (input) {
|
||||||
|
yield* result.get(input.sessionID)
|
||||||
|
return yield* new OperationUnavailableError({ operation: "compact" })
|
||||||
|
}),
|
||||||
|
wait: Effect.fn("V2Session.wait")(function* (sessionID) {
|
||||||
|
yield* result.get(sessionID)
|
||||||
|
return yield* new OperationUnavailableError({ operation: "wait" })
|
||||||
|
}),
|
||||||
|
resume: Effect.fn("V2Session.resume")(function* (sessionID) {
|
||||||
|
yield* result.get(sessionID)
|
||||||
|
yield* execution.resume(sessionID)
|
||||||
|
}),
|
||||||
|
interrupt: Effect.fn("V2Session.interrupt")((sessionID) =>
|
||||||
|
Effect.uninterruptible(execution.interrupt(sessionID)),
|
||||||
|
),
|
||||||
|
revert: {
|
||||||
|
stage: Effect.fn("V2Session.revert.stage")(function* (input) {
|
||||||
|
const session = yield* result.get(input.sessionID)
|
||||||
|
return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe(
|
||||||
|
Effect.provideService(Database.Service, database),
|
||||||
|
Effect.provideService(EventV2.Service, events),
|
||||||
|
Effect.provide(locations.get(session.location)),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) {
|
||||||
|
const session = yield* result.get(sessionID)
|
||||||
|
yield* SessionRevert.clear(session).pipe(
|
||||||
|
Effect.provideService(EventV2.Service, events),
|
||||||
|
Effect.provide(locations.get(session.location)),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) {
|
||||||
|
const session = yield* result.get(sessionID)
|
||||||
|
yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events))
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(
|
|
||||||
Layer.unwrap(Effect.promise(() => import("./location-layer")).pipe(Effect.map((m) => m.LocationServiceMap.layer))),
|
|
||||||
),
|
|
||||||
Layer.provide(SessionExecution.noopLayer),
|
Layer.provide(SessionExecution.noopLayer),
|
||||||
Layer.provide(SessionStore.defaultLayer),
|
Layer.provide(SessionStore.defaultLayer),
|
||||||
Layer.provide(SessionProjector.defaultLayer),
|
Layer.provide(SessionProjector.defaultLayer),
|
||||||
|
|
@ -451,3 +442,17 @@ export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(ProjectV2.defaultLayer),
|
Layer.provide(ProjectV2.defaultLayer),
|
||||||
Layer.orDie,
|
Layer.orDie,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeGlobalNode({
|
||||||
|
service: Service,
|
||||||
|
layer: layer.pipe(Layer.orDie),
|
||||||
|
deps: [
|
||||||
|
Database.node,
|
||||||
|
EventV2.node,
|
||||||
|
ProjectV2.node,
|
||||||
|
SessionExecutionLocal.node,
|
||||||
|
SessionStore.node,
|
||||||
|
locationServiceMapNode,
|
||||||
|
SessionProjector.node,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Cause, Effect, Layer } from "effect"
|
import { Cause, Effect, Layer } from "effect"
|
||||||
import { LocationServiceMap } from "../../location-layer"
|
import { LocationServiceMap, node as locationServiceMapNode } from "../../location-service-map"
|
||||||
|
import { makeGlobalNode } from "../../effect/scoped-node"
|
||||||
import { SessionRunCoordinator } from "../run-coordinator"
|
import { SessionRunCoordinator } from "../run-coordinator"
|
||||||
import { SessionRunner } from "../runner"
|
import { SessionRunner } from "../runner"
|
||||||
import { SessionSchema } from "../schema"
|
import { SessionSchema } from "../schema"
|
||||||
|
|
@ -36,3 +37,11 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(SessionStore.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(SessionStore.defaultLayer))
|
||||||
|
|
||||||
|
export const node = makeGlobalNode({
|
||||||
|
service: SessionExecution.Service,
|
||||||
|
layer,
|
||||||
|
deps: [SessionStore.node, locationServiceMapNode],
|
||||||
|
})
|
||||||
|
|
||||||
|
export * as SessionExecutionLocal from "./local"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { and, desc, eq, gt, or, sql } from "drizzle-orm"
|
||||||
import { DateTime, Effect, Layer, Schema } from "effect"
|
import { DateTime, Effect, Layer, Schema } from "effect"
|
||||||
import { Database } from "../database/database"
|
import { Database } from "../database/database"
|
||||||
import { EventV2 } from "../event"
|
import { EventV2 } from "../event"
|
||||||
import { LayerNode } from "../effect/layer-node"
|
import { makeGlobalNode } from "../effect/scoped-node"
|
||||||
import { SessionEvent } from "./event"
|
import { SessionEvent } from "./event"
|
||||||
import { SessionV1 } from "../v1/session"
|
import { SessionV1 } from "../v1/session"
|
||||||
import { WorkspaceTable } from "../control-plane/workspace.sql"
|
import { WorkspaceTable } from "../control-plane/workspace.sql"
|
||||||
|
|
@ -456,4 +456,4 @@ export const layer = Layer.effectDiscard(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(Database.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(Database.defaultLayer))
|
||||||
export const node = LayerNode.make({ name: "session-projector", layer, deps: [EventV2.node, Database.node] })
|
export const node = makeGlobalNode({ name: "session-projector", layer, deps: [EventV2.node, Database.node] })
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ import { createLLMEventPublisher } from "./publish-llm-event"
|
||||||
import { toLLMMessages } from "./to-llm-message"
|
import { toLLMMessages } from "./to-llm-message"
|
||||||
import { MAX_STEPS_PROMPT } from "./max-steps"
|
import { MAX_STEPS_PROMPT } from "./max-steps"
|
||||||
import { Snapshot } from "../../snapshot"
|
import { Snapshot } from "../../snapshot"
|
||||||
|
import { makeLocationNode } from "../../effect/scoped-node"
|
||||||
|
import { llmClient } from "../../effect/layer-node-platform"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs one durable coding-agent Session until it settles.
|
* Runs one durable coding-agent Session until it settles.
|
||||||
|
|
@ -404,3 +406,23 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer
|
export const defaultLayer = layer
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer,
|
||||||
|
deps: [
|
||||||
|
EventV2.node,
|
||||||
|
llmClient,
|
||||||
|
AgentV2.node,
|
||||||
|
ToolRegistry.node,
|
||||||
|
SessionRunnerModel.node,
|
||||||
|
SessionStore.node,
|
||||||
|
Location.node,
|
||||||
|
SystemContextRegistry.node,
|
||||||
|
SkillGuidance.node,
|
||||||
|
ReferenceGuidance.node,
|
||||||
|
Config.node,
|
||||||
|
Snapshot.node,
|
||||||
|
Database.node,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as SessionRunnerModel from "./model"
|
export * as SessionRunnerModel from "./model"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "../../effect/scoped-node"
|
||||||
import { type Model } from "@opencode-ai/llm"
|
import { type Model } from "@opencode-ai/llm"
|
||||||
import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
|
import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
|
||||||
import * as OpenAICompatibleChat from "@opencode-ai/llm/protocols/openai-compatible-chat"
|
import * as OpenAICompatibleChat from "@opencode-ai/llm/protocols/openai-compatible-chat"
|
||||||
|
|
@ -213,3 +214,5 @@ export const locationLayer = Layer.effect(
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [Catalog.node, Integration.node] })
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export * as SessionStore from "./store"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { Database } from "../database/database"
|
import { Database } from "../database/database"
|
||||||
|
import { makeGlobalNode } from "../effect/scoped-node"
|
||||||
import { SessionHistory } from "./history"
|
import { SessionHistory } from "./history"
|
||||||
import { MessageDecodeError } from "./error"
|
import { MessageDecodeError } from "./error"
|
||||||
import { SessionMessage } from "./message"
|
import { SessionMessage } from "./message"
|
||||||
|
|
@ -60,3 +61,5 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
|
||||||
|
|
||||||
|
export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] })
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { asc, eq } from "drizzle-orm"
|
||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { SessionTodo } from "@opencode-ai/schema/session-todo"
|
import { SessionTodo } from "@opencode-ai/schema/session-todo"
|
||||||
import { Database } from "../database/database"
|
import { Database } from "../database/database"
|
||||||
|
import { makeLocationNode } from "../effect/scoped-node"
|
||||||
import { EventV2 } from "../event"
|
import { EventV2 } from "../event"
|
||||||
import { SessionSchema } from "./schema"
|
import { SessionSchema } from "./schema"
|
||||||
import { TodoTable } from "./sql"
|
import { TodoTable } from "./sql"
|
||||||
|
|
@ -75,3 +76,5 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(Database.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(Database.defaultLayer))
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Database.node] })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as SkillV2 from "./skill"
|
export * as SkillV2 from "./skill"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Context, Effect, Layer, Schema, Types } from "effect"
|
import { Context, Effect, Layer, Schema, Types } from "effect"
|
||||||
import { Skill } from "@opencode-ai/schema/skill"
|
import { Skill } from "@opencode-ai/schema/skill"
|
||||||
|
|
@ -129,3 +130,5 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer.pipe(Layer.provide(SkillDiscovery.defaultLayer))
|
export const locationLayer = layer.pipe(Layer.provide(SkillDiscovery.defaultLayer))
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [SkillDiscovery.node, FSUtil.node] })
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { Context, Effect, Layer, Schedule, Schema } from "effect"
|
||||||
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||||
import { FSUtil } from "../fs-util"
|
import { FSUtil } from "../fs-util"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
|
import { makeGlobalNode } from "../effect/scoped-node"
|
||||||
|
import { httpClient } from "../effect/layer-node-platform"
|
||||||
import { AbsolutePath } from "../schema"
|
import { AbsolutePath } from "../schema"
|
||||||
|
|
||||||
const skillConcurrency = 4
|
const skillConcurrency = 4
|
||||||
|
|
@ -165,3 +167,5 @@ export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(FSUtil.defaultLayer),
|
Layer.provide(FSUtil.defaultLayer),
|
||||||
Layer.provide(Global.defaultLayer),
|
Layer.provide(Global.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeGlobalNode({ service: Service, layer, deps: [httpClient, FSUtil.node, Global.node] })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as SkillGuidance from "./guidance"
|
export * as SkillGuidance from "./guidance"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "../effect/scoped-node"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
import { PermissionV2 } from "../permission"
|
import { PermissionV2 } from "../permission"
|
||||||
|
|
@ -71,3 +72,5 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer
|
export const locationLayer = layer
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [SkillV2.node] })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as Snapshot from "./snapshot"
|
export * as Snapshot from "./snapshot"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "./effect/scoped-node"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { Config } from "./config"
|
import { Config } from "./config"
|
||||||
|
|
@ -228,6 +229,12 @@ export const layer = Layer.effect(
|
||||||
|
|
||||||
export const locationLayer = layer.pipe(Layer.provideMerge(Config.locationLayer))
|
export const locationLayer = layer.pipe(Layer.provideMerge(Config.locationLayer))
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer,
|
||||||
|
deps: [Config.node, FSUtil.node, Git.node, Global.node, Location.node],
|
||||||
|
})
|
||||||
|
|
||||||
export const noopLayer = Layer.succeed(
|
export const noopLayer = Layer.succeed(
|
||||||
Service,
|
Service,
|
||||||
Service.of({
|
Service.of({
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
export * as SystemContextBuiltIns from "./builtins"
|
export * as SystemContextBuiltIns from "./builtins"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "../effect/scoped-node"
|
||||||
import { DateTime, Effect, Layer, Schema } from "effect"
|
import { DateTime, Effect, Layer, Schema } from "effect"
|
||||||
import { Location } from "../location"
|
import { Location } from "../location"
|
||||||
import { SystemContext } from "./index"
|
import { SystemContext } from "./index"
|
||||||
import { InstructionContext } from "../instruction-context"
|
import { InstructionContext } from "../instruction-context"
|
||||||
import { SystemContextRegistry } from "./registry"
|
import { SystemContextRegistry } from "./registry"
|
||||||
|
import { FSUtil } from "../fs-util"
|
||||||
|
import { Global } from "../global"
|
||||||
|
|
||||||
const builtIns = Layer.effectDiscard(
|
const builtIns = Layer.effectDiscard(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -45,3 +48,9 @@ export const layer = Layer.mergeAll(builtIns, InstructionContext.layer).pipe(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer
|
export const locationLayer = layer
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
name: "system-context-builtins",
|
||||||
|
layer,
|
||||||
|
deps: [Location.node, SystemContextRegistry.node, InstructionContext.node, FSUtil.node, Global.node],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ export * as SystemContextRegistry from "./registry"
|
||||||
|
|
||||||
import { Context, Effect, Layer, Ref, Scope } from "effect"
|
import { Context, Effect, Layer, Ref, Scope } from "effect"
|
||||||
import { SystemContext } from "./index"
|
import { SystemContext } from "./index"
|
||||||
|
import { makeLocationNode } from "../effect/scoped-node"
|
||||||
|
|
||||||
export interface Entry {
|
export interface Entry {
|
||||||
readonly key: SystemContext.Key
|
readonly key: SystemContext.Key
|
||||||
|
|
@ -44,3 +45,5 @@ export const layer = Layer.effect(
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [] })
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effe
|
||||||
import { Config } from "./config"
|
import { Config } from "./config"
|
||||||
import { FSUtil } from "./fs-util"
|
import { FSUtil } from "./fs-util"
|
||||||
import { Global } from "./global"
|
import { Global } from "./global"
|
||||||
|
import { makeGlobalNode, makeLocationNode } from "./effect/scoped-node"
|
||||||
import { SessionSchema } from "./session/schema"
|
import { SessionSchema } from "./session/schema"
|
||||||
import { Identifier } from "./util/identifier"
|
import { Identifier } from "./util/identifier"
|
||||||
import type { ToolOutput } from "@opencode-ai/llm"
|
import type { ToolOutput } from "@opencode-ai/llm"
|
||||||
|
|
@ -193,6 +194,8 @@ export const layer = Layer.effect(
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(Global.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(Global.defaultLayer))
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Global.node, Config.node] })
|
||||||
|
|
||||||
/** Runs retention scanning once globally rather than once per active Location. */
|
/** Runs retention scanning once globally rather than once per active Location. */
|
||||||
export const cleanupLayer = Layer.effectDiscard(
|
export const cleanupLayer = Layer.effectDiscard(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -202,3 +205,5 @@ export const cleanupLayer = Layer.effectDiscard(
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultCleanupLayer = Layer.merge(defaultLayer, cleanupLayer.pipe(Layer.provide(defaultLayer)))
|
export const defaultCleanupLayer = Layer.merge(defaultLayer, cleanupLayer.pipe(Layer.provide(defaultLayer)))
|
||||||
|
|
||||||
|
export const cleanupNode = makeGlobalNode({ name: "tool-output-cleanup", layer: defaultCleanupLayer, deps: [] })
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export * as ApplicationTools from "./application-tools"
|
||||||
import { Context, Effect, Layer, Scope } from "effect"
|
import { Context, Effect, Layer, Scope } from "effect"
|
||||||
import { State } from "../state"
|
import { State } from "../state"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
|
import { makeGlobalNode } from "../effect/scoped-node"
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
readonly entries: Map<string, Entry>
|
readonly entries: Map<string, Entry>
|
||||||
|
|
@ -52,3 +53,5 @@ export const layer = Layer.effect(
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * as BuiltInTools from "./builtins"
|
export * as BuiltInTools from "./builtins"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "../effect/scoped-node"
|
||||||
import { Layer } from "effect"
|
import { Layer } from "effect"
|
||||||
import { BashTool } from "./bash"
|
import { BashTool } from "./bash"
|
||||||
import { ApplyPatchTool } from "./apply-patch"
|
import { ApplyPatchTool } from "./apply-patch"
|
||||||
|
|
@ -14,6 +15,20 @@ import { TodoWriteTool } from "./todowrite"
|
||||||
import { WebFetchTool } from "./webfetch"
|
import { WebFetchTool } from "./webfetch"
|
||||||
import { WebSearchTool } from "./websearch"
|
import { WebSearchTool } from "./websearch"
|
||||||
import { WriteTool } from "./write"
|
import { WriteTool } from "./write"
|
||||||
|
import { FSUtil } from "../fs-util"
|
||||||
|
import { AppProcess } from "../process"
|
||||||
|
import { Config } from "../config"
|
||||||
|
import { Location } from "../location"
|
||||||
|
import { LocationMutation } from "../location-mutation"
|
||||||
|
import { FileMutation } from "../file-mutation"
|
||||||
|
import { PermissionV2 } from "../permission"
|
||||||
|
import { Ripgrep } from "../ripgrep"
|
||||||
|
import { Image } from "../image"
|
||||||
|
import { QuestionV2 } from "../question"
|
||||||
|
import { SkillV2 } from "../skill"
|
||||||
|
import { SessionTodo } from "../session/todo"
|
||||||
|
import { ToolRegistry } from "./registry"
|
||||||
|
import { httpClient } from "../effect/layer-node-platform"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composes only the shipped Location-scoped built-in tool transforms.
|
* Composes only the shipped Location-scoped built-in tool transforms.
|
||||||
|
|
@ -42,3 +57,25 @@ export const locationLayer = Layer.mergeAll(
|
||||||
WebSearchTool.layer.pipe(Layer.provide(WebSearchTool.defaultConfigLayer)),
|
WebSearchTool.layer.pipe(Layer.provide(WebSearchTool.defaultConfigLayer)),
|
||||||
WriteTool.layer,
|
WriteTool.layer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
name: "built-in-tools",
|
||||||
|
layer: locationLayer,
|
||||||
|
deps: [
|
||||||
|
ToolRegistry.toolsNode,
|
||||||
|
FSUtil.node,
|
||||||
|
AppProcess.node,
|
||||||
|
Config.node,
|
||||||
|
Location.node,
|
||||||
|
LocationMutation.node,
|
||||||
|
FileMutation.node,
|
||||||
|
PermissionV2.node,
|
||||||
|
Ripgrep.node,
|
||||||
|
Image.node,
|
||||||
|
QuestionV2.node,
|
||||||
|
SkillV2.node,
|
||||||
|
SessionTodo.node,
|
||||||
|
ReadToolFileSystem.node,
|
||||||
|
httpClient,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { pathToFileURL } from "url"
|
||||||
import { Context, Effect, Layer, Option, Schema } from "effect"
|
import { Context, Effect, Layer, Option, Schema } from "effect"
|
||||||
import { FileSystem } from "../filesystem"
|
import { FileSystem } from "../filesystem"
|
||||||
import { FSUtil } from "../fs-util"
|
import { FSUtil } from "../fs-util"
|
||||||
|
import { makeLocationNode } from "../effect/scoped-node"
|
||||||
import { AbsolutePath, PositiveInt, RelativePath } from "../schema"
|
import { AbsolutePath, PositiveInt, RelativePath } from "../schema"
|
||||||
|
|
||||||
export const MAX_READ_LINES = 2_000
|
export const MAX_READ_LINES = 2_000
|
||||||
|
|
@ -361,3 +362,5 @@ export const layer = Layer.effect(
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] })
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { Wildcard } from "../util/wildcard"
|
||||||
import { ApplicationTools } from "./application-tools"
|
import { ApplicationTools } from "./application-tools"
|
||||||
import { definition, permission, settle, validateName, type AnyTool, type RegistrationError } from "./tool"
|
import { definition, permission, settle, validateName, type AnyTool, type RegistrationError } from "./tool"
|
||||||
import { Tools } from "./tools"
|
import { Tools } from "./tools"
|
||||||
|
import { makeLocationNode } from "../effect/scoped-node"
|
||||||
|
|
||||||
export type ExecuteInput = {
|
export type ExecuteInput = {
|
||||||
readonly sessionID: SessionSchema.ID
|
readonly sessionID: SessionSchema.ID
|
||||||
|
|
@ -137,3 +138,15 @@ export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(ApplicationTools.layer),
|
Layer.provide(ApplicationTools.layer),
|
||||||
Layer.provide(ToolOutputStore.defaultLayer),
|
Layer.provide(ToolOutputStore.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer,
|
||||||
|
deps: [ApplicationTools.node, ToolOutputStore.node],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const toolsNode = makeLocationNode({
|
||||||
|
service: Tools.Service,
|
||||||
|
layer,
|
||||||
|
deps: [ApplicationTools.node, ToolOutputStore.node],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import type { FileSystem, Scope } from "effect"
|
||||||
import type { PlatformError } from "effect/PlatformError"
|
import type { PlatformError } from "effect/PlatformError"
|
||||||
import { FSUtil } from "../fs-util"
|
import { FSUtil } from "../fs-util"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { LayerNode } from "../effect/layer-node"
|
import { makeGlobalNode } from "../effect/scoped-node"
|
||||||
import { Hash } from "./hash"
|
import { Hash } from "./hash"
|
||||||
|
|
||||||
export namespace EffectFlock {
|
export namespace EffectFlock {
|
||||||
|
|
@ -281,5 +281,5 @@ export namespace EffectFlock {
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(Global.layer))
|
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(Global.layer))
|
||||||
export const node = LayerNode.make({ service: Service, layer: layer, deps: [Global.node, FSUtil.node] })
|
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Global.node, FSUtil.node] })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Tool } from "@opencode-ai/core/tool/tool"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
|
@ -34,7 +34,7 @@ const applicationTools = ApplicationTools.layer
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
Layer.merge(
|
Layer.merge(
|
||||||
Layer.mergeAll(applicationTools, Database.defaultLayer, EventV2.defaultLayer),
|
Layer.mergeAll(applicationTools, Database.defaultLayer, EventV2.defaultLayer),
|
||||||
LocationServiceMap.layer.pipe(
|
locationServiceMapLayer.pipe(
|
||||||
Layer.provide(applicationTools),
|
Layer.provide(applicationTools),
|
||||||
Layer.provide(
|
Layer.provide(
|
||||||
Layer.mergeAll(
|
Layer.mergeAll(
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||||
import { ProjectDirectories } from "@opencode-ai/core/project/directories"
|
import { ProjectDirectories } from "@opencode-ai/core/project/directories"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||||
|
|
@ -29,7 +29,7 @@ const project = Project.layer.pipe(
|
||||||
Layer.provide(ProjectDirectories.defaultLayer),
|
Layer.provide(ProjectDirectories.defaultLayer),
|
||||||
)
|
)
|
||||||
const sessions = SessionV2.layer.pipe(
|
const sessions = SessionV2.layer.pipe(
|
||||||
Layer.provide(LocationServiceMap.layer),
|
Layer.provide(locationServiceMapLayer),
|
||||||
Layer.provide(Database.defaultLayer),
|
Layer.provide(Database.defaultLayer),
|
||||||
Layer.provide(EventV2.defaultLayer),
|
Layer.provide(EventV2.defaultLayer),
|
||||||
Layer.provide(project),
|
Layer.provide(project),
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { Project } from "@opencode-ai/core/project"
|
||||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||||
|
|
@ -24,7 +24,7 @@ const current = Layer.succeed(
|
||||||
Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
|
Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
|
||||||
)
|
)
|
||||||
const sessions = SessionV2.layer.pipe(
|
const sessions = SessionV2.layer.pipe(
|
||||||
Layer.provide(LocationServiceMap.layer),
|
Layer.provide(locationServiceMapLayer),
|
||||||
Layer.provide(EventV2.defaultLayer),
|
Layer.provide(EventV2.defaultLayer),
|
||||||
Layer.provide(Database.defaultLayer),
|
Layer.provide(Database.defaultLayer),
|
||||||
Layer.provide(SessionStore.defaultLayer),
|
Layer.provide(SessionStore.defaultLayer),
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
|
|
@ -35,7 +35,7 @@ const projects = Layer.succeed(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const sessions = SessionV2.layer.pipe(
|
const sessions = SessionV2.layer.pipe(
|
||||||
Layer.provide(LocationServiceMap.layer),
|
Layer.provide(locationServiceMapLayer),
|
||||||
Layer.provide(EventV2.defaultLayer),
|
Layer.provide(EventV2.defaultLayer),
|
||||||
Layer.provide(Database.defaultLayer),
|
Layer.provide(Database.defaultLayer),
|
||||||
Layer.provide(SessionStore.defaultLayer),
|
Layer.provide(SessionStore.defaultLayer),
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||||
|
|
@ -164,7 +164,7 @@ describe("SessionProjector", () => {
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.provide(
|
Effect.provide(
|
||||||
SessionV2.layer.pipe(
|
SessionV2.layer.pipe(
|
||||||
Layer.provide(LocationServiceMap.layer),
|
Layer.provide(locationServiceMapLayer),
|
||||||
Layer.provide(EventV2.defaultLayer),
|
Layer.provide(EventV2.defaultLayer),
|
||||||
Layer.provide(Database.defaultLayer),
|
Layer.provide(Database.defaultLayer),
|
||||||
Layer.provide(Project.defaultLayer),
|
Layer.provide(Project.defaultLayer),
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { Project } from "@opencode-ai/core/project"
|
||||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
|
|
@ -40,7 +40,7 @@ const execution = Layer.succeed(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const sessions = SessionV2.layer.pipe(
|
const sessions = SessionV2.layer.pipe(
|
||||||
Layer.provide(LocationServiceMap.layer),
|
Layer.provide(locationServiceMapLayer),
|
||||||
Layer.provide(EventV2.defaultLayer),
|
Layer.provide(EventV2.defaultLayer),
|
||||||
Layer.provide(Database.defaultLayer),
|
Layer.provide(Database.defaultLayer),
|
||||||
Layer.provide(SessionStore.defaultLayer),
|
Layer.provide(SessionStore.defaultLayer),
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { Project } from "@opencode-ai/core/project"
|
||||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { Snapshot } from "@opencode-ai/core/snapshot"
|
import { Snapshot } from "@opencode-ai/core/snapshot"
|
||||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
|
|
@ -102,7 +102,7 @@ const execution = Layer.effect(
|
||||||
}),
|
}),
|
||||||
).pipe(Layer.provide(runner))
|
).pipe(Layer.provide(runner))
|
||||||
const sessions = SessionV2.layer.pipe(
|
const sessions = SessionV2.layer.pipe(
|
||||||
Layer.provide(LocationServiceMap.layer),
|
Layer.provide(locationServiceMapLayer),
|
||||||
Layer.provide(EventV2.defaultLayer),
|
Layer.provide(EventV2.defaultLayer),
|
||||||
Layer.provide(Database.defaultLayer),
|
Layer.provide(Database.defaultLayer),
|
||||||
Layer.provide(SessionStore.defaultLayer),
|
Layer.provide(SessionStore.defaultLayer),
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||||
import { QuestionV2 } from "@opencode-ai/core/question"
|
import { QuestionV2 } from "@opencode-ai/core/question"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { Snapshot } from "@opencode-ai/core/snapshot"
|
import { Snapshot } from "@opencode-ai/core/snapshot"
|
||||||
import { ContextSnapshotDecodeError } from "@opencode-ai/core/session/error"
|
import { ContextSnapshotDecodeError } from "@opencode-ai/core/session/error"
|
||||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||||
|
|
@ -261,7 +261,7 @@ const execution = Layer.effect(
|
||||||
}),
|
}),
|
||||||
).pipe(Layer.provide(runner))
|
).pipe(Layer.provide(runner))
|
||||||
const sessions = SessionV2.layer.pipe(
|
const sessions = SessionV2.layer.pipe(
|
||||||
Layer.provide(LocationServiceMap.layer),
|
Layer.provide(locationServiceMapLayer),
|
||||||
Layer.provide(EventV2.defaultLayer),
|
Layer.provide(EventV2.defaultLayer),
|
||||||
Layer.provide(Database.defaultLayer),
|
Layer.provide(Database.defaultLayer),
|
||||||
Layer.provide(SessionStore.defaultLayer),
|
Layer.provide(SessionStore.defaultLayer),
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
||||||
import { AbsolutePath, type DeepMutable } from "@opencode-ai/core/schema"
|
import { AbsolutePath, type DeepMutable } from "@opencode-ai/core/schema"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { Reference } from "@opencode-ai/core/reference"
|
import { Reference } from "@opencode-ai/core/reference"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
|
|
@ -444,10 +444,10 @@ export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(Auth.defaultLayer),
|
Layer.provide(Auth.defaultLayer),
|
||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
Layer.provide(Skill.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, layer: locationServiceMapLayer, deps: [] })
|
||||||
|
|
||||||
export const node = LayerNode.make({
|
export const node = LayerNode.make({
|
||||||
service: Service,
|
service: Service,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
|
||||||
import { effectCmd } from "../../effect-cmd"
|
import { effectCmd } from "../../effect-cmd"
|
||||||
|
|
@ -10,7 +10,7 @@ import { cmd } from "../cmd"
|
||||||
const filesystem = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
const filesystem = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||||
effect.pipe(
|
effect.pipe(
|
||||||
Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(process.cwd()) }))),
|
Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(process.cwd()) }))),
|
||||||
Effect.provide(LocationServiceMap.layer),
|
Effect.provide(locationServiceMapLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
const FileSearchCommand = effectCmd({
|
const FileSearchCommand = effectCmd({
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { effectCmd } from "../../effect-cmd"
|
import { effectCmd } from "../../effect-cmd"
|
||||||
|
|
@ -37,6 +37,6 @@ export const V2Command = effectCmd({
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Effect.provide(LocationServiceMap.layer),
|
Effect.provide(locationServiceMapLayer),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
13
packages/opencode/src/effect/build-node.ts
Normal file
13
packages/opencode/src/effect/build-node.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Layer } from "effect"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node-tree"
|
||||||
|
|
||||||
|
export function buildNode<A, E>(
|
||||||
|
root: LayerNode.Node<A, E, any>,
|
||||||
|
replacements?: readonly LayerNode.Replacement[],
|
||||||
|
): Layer.Layer<A, E> {
|
||||||
|
return LayerNodeTree.compile(
|
||||||
|
root,
|
||||||
|
new Map(replacements?.map((item) => [item.source, item.replacement])),
|
||||||
|
) as Layer.Layer<A, E>
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import * as InstanceState from "@/effect/instance-state"
|
import * as InstanceState from "@/effect/instance-state"
|
||||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
|
|
@ -136,4 +136,4 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
|
||||||
.handle("content", content)
|
.handle("content", content)
|
||||||
.handle("status", status)
|
.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 { PtyProtocol } from "@opencode-ai/core/pty/protocol"
|
||||||
import { PtyID } from "@opencode-ai/core/pty/schema"
|
import { PtyID } from "@opencode-ai/core/pty/schema"
|
||||||
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { Shell } from "@opencode-ai/core/shell"
|
import { Shell } from "@opencode-ai/core/shell"
|
||||||
|
|
@ -158,7 +158,7 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler
|
||||||
.handle("remove", remove)
|
.handle("remove", remove)
|
||||||
.handle("connectToken", connectToken)
|
.handle("connectToken", connectToken)
|
||||||
}),
|
}),
|
||||||
).pipe(Layer.provide(LocationServiceMap.layer))
|
).pipe(Layer.provide(locationServiceMapLayer))
|
||||||
|
|
||||||
export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-connect", (handlers) =>
|
export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-connect", (handlers) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -270,4 +270,4 @@ export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-conne
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
).pipe(Layer.provide(LocationServiceMap.layer))
|
).pipe(Layer.provide(locationServiceMapLayer))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Config as EffectConfig, Context, Effect, Layer } from "effect"
|
import { Config as EffectConfig, Context, Effect, Layer } from "effect"
|
||||||
|
import { buildNode } from "@/effect/build-node"
|
||||||
import { HttpApiBuilder, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiBuilder, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { HttpClient, HttpMiddleware, HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http"
|
import { HttpClient, HttpMiddleware, HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http"
|
||||||
import * as Socket from "effect/unstable/socket/Socket"
|
import * as Socket from "effect/unstable/socket/Socket"
|
||||||
|
|
@ -52,6 +53,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
import { MoveSession } from "@opencode-ai/core/control-plane/move-session"
|
import { MoveSession } from "@opencode-ai/core/control-plane/move-session"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node-tree"
|
||||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||||
|
|
@ -61,6 +63,7 @@ import { ProjectCopy } from "@opencode-ai/core/project/copy"
|
||||||
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
||||||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@opencode-ai/server/cors"
|
import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@opencode-ai/server/cors"
|
||||||
import { serveUIEffect } from "@/server/shared/ui"
|
import { serveUIEffect } from "@/server/shared/ui"
|
||||||
|
|
@ -95,6 +98,10 @@ import { sessionHandlers } from "./handlers/session"
|
||||||
import { syncHandlers } from "./handlers/sync"
|
import { syncHandlers } from "./handlers/sync"
|
||||||
import { tuiHandlers } from "./handlers/tui"
|
import { tuiHandlers } from "./handlers/tui"
|
||||||
import { handlers } from "@opencode-ai/server/handlers"
|
import { handlers } from "@opencode-ai/server/handlers"
|
||||||
|
import { locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
|
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 { schemaErrorLayer as v2SchemaErrorLayer } from "@opencode-ai/server/middleware/schema-error"
|
||||||
import { workspaceHandlers } from "./handlers/workspace"
|
import { workspaceHandlers } from "./handlers/workspace"
|
||||||
import { instanceContextLayer } from "./middleware/instance-context"
|
import { instanceContextLayer } from "./middleware/instance-context"
|
||||||
|
|
@ -279,7 +286,12 @@ export function createRoutes(
|
||||||
MoveSession.defaultLayer,
|
MoveSession.defaultLayer,
|
||||||
HttpServer.layerServices,
|
HttpServer.layerServices,
|
||||||
]),
|
]),
|
||||||
Layer.provide(LayerNode.buildLayer(app)),
|
Layer.provide(sessionLocationLayer),
|
||||||
|
Layer.provide(locationLayer),
|
||||||
|
Layer.provide(PtyEnvironment.defaultLayer),
|
||||||
|
Layer.provide(SessionV2.defaultLayer.pipe(Layer.provide(locationServiceMapLayer))),
|
||||||
|
Layer.provide(buildNode(app)),
|
||||||
|
Layer.provide(locationServiceMapLayer),
|
||||||
Layer.provide(Layer.succeed(CorsConfig)(corsOptions)),
|
Layer.provide(Layer.succeed(CorsConfig)(corsOptions)),
|
||||||
Layer.provideMerge(Observability.layer),
|
Layer.provideMerge(Observability.layer),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,6 @@ import type { SystemError } from "bun"
|
||||||
import type { Provider } from "@/provider/provider"
|
import type { Provider } from "@/provider/provider"
|
||||||
import { Effect, Schema } from "effect"
|
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 */
|
/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
|
||||||
interface FetchDecompressionError extends Error {
|
interface FetchDecompressionError extends Error {
|
||||||
code: "ZlibError"
|
code: "ZlibError"
|
||||||
|
|
@ -733,3 +731,4 @@ export function fromError(
|
||||||
}
|
}
|
||||||
|
|
||||||
export * as MessageV2 from "./message-v2"
|
export * as MessageV2 from "./message-v2"
|
||||||
|
export const node = LayerNode.group([Database.node])
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
|
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
|
||||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
|
import { locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||||
|
|
||||||
import { NotFoundError } from "@/storage/storage"
|
import { NotFoundError } from "@/storage/storage"
|
||||||
|
|
@ -924,7 +925,7 @@ export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(Database.defaultLayer),
|
Layer.provide(Database.defaultLayer),
|
||||||
Layer.provide(EventV2Bridge.defaultLayer),
|
Layer.provide(EventV2Bridge.defaultLayer),
|
||||||
Layer.provide(SessionExecution.noopLayer),
|
Layer.provide(SessionExecution.noopLayer),
|
||||||
Layer.provide(SessionV2.defaultLayer),
|
Layer.provide(SessionV2.defaultLayer.pipe(Layer.provide(locationServiceMapLayer))),
|
||||||
Layer.provide(RuntimeFlags.defaultLayer),
|
Layer.provide(RuntimeFlags.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import { Permission } from "@/permission"
|
||||||
import { Skill } from "@/skill"
|
import { Skill } from "@/skill"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { Reference } from "@opencode-ai/core/reference"
|
import { Reference } from "@opencode-ai/core/reference"
|
||||||
import { MCP } from "@/mcp"
|
import { MCP } from "@/mcp"
|
||||||
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
||||||
|
|
@ -131,10 +131,10 @@ export const layer = Layer.effect(
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(Skill.defaultLayer),
|
Layer.provide(Skill.defaultLayer),
|
||||||
Layer.provide(MCP.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, layer: locationServiceMapLayer, deps: [] })
|
||||||
|
|
||||||
export const node = LayerNode.make({
|
export const node = LayerNode.make({
|
||||||
service: Service,
|
service: Service,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { Plugin } from "../../src/plugin"
|
||||||
import { Provider } from "../../src/provider/provider"
|
import { Provider } from "../../src/provider/provider"
|
||||||
import { Skill } from "../../src/skill"
|
import { Skill } from "../../src/skill"
|
||||||
import { Truncate } from "../../src/tool/truncate"
|
import { Truncate } from "../../src/tool/truncate"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
|
|
||||||
const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||||
Agent.layer.pipe(
|
Agent.layer.pipe(
|
||||||
|
|
@ -23,7 +23,7 @@ const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||||
Layer.provide(Auth.defaultLayer),
|
Layer.provide(Auth.defaultLayer),
|
||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
Layer.provide(Skill.defaultLayer),
|
Layer.provide(Skill.defaultLayer),
|
||||||
Layer.provide(LocationServiceMap.layer),
|
Layer.provide(locationServiceMapLayer),
|
||||||
Layer.provide(RuntimeFlags.layer(flags)),
|
Layer.provide(RuntimeFlags.layer(flags)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { expect } from "bun:test"
|
import { expect } from "bun:test"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { FetchHttpClient } from "effect/unstable/http"
|
import { FetchHttpClient } from "effect/unstable/http"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
@ -44,7 +44,7 @@ const agentLayer = Agent.layer.pipe(
|
||||||
Layer.provide(SkillTest.empty),
|
Layer.provide(SkillTest.empty),
|
||||||
Layer.provide(provider.layer),
|
Layer.provide(provider.layer),
|
||||||
Layer.provide(pluginLayer),
|
Layer.provide(pluginLayer),
|
||||||
Layer.provide(LocationServiceMap.layer),
|
Layer.provide(locationServiceMapLayer),
|
||||||
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
|
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,55 @@
|
||||||
import { test } from "bun:test"
|
import { test } from "bun:test"
|
||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node-tree"
|
||||||
|
|
||||||
class A extends Context.Service<A, {}>()("test/TierA") {}
|
class A extends Context.Service<A, {}>()("test/TierA") {}
|
||||||
class B extends Context.Service<B, {}>()("test/TierB") {}
|
class B extends Context.Service<B, {}>()("test/TierB") {}
|
||||||
|
class C extends Context.Service<C, {}>()("test/TierC") {}
|
||||||
|
|
||||||
const tiers = LayerNode.tiers(["request", "global"])
|
const tiers = LayerNode.tiers(["request", "global"])
|
||||||
const request = tiers.make("request")
|
const request = tiers.make("request")
|
||||||
const global = tiers.make("global")
|
const global = tiers.make("global")
|
||||||
const globalA = global({ service: A, layer: Layer.succeed(A, A.of({})), deps: [] })
|
const globalA = global({ service: A, layer: Layer.succeed(A, A.of({})), deps: [] })
|
||||||
|
const requestA = request({ service: A, layer: Layer.succeed(A, A.of({})), deps: [] })
|
||||||
|
const requestB = request({ service: B, layer: Layer.succeed(B, B.of({})), deps: [] })
|
||||||
const bLayer = Layer.effect(B, Effect.as(A, B.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 requestInput = LayerNode.unbound(A, tiers.values.request)
|
||||||
|
const globalInput = LayerNode.unbound(B, tiers.values.global)
|
||||||
|
const inputs = LayerNode.group([requestInput, globalInput])
|
||||||
|
const separatedInputs = LayerNodeTree.separate(inputs, tiers)
|
||||||
|
void separatedInputs
|
||||||
|
|
||||||
request({ service: B, layer: bLayer, deps: [globalA] })
|
request({ service: B, layer: bLayer, deps: [globalA] })
|
||||||
|
request({ service: C, layer: cLayer, deps: [globalA, requestB] })
|
||||||
|
request({ service: C, layer: cLayer, deps: [LayerNode.group([globalA, requestB])] })
|
||||||
|
request({ service: B, layer: bLayer, deps: [LayerNode.unbound(A, tiers.values.request)] })
|
||||||
|
|
||||||
|
// @ts-expect-error An unrelated dependency cannot satisfy A
|
||||||
|
request({ service: B, layer: bLayer, deps: [requestB] })
|
||||||
|
|
||||||
|
// @ts-expect-error Providing only A leaves B missing
|
||||||
|
request({ service: C, layer: cLayer, deps: [globalA] })
|
||||||
|
|
||||||
|
// @ts-expect-error Providing only B leaves A missing
|
||||||
|
request({ service: C, layer: cLayer, deps: [requestB] })
|
||||||
|
|
||||||
|
// @ts-expect-error Duplicate A providers still leave B missing
|
||||||
|
request({ service: C, layer: cLayer, deps: [globalA, requestA] })
|
||||||
|
|
||||||
|
// @ts-expect-error A group with only A still leaves B missing
|
||||||
|
request({ service: C, layer: cLayer, deps: [LayerNode.group([globalA])] })
|
||||||
|
|
||||||
// @ts-expect-error Global cannot depend on request
|
// @ts-expect-error Global cannot depend on request
|
||||||
global({ service: B, layer: bLayer, deps: [request({ service: A, layer: Layer.succeed(A, A.of({})), deps: [] })] })
|
global({ service: B, layer: bLayer, deps: [requestA] })
|
||||||
|
|
||||||
test("type exploration compiles", () => {})
|
test("type exploration compiles", () => {})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import { expect, test } from "bun:test"
|
import { expect, test } from "bun:test"
|
||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node-tree"
|
||||||
|
import { ScopedNode } from "@opencode-ai/core/effect/scoped-node"
|
||||||
|
import { ScopedNodeBuild } from "@opencode-ai/core/effect/scoped-node-build"
|
||||||
|
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||||
|
import { Location } from "@opencode-ai/core/location"
|
||||||
|
|
||||||
class Value extends Context.Service<Value, { readonly value: string }>()("test/TierValue") {}
|
class Value extends Context.Service<Value, { readonly value: string }>()("test/TierValue") {}
|
||||||
class Result extends Context.Service<Result, { readonly value: string }>()("test/TierResult") {}
|
class Result extends Context.Service<Result, { readonly value: string }>()("test/TierResult") {}
|
||||||
|
|
@ -8,8 +13,7 @@ class Left extends Context.Service<Left, { readonly value: string }>()("test/Tie
|
||||||
class Right extends Context.Service<Right, { readonly value: string }>()("test/TierRight") {}
|
class Right extends Context.Service<Right, { readonly value: string }>()("test/TierRight") {}
|
||||||
class Last extends Context.Service<Last, { readonly value: string }>()("test/TierLast") {}
|
class Last extends Context.Service<Last, { readonly value: string }>()("test/TierLast") {}
|
||||||
|
|
||||||
test("builds tiers with a custom builder", async () => {
|
test("returns independently composable tier layers", async () => {
|
||||||
let locationBuilds = 0
|
|
||||||
const tiers = LayerNode.tiers(["location", "global"])
|
const tiers = LayerNode.tiers(["location", "global"])
|
||||||
const global = tiers.make("global")
|
const global = tiers.make("global")
|
||||||
const location = tiers.make("location")
|
const location = tiers.make("location")
|
||||||
|
|
@ -24,20 +28,15 @@ test("builds tiers with a custom builder", async () => {
|
||||||
),
|
),
|
||||||
deps: [value],
|
deps: [value],
|
||||||
})
|
})
|
||||||
const layer = LayerNode.buildLayer(LayerNode.group([result]), {
|
const serviceLayer = ScopedNodeBuild.build(LayerNode.group([result]))
|
||||||
tiers,
|
const layer = LocationServiceMap.get({ directory: "/tmp" } as Location.Ref).pipe(
|
||||||
buildTier: (tier, layers) => {
|
Layer.provide(serviceLayer),
|
||||||
if (tier !== "location") return LayerNode.combine(layers)
|
) as unknown as Layer.Layer<Result>
|
||||||
locationBuilds++
|
|
||||||
return LayerNode.combine(layers).pipe(Layer.fresh)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const program = Effect.gen(function* () {
|
const program = Effect.gen(function* () {
|
||||||
return (yield* Result).value
|
return (yield* Result).value
|
||||||
}).pipe(Effect.provide(layer))
|
}).pipe(Effect.provide(layer))
|
||||||
|
|
||||||
expect(await Effect.runPromise(program)).toBe("value")
|
expect(await Effect.runPromise(program)).toBe("value")
|
||||||
expect(locationBuilds).toBe(1)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("rejects conflicting higher-tier service implementations", () => {
|
test("rejects conflicting higher-tier service implementations", () => {
|
||||||
|
|
@ -57,7 +56,7 @@ test("rejects conflicting higher-tier service implementations", () => {
|
||||||
deps: [second],
|
deps: [second],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(() => LayerNode.buildLayer(LayerNode.group([left, right]), { tiers })).toThrow(
|
expect(() => ScopedNodeBuild.build(LayerNode.group([left, right]))).toThrow(
|
||||||
"conflicting implementations for test/TierValue",
|
"conflicting implementations for test/TierValue",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -76,7 +75,7 @@ test("validates tier dependencies through groups", () => {
|
||||||
deps: [LayerNode.group([local])],
|
deps: [LayerNode.group([local])],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(() => LayerNode.buildLayer(invalid, { tiers })).toThrow("Tier global cannot depend on lower tier location")
|
expect(() => ScopedNodeBuild.build(LayerNode.group([invalid]))).toThrow("Tier global cannot depend on lower tier location")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("validates shared groups in each consumer tier", () => {
|
test("validates shared groups in each consumer tier", () => {
|
||||||
|
|
@ -102,7 +101,7 @@ test("validates shared groups in each consumer tier", () => {
|
||||||
deps: [shared],
|
deps: [shared],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(() => LayerNode.buildLayer(LayerNode.group([valid, invalid]), { tiers })).toThrow(
|
expect(() => ScopedNodeBuild.build(LayerNode.group([valid, invalid]))).toThrow(
|
||||||
"Tier global cannot depend on lower tier location",
|
"Tier global cannot depend on lower tier location",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -114,7 +113,7 @@ test("rejects a service assigned to multiple tiers", () => {
|
||||||
const local = location({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "local" })), deps: [] })
|
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: [] })
|
const shared = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "global" })), deps: [] })
|
||||||
|
|
||||||
expect(() => LayerNode.buildLayer(LayerNode.group([local, shared]), { tiers })).toThrow(
|
expect(() => ScopedNodeBuild.build(LayerNode.group([local, shared]))).toThrow(
|
||||||
"Service test/TierValue belongs to both tier location and tier global",
|
"Service test/TierValue belongs to both tier location and tier global",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -159,7 +158,7 @@ test("rebinds same-tier providers without reacquiring them", async () => {
|
||||||
),
|
),
|
||||||
deps: [first],
|
deps: [first],
|
||||||
})
|
})
|
||||||
const layer = LayerNode.buildLayer(LayerNode.group([left, right, last]), { tiers })
|
const layer = ScopedNodeBuild.build(LayerNode.group([left, right, last])) as Layer.Layer<Left | Right | Last>
|
||||||
const values = Effect.gen(function* () {
|
const values = Effect.gen(function* () {
|
||||||
return [(yield* Left).value, (yield* Right).value, (yield* Last).value]
|
return [(yield* Left).value, (yield* Right).value, (yield* Last).value]
|
||||||
}).pipe(Effect.provide(layer))
|
}).pipe(Effect.provide(layer))
|
||||||
|
|
|
||||||
130
packages/opencode/test/effect/layer-node-tree.test.ts
Normal file
130
packages/opencode/test/effect/layer-node-tree.test.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { expect, test } from "bun:test"
|
||||||
|
import { Context, Effect, Layer } from "effect"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node-tree"
|
||||||
|
import { ScopedNode } from "@opencode-ai/core/effect/scoped-node"
|
||||||
|
import { ScopedNodeBuild } from "@opencode-ai/core/effect/scoped-node-build"
|
||||||
|
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||||
|
import { Location } from "@opencode-ai/core/location"
|
||||||
|
|
||||||
|
class Database extends Context.Service<Database, { readonly name: string }>()("test/GraphDatabase") {}
|
||||||
|
class Users extends Context.Service<Users, { readonly list: Effect.Effect<string[]> }>()("test/GraphUsers") {}
|
||||||
|
class App extends Context.Service<App, { readonly run: Effect.Effect<string[]> }>()("test/GraphApp") {}
|
||||||
|
|
||||||
|
test("separates, hoists, and compiles tier graphs", async () => {
|
||||||
|
const tiers = LayerNode.tiers(["location", "global"])
|
||||||
|
const global = tiers.make("global")
|
||||||
|
const location = tiers.make("location")
|
||||||
|
const database = global({
|
||||||
|
service: Database,
|
||||||
|
layer: Layer.succeed(Database, Database.of({ name: "Alice" })),
|
||||||
|
deps: [],
|
||||||
|
})
|
||||||
|
const users = location({
|
||||||
|
service: Users,
|
||||||
|
layer: Layer.effect(
|
||||||
|
Users,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const db = yield* Database
|
||||||
|
return Users.of({ list: Effect.succeed([db.name]) })
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
deps: [database],
|
||||||
|
})
|
||||||
|
const app = location({
|
||||||
|
service: App,
|
||||||
|
layer: Layer.effect(
|
||||||
|
App,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const service = yield* Users
|
||||||
|
return App.of({ run: service.list })
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
deps: [users],
|
||||||
|
})
|
||||||
|
|
||||||
|
const separated = LayerNodeTree.separate(LayerNode.group([app]), tiers)
|
||||||
|
expect(separated.location.dependencies).toEqual([app])
|
||||||
|
expect(separated.global.dependencies).toEqual([])
|
||||||
|
|
||||||
|
const locationResult = LayerNodeTree.hoist(separated.location, tiers.values.location, tiers)
|
||||||
|
const globalResult = LayerNodeTree.hoist(
|
||||||
|
{ ...separated.global, dependencies: [...separated.global.dependencies, ...locationResult.hoisted.dependencies] },
|
||||||
|
tiers.values.global,
|
||||||
|
tiers,
|
||||||
|
)
|
||||||
|
expect(locationResult.node.dependencies[0]?.dependencies[0]?.dependencies[0]).toMatchObject({
|
||||||
|
kind: "group",
|
||||||
|
dependencies: [],
|
||||||
|
})
|
||||||
|
expect(locationResult.hoisted.dependencies).toEqual([database])
|
||||||
|
|
||||||
|
const layer = LayerNodeTree.compile(locationResult.node).pipe(
|
||||||
|
Layer.provide(LayerNodeTree.compile(globalResult.node)),
|
||||||
|
) as unknown as Layer.Layer<App>
|
||||||
|
const program = Effect.gen(function* () {
|
||||||
|
return yield* (yield* App).run
|
||||||
|
}).pipe(Effect.provide(layer))
|
||||||
|
|
||||||
|
expect(await Effect.runPromise(program)).toEqual(["Alice"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects conflicting hoisted implementations", () => {
|
||||||
|
const tiers = LayerNode.tiers(["location", "global"])
|
||||||
|
const global = tiers.make("global")
|
||||||
|
const location = tiers.make("location")
|
||||||
|
const first = global({ service: Database, layer: Layer.succeed(Database, Database.of({ name: "first" })), deps: [] })
|
||||||
|
const second = global({ service: Database, layer: Layer.succeed(Database, Database.of({ name: "second" })), deps: [] })
|
||||||
|
const left = location({ service: Users, layer: Layer.effect(Users, Effect.as(Database, Users.of({ list: Effect.succeed([]) }))), deps: [first] })
|
||||||
|
const right = location({ service: App, layer: Layer.effect(App, Effect.as(Database, App.of({ run: Effect.succeed([]) }))), deps: [second] })
|
||||||
|
|
||||||
|
const separated = LayerNodeTree.separate(LayerNode.group([left, right]), tiers)
|
||||||
|
expect(() => LayerNodeTree.hoist(separated.location, tiers.values.location, tiers)).toThrow(
|
||||||
|
"Tier location has conflicting implementations for test/GraphDatabase",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("treats dependency groups as transparent while hoisting", () => {
|
||||||
|
const tiers = LayerNode.tiers(["location", "global"])
|
||||||
|
const global = tiers.make("global")
|
||||||
|
const location = tiers.make("location")
|
||||||
|
const database = global({
|
||||||
|
service: Database,
|
||||||
|
layer: Layer.succeed(Database, Database.of({ name: "Alice" })),
|
||||||
|
deps: [],
|
||||||
|
})
|
||||||
|
const users = location({
|
||||||
|
service: Users,
|
||||||
|
layer: Layer.effect(Users, Effect.as(Database, Users.of({ list: Effect.succeed([]) }))),
|
||||||
|
deps: [LayerNode.group([database])],
|
||||||
|
})
|
||||||
|
const separated = LayerNodeTree.separate(LayerNode.group([users]), tiers)
|
||||||
|
const result = LayerNodeTree.hoist(separated.location, tiers.values.location, tiers)
|
||||||
|
|
||||||
|
expect(result.node.dependencies[0]?.dependencies[0]?.dependencies[0]).toMatchObject({
|
||||||
|
kind: "group",
|
||||||
|
dependencies: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("builds the scoped location and global trees", async () => {
|
||||||
|
const database = ScopedNode.makeGlobalNode({
|
||||||
|
service: Database,
|
||||||
|
layer: Layer.succeed(Database, Database.of({ name: "Alice" })),
|
||||||
|
deps: [],
|
||||||
|
})
|
||||||
|
const app = ScopedNode.makeLocationNode({
|
||||||
|
service: App,
|
||||||
|
layer: Layer.effect(App, Effect.map(Database, (db) => App.of({ run: Effect.succeed([db.name]) }))),
|
||||||
|
deps: [database],
|
||||||
|
})
|
||||||
|
const serviceLayer = ScopedNodeBuild.build(LayerNode.group([app]))
|
||||||
|
const layer = LocationServiceMap.get({ directory: "/tmp" } as Location.Ref).pipe(
|
||||||
|
Layer.provide(serviceLayer),
|
||||||
|
) as unknown as Layer.Layer<App>
|
||||||
|
const program = Effect.gen(function* () {
|
||||||
|
return yield* (yield* App).run
|
||||||
|
}).pipe(Effect.provide(layer))
|
||||||
|
|
||||||
|
expect(await Effect.runPromise(program)).toEqual(["Alice"])
|
||||||
|
})
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { test } from "bun:test"
|
import { test } from "bun:test"
|
||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node-tree"
|
||||||
|
|
||||||
class A extends Context.Service<A, {}>()("test/LayerNodeA") {}
|
class A extends Context.Service<A, {}>()("test/LayerNodeA") {}
|
||||||
class B extends Context.Service<B, {}>()("test/LayerNodeB") {}
|
class B extends Context.Service<B, {}>()("test/LayerNodeB") {}
|
||||||
|
|
@ -14,6 +15,8 @@ class OtherError {
|
||||||
|
|
||||||
const tiers = LayerNode.tiers(["app"])
|
const tiers = LayerNode.tiers(["app"])
|
||||||
const make = tiers.make("app")
|
const make = tiers.make("app")
|
||||||
|
const build = <A, E>(root: LayerNode.Node<A, E, any>) =>
|
||||||
|
LayerNodeTree.compile(LayerNodeTree.separate(root, tiers).app) as Layer.Layer<A, E>
|
||||||
const aLayer = Layer.succeed(A, A.of({}))
|
const aLayer = Layer.succeed(A, A.of({}))
|
||||||
const bLayer = Layer.effect(B, Effect.as(A, B.of({})))
|
const bLayer = Layer.effect(B, Effect.as(A, B.of({})))
|
||||||
const cLayer = Layer.effect(
|
const cLayer = Layer.effect(
|
||||||
|
|
@ -30,6 +33,8 @@ const b = make({ service: B, layer: bLayer, deps: [a] })
|
||||||
const c = make({ service: C, layer: cLayer, deps: [a, b] })
|
const c = make({ service: C, layer: cLayer, deps: [a, b] })
|
||||||
const failing = make({ service: A, layer: failingA, deps: [] })
|
const failing = make({ service: A, layer: failingA, deps: [] })
|
||||||
const dependent = make({ service: B, layer: bLayer, deps: [failing] })
|
const dependent = make({ service: B, layer: bLayer, deps: [failing] })
|
||||||
|
const inputA = LayerNode.unbound(A, tiers.values.app)
|
||||||
|
const inputDependent = make({ service: B, layer: bLayer, deps: [inputA] })
|
||||||
|
|
||||||
make({ name: "manual-a", layer: aLayer, deps: [] })
|
make({ name: "manual-a", layer: aLayer, deps: [] })
|
||||||
|
|
||||||
|
|
@ -45,8 +50,8 @@ make({ service: B, layer: bLayer, deps: [] })
|
||||||
// @ts-expect-error C requires A and B
|
// @ts-expect-error C requires A and B
|
||||||
make({ service: C, layer: cLayer, deps: [a] })
|
make({ service: C, layer: cLayer, deps: [a] })
|
||||||
|
|
||||||
const closed = LayerNode.buildLayer(c, { tiers })
|
const closed = build(LayerNode.group([c]))
|
||||||
const closedWithError = LayerNode.buildLayer(dependent, { tiers })
|
const closedWithError = build(LayerNode.group([dependent]))
|
||||||
const checkClosed: Layer.Layer<C, never, never> = closed
|
const checkClosed: Layer.Layer<C, never, never> = closed
|
||||||
const checkError: Layer.Layer<B, LayerError, never> = closedWithError
|
const checkError: Layer.Layer<B, LayerError, never> = closedWithError
|
||||||
void checkClosed
|
void checkClosed
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { buildNode } from "@/effect/build-node"
|
||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node-tree"
|
||||||
|
|
||||||
class Value extends Context.Service<Value, { readonly value: string }>()("test/LayerNodeValue") {}
|
class Value extends Context.Service<Value, { readonly value: string }>()("test/LayerNodeValue") {}
|
||||||
class Greeting extends Context.Service<Greeting, { readonly value: string }>()("test/LayerNodeGreeting") {}
|
class Greeting extends Context.Service<Greeting, { readonly value: string }>()("test/LayerNodeGreeting") {}
|
||||||
|
|
@ -9,6 +11,11 @@ class Right extends Context.Service<Right, { readonly value: string }>()("test/L
|
||||||
|
|
||||||
const tiers = LayerNode.tiers(["app"])
|
const tiers = LayerNode.tiers(["app"])
|
||||||
const make = tiers.make("app")
|
const make = tiers.make("app")
|
||||||
|
const build = <A, E>(root: LayerNode.Node<A, E, any>, replacements?: readonly LayerNode.Replacement[]) =>
|
||||||
|
LayerNodeTree.compile(
|
||||||
|
LayerNodeTree.separate(root, tiers).app,
|
||||||
|
new Map(replacements?.map((item) => [item.source, item.replacement])),
|
||||||
|
) as Layer.Layer<A, E>
|
||||||
const valueLayer = Layer.succeed(Value, Value.of({ value: "production" }))
|
const valueLayer = Layer.succeed(Value, Value.of({ value: "production" }))
|
||||||
const greetingLayer = Layer.effect(
|
const greetingLayer = Layer.effect(
|
||||||
Greeting,
|
Greeting,
|
||||||
|
|
@ -21,21 +28,54 @@ describe("layer node", () => {
|
||||||
test("builds an untiered graph", async () => {
|
test("builds an untiered graph", async () => {
|
||||||
const value = LayerNode.make({ service: Value, layer: valueLayer, deps: [] })
|
const value = LayerNode.make({ service: Value, layer: valueLayer, deps: [] })
|
||||||
const greeting = LayerNode.make({ service: Greeting, layer: greetingLayer, deps: [value] })
|
const greeting = LayerNode.make({ service: Greeting, layer: greetingLayer, deps: [value] })
|
||||||
const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(LayerNode.buildLayer(greeting)))
|
const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(buildNode(LayerNode.group([greeting]))))
|
||||||
expect(await Effect.runPromise(program)).toBe("hello production")
|
expect(await Effect.runPromise(program)).toBe("hello production")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("builds a dependency graph", async () => {
|
test("builds a dependency graph", async () => {
|
||||||
const program = Effect.map(Greeting, (item) => item.value).pipe(
|
const program = Effect.map(Greeting, (item) => item.value).pipe(
|
||||||
Effect.provide(LayerNode.buildLayer(greeting, { tiers })),
|
Effect.provide(build(LayerNode.group([greeting]))),
|
||||||
)
|
)
|
||||||
expect(await Effect.runPromise(program)).toBe("hello production")
|
expect(await Effect.runPromise(program)).toBe("hello production")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("exposes roots but hides transitive dependencies", () => {
|
||||||
|
const layer = build(LayerNode.group([greeting]))
|
||||||
|
const check: Layer.Layer<Greeting> = layer
|
||||||
|
void check
|
||||||
|
})
|
||||||
|
|
||||||
|
test("preserves branch-specific implementations across roots", async () => {
|
||||||
|
const firstValue = make({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "first" })), deps: [] })
|
||||||
|
const secondValue = make({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "second" })), deps: [] })
|
||||||
|
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: [firstValue] })
|
||||||
|
const right = make({ service: Right, layer: rightLayer, deps: [secondValue] })
|
||||||
|
const layer = build(LayerNode.group([left, right]))
|
||||||
|
const program = Effect.gen(function* () {
|
||||||
|
return [(yield* Left).value, (yield* Right).value]
|
||||||
|
}).pipe(Effect.provide(layer))
|
||||||
|
expect(await Effect.runPromise(program)).toEqual(["first", "second"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("requires unbound nodes to be bound before compilation", async () => {
|
||||||
|
const unbound = LayerNode.unbound(Value, tiers.values.app)
|
||||||
|
const greeting = make({ service: Greeting, layer: greetingLayer, deps: [unbound] })
|
||||||
|
const tree = LayerNodeTree.separate(LayerNode.group([greeting]), tiers).app
|
||||||
|
expect(() => LayerNodeTree.compile(tree)).toThrow("Unbound layer node: test/LayerNodeValue")
|
||||||
|
const bound = LayerNodeTree.bind(tree, unbound, value)
|
||||||
|
const layer = LayerNodeTree.compile(bound) as Layer.Layer<Greeting>
|
||||||
|
const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(layer))
|
||||||
|
expect(await Effect.runPromise(program)).toBe("hello production")
|
||||||
|
})
|
||||||
|
|
||||||
test("replaces a layer by identity", async () => {
|
test("replaces a layer by identity", async () => {
|
||||||
const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
|
const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
|
||||||
const program = Effect.map(Greeting, (item) => item.value).pipe(
|
const program = Effect.map(Greeting, (item) => item.value).pipe(
|
||||||
Effect.provide(LayerNode.buildLayer(greeting, { tiers, replacements: [LayerNode.replace(valueLayer, replacement)] })),
|
Effect.provide(
|
||||||
|
build(LayerNode.group([greeting]), [LayerNode.replace(valueLayer, replacement)]),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
expect(await Effect.runPromise(program)).toBe("hello simulation")
|
expect(await Effect.runPromise(program)).toBe("hello simulation")
|
||||||
})
|
})
|
||||||
|
|
@ -52,10 +92,9 @@ describe("layer node", () => {
|
||||||
const left = make({ service: Left, layer: leftLayer, deps: [value] })
|
const left = make({ service: Left, layer: leftLayer, deps: [value] })
|
||||||
const right = make({ service: Right, layer: rightLayer, deps: [value] })
|
const right = make({ service: Right, layer: rightLayer, deps: [value] })
|
||||||
const replacement = Layer.succeed(Value, Value.of({ value: "replaced" }))
|
const replacement = Layer.succeed(Value, Value.of({ value: "replaced" }))
|
||||||
const layer = LayerNode.buildLayer(LayerNode.group([left, right]), {
|
const layer = build(LayerNode.group([left, right]), [
|
||||||
tiers,
|
LayerNode.replace(valueLayer, replacement),
|
||||||
replacements: [LayerNode.replace(valueLayer, replacement)],
|
])
|
||||||
})
|
|
||||||
const program = Effect.gen(function* () {
|
const program = Effect.gen(function* () {
|
||||||
return [(yield* Left).value, (yield* Right).value]
|
return [(yield* Left).value, (yield* Right).value]
|
||||||
}).pipe(Effect.provide(layer))
|
}).pipe(Effect.provide(layer))
|
||||||
|
|
@ -74,7 +113,9 @@ describe("layer node", () => {
|
||||||
)
|
)
|
||||||
await Effect.runPromise(
|
await Effect.runPromise(
|
||||||
Effect.map(Greeting, (item) => item.value).pipe(
|
Effect.map(Greeting, (item) => item.value).pipe(
|
||||||
Effect.provide(LayerNode.buildLayer(greeting, { tiers, replacements: [LayerNode.replace(other, replacement)] })),
|
Effect.provide(
|
||||||
|
build(LayerNode.group([greeting]), [LayerNode.replace(other, replacement)]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
expect(acquisitions).toBe(0)
|
expect(acquisitions).toBe(0)
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
|
import { buildNode } from "@/effect/build-node"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node-tree"
|
||||||
import { Git } from "../../src/git"
|
import { Git } from "../../src/git"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { tmpdir } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
const weird = process.platform === "win32" ? "space file.txt" : "tab\tfile.txt"
|
const weird = process.platform === "win32" ? "space file.txt" : "tab\tfile.txt"
|
||||||
const it = testEffect(LayerNode.buildLayer(Git.node))
|
const it = testEffect(buildNode(LayerNode.group([Git.node])))
|
||||||
|
|
||||||
const scopedTmpdir = (options?: Parameters<typeof tmpdir>[0]) =>
|
const scopedTmpdir = (options?: Parameters<typeof tmpdir>[0]) =>
|
||||||
Effect.acquireRelease(
|
Effect.acquireRelease(
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||||
import { SessionStatus } from "../../src/session/status"
|
import { SessionStatus } from "../../src/session/status"
|
||||||
import { SessionSummary } from "../../src/session/summary"
|
import { SessionSummary } from "../../src/session/summary"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
|
import { locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||||
|
|
||||||
import type { Provider } from "@/provider/provider"
|
import type { Provider } from "@/provider/provider"
|
||||||
|
|
@ -615,6 +616,7 @@ describe("session.compaction.create", () => {
|
||||||
const v2 = yield* SessionV2.Service.use((svc) => svc.messages({ sessionID: info.id })).pipe(
|
const v2 = yield* SessionV2.Service.use((svc) => svc.messages({ sessionID: info.id })).pipe(
|
||||||
Effect.provide(SessionExecution.noopLayer),
|
Effect.provide(SessionExecution.noopLayer),
|
||||||
Effect.provide(SessionV2.defaultLayer),
|
Effect.provide(SessionV2.defaultLayer),
|
||||||
|
Effect.provide(locationServiceMapLayer),
|
||||||
)
|
)
|
||||||
expect(v2.at(-1)).toMatchObject({
|
expect(v2.at(-1)).toMatchObject({
|
||||||
type: "compaction",
|
type: "compaction",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { buildNode } from "@/effect/build-node"
|
||||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node-tree"
|
||||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||||
import { Effect, Option } from "effect"
|
import { Effect, Option } from "effect"
|
||||||
import { Session as SessionNs } from "@/session/session"
|
import { Session as SessionNs } from "@/session/session"
|
||||||
|
|
@ -12,7 +14,7 @@ import { testEffect } from "../lib/effect"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
|
||||||
const it = testEffect(LayerNode.buildLayer(LayerNode.group([SessionNs.node, MessageV2.node, SessionProjector.node])))
|
const it = testEffect(buildNode(LayerNode.group([SessionNs.node, MessageV2.node, SessionProjector.node])))
|
||||||
|
|
||||||
const withSession = <A, E, R>(
|
const withSession = <A, E, R>(
|
||||||
fn: (input: { session: SessionNs.Interface; sessionID: SessionID }) => Effect.Effect<A, E, R>,
|
fn: (input: { session: SessionNs.Interface; sessionID: SessionID }) => Effect.Effect<A, E, R>,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||||
|
import { buildNode } from "@/effect/build-node"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node-tree"
|
||||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||||
import { expect } from "bun:test"
|
import { expect } from "bun:test"
|
||||||
import { tool } from "ai"
|
import { tool } from "ai"
|
||||||
|
|
@ -180,9 +182,9 @@ const replacements = [
|
||||||
LayerNode.replace(SessionSummary.layer, summary),
|
LayerNode.replace(SessionSummary.layer, summary),
|
||||||
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer({ experimentalEventSystem: true })),
|
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer({ experimentalEventSystem: true })),
|
||||||
]
|
]
|
||||||
const env = LayerNode.buildLayer(
|
const env = buildNode(
|
||||||
LayerNode.group([root, LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })]),
|
LayerNode.group([root, LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })]),
|
||||||
{ replacements },
|
replacements,
|
||||||
)
|
)
|
||||||
|
|
||||||
const it = testEffect(env)
|
const it = testEffect(env)
|
||||||
|
|
@ -207,9 +209,10 @@ const providerErrorLLM = Layer.succeed(
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const providerErrorEnv = LayerNode.buildLayer(root, {
|
const providerErrorEnv = buildNode(root, [
|
||||||
replacements: [...replacements, LayerNode.replace(LLM.layer, providerErrorLLM)],
|
...replacements,
|
||||||
})
|
LayerNode.replace(LLM.layer, providerErrorLLM),
|
||||||
|
])
|
||||||
const itProviderError = testEffect(providerErrorEnv)
|
const itProviderError = testEffect(providerErrorEnv)
|
||||||
|
|
||||||
const fragmentFailureLLM = Layer.succeed(
|
const fragmentFailureLLM = Layer.succeed(
|
||||||
|
|
@ -226,9 +229,10 @@ const fragmentFailureLLM = Layer.succeed(
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const fragmentFailureEnv = LayerNode.buildLayer(root, {
|
const fragmentFailureEnv = buildNode(root, [
|
||||||
replacements: [...replacements, LayerNode.replace(LLM.layer, fragmentFailureLLM)],
|
...replacements,
|
||||||
})
|
LayerNode.replace(LLM.layer, fragmentFailureLLM),
|
||||||
|
])
|
||||||
const itFragmentFailure = testEffect(fragmentFailureEnv)
|
const itFragmentFailure = testEffect(fragmentFailureEnv)
|
||||||
|
|
||||||
const boot = Effect.fn("test.boot")(function* () {
|
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 { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-layer"
|
||||||
|
|
||||||
const summary = Layer.succeed(
|
const summary = Layer.succeed(
|
||||||
SessionSummary.Service,
|
SessionSummary.Service,
|
||||||
|
|
@ -230,7 +230,7 @@ function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; proces
|
||||||
Layer.provide(
|
Layer.provide(
|
||||||
SystemPrompt.layer.pipe(
|
SystemPrompt.layer.pipe(
|
||||||
Layer.provide(Skill.defaultLayer),
|
Layer.provide(Skill.defaultLayer),
|
||||||
Layer.provide(LocationServiceMap.layer),
|
Layer.provide(locationServiceMapLayer),
|
||||||
Layer.provide(deps),
|
Layer.provide(deps),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -656,6 +656,7 @@ noLLMServer.instance.skip(
|
||||||
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
|
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
|
||||||
Effect.provide(SessionExecution.noopLayer),
|
Effect.provide(SessionExecution.noopLayer),
|
||||||
Effect.provide(SessionV2.defaultLayer),
|
Effect.provide(SessionV2.defaultLayer),
|
||||||
|
Effect.provide(locationServiceMapLayer),
|
||||||
)
|
)
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
const row = yield* db
|
const row = yield* db
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { buildNode } from "@/effect/build-node"
|
||||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||||
import type { NamedError } from "@opencode-ai/core/util/error"
|
import type { NamedError } from "@opencode-ai/core/util/error"
|
||||||
import { APICallError } from "ai"
|
import { APICallError } from "ai"
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
import { setTimeout as sleep } from "node:timers/promises"
|
||||||
import { Effect, Schedule, Schema } from "effect"
|
import { Effect, Schedule, Schema } from "effect"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node-tree"
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { SessionRetry } from "../../src/session/retry"
|
import { SessionRetry } from "../../src/session/retry"
|
||||||
import { MessageV2 } from "../../src/session/message-v2"
|
import { MessageV2 } from "../../src/session/message-v2"
|
||||||
|
|
@ -16,7 +18,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
|
|
||||||
const providerID = ProviderV2.ID.make("test")
|
const providerID = ProviderV2.ID.make("test")
|
||||||
const retryProvider = "test"
|
const retryProvider = "test"
|
||||||
const it = testEffect(LayerNode.buildLayer(LayerNode.group([SessionStatus.node, CrossSpawnSpawner.node])))
|
const it = testEffect(buildNode(LayerNode.group([SessionStatus.node, CrossSpawnSpawner.node])))
|
||||||
|
|
||||||
function apiError(headers?: Record<string, string>): SessionV1.APIError {
|
function apiError(headers?: Record<string, string>): SessionV1.APIError {
|
||||||
return Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
|
return Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
* before tools run by hooking into start-step, but the AI SDK executes
|
* before tools run by hooking into start-step, but the AI SDK executes
|
||||||
* tools internally during multi-step processing before emitting events.
|
* tools internally during multi-step processing before emitting events.
|
||||||
*/
|
*/
|
||||||
|
import { buildNode } from "@/effect/build-node"
|
||||||
import { expect } from "bun:test"
|
import { expect } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
|
@ -87,13 +88,11 @@ const root = LayerNode.group([
|
||||||
LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] }),
|
LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] }),
|
||||||
])
|
])
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
LayerNode.buildLayer(root, {
|
buildNode(root, [
|
||||||
replacements: [
|
|
||||||
LayerNode.replace(MCP.layer, mcp),
|
LayerNode.replace(MCP.layer, mcp),
|
||||||
LayerNode.replace(LSP.layer, lsp),
|
LayerNode.replace(LSP.layer, lsp),
|
||||||
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer({ experimentalEventSystem: true })),
|
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer({ experimentalEventSystem: true })),
|
||||||
],
|
]),
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const providerCfg = (url: string) => ({
|
const providerCfg = (url: string) => ({
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue