feat: unwrap effect namespaces to flat exports + barrel (#22745)

This commit is contained in:
Kit Langton 2026-04-15 23:29:12 -04:00 committed by GitHub
commit d4cfbd020d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 318 additions and 321 deletions

View file

@ -1,6 +1,6 @@
import { Layer, ManagedRuntime } from "effect" import { Layer, ManagedRuntime } from "effect"
import { attach, memoMap } from "./run-service" import { attach, memoMap } from "./run-service"
import { Observability } from "./observability" import { Observability } from "."
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Bus } from "@/bus" import { Bus } from "@/bus"

View file

@ -10,7 +10,7 @@ import { File } from "@/file"
import { Vcs } from "@/project" import { Vcs } from "@/project"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { Observability } from "./observability" import { Observability } from "."
export const BootstrapLayer = Layer.mergeAll( export const BootstrapLayer = Layer.mergeAll(
Plugin.defaultLayer, Plugin.defaultLayer,

View file

@ -1,2 +1,5 @@
export * as InstanceState from "./instance-state" export * as InstanceState from "./instance-state"
export * as EffectBridge from "./bridge" export * as EffectBridge from "./bridge"
export * as Runner from "./runner"
export * as Observability from "./observability"
export * as EffectLogger from "./logger"

View file

@ -1,5 +1,5 @@
import { Effect, Fiber, ScopedCache, Scope, Context } from "effect" import { Effect, Fiber, ScopedCache, Scope, Context } from "effect"
import { EffectLogger } from "@/effect/logger" import { EffectLogger } from "@/effect"
import { Instance, type InstanceContext } from "@/project/instance" import { Instance, type InstanceContext } from "@/project/instance"
import { LocalContext } from "@/util" import { LocalContext } from "@/util"
import { InstanceRef, WorkspaceRef } from "./instance-ref" import { InstanceRef, WorkspaceRef } from "./instance-ref"

View file

@ -1,32 +1,31 @@
import { Cause, Effect, Logger, References } from "effect" import { Cause, Effect, Logger, References } from "effect"
import { Log } from "@/util" import { Log } from "@/util"
export namespace EffectLogger { type Fields = Record<string, unknown>
type Fields = Record<string, unknown>
export interface Handle { export interface Handle {
readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect<void> readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect<void> readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect<void> readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect<void> readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
readonly with: (extra: Fields) => Handle readonly with: (extra: Fields) => Handle
} }
const clean = (input?: Fields): Fields => const clean = (input?: Fields): Fields =>
Object.fromEntries(Object.entries(input ?? {}).filter((entry) => entry[1] !== undefined && entry[1] !== null)) Object.fromEntries(Object.entries(input ?? {}).filter((entry) => entry[1] !== undefined && entry[1] !== null))
const text = (input: unknown): string => { const text = (input: unknown): string => {
if (Array.isArray(input)) return input.map((item) => String(item)).join(" ") if (Array.isArray(input)) return input.map((item) => String(item)).join(" ")
return input === undefined ? "" : String(input) return input === undefined ? "" : String(input)
} }
const call = (run: (msg?: unknown) => Effect.Effect<void>, base: Fields, msg?: unknown, extra?: Fields) => { const call = (run: (msg?: unknown) => Effect.Effect<void>, base: Fields, msg?: unknown, extra?: Fields) => {
const ann = clean({ ...base, ...extra }) const ann = clean({ ...base, ...extra })
const fx = run(msg) const fx = run(msg)
return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx
} }
export const logger = Logger.make((opts) => { export const logger = Logger.make((opts) => {
const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations)) const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations))
const now = opts.date.getTime() const now = opts.date.getTime()
for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) { for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) {
@ -53,15 +52,14 @@ export namespace EffectLogger {
default: default:
return log.info(msg, extra) return log.info(msg, extra)
} }
}) })
export const layer = Logger.layer([logger], { mergeWithExisting: false }) export const layer = Logger.layer([logger], { mergeWithExisting: false })
export const create = (base: Fields = {}): Handle => ({ export const create = (base: Fields = {}): Handle => ({
debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra), debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra),
info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra), info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra),
warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra), warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra),
error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra), error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra),
with: (extra) => create({ ...base, ...extra }), with: (extra) => create({ ...base, ...extra }),
}) })
}

View file

@ -1,15 +1,14 @@
import { Effect, Layer, Logger } from "effect" import { Effect, Layer, Logger } from "effect"
import { FetchHttpClient } from "effect/unstable/http" import { FetchHttpClient } from "effect/unstable/http"
import { OtlpLogger, OtlpSerialization } from "effect/unstable/observability" import { OtlpLogger, OtlpSerialization } from "effect/unstable/observability"
import { EffectLogger } from "@/effect/logger" import { EffectLogger } from "@/effect"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { CHANNEL, VERSION } from "@/installation/meta" import { CHANNEL, VERSION } from "@/installation/meta"
export namespace Observability { const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT
const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT export const enabled = !!base
export const enabled = !!base
const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS
? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce( ? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce(
(acc, x) => { (acc, x) => {
const [key, ...value] = x.split("=") const [key, ...value] = x.split("=")
@ -20,16 +19,16 @@ export namespace Observability {
) )
: undefined : undefined
const resource = { const resource = {
serviceName: "opencode", serviceName: "opencode",
serviceVersion: VERSION, serviceVersion: VERSION,
attributes: { attributes: {
"deployment.environment.name": CHANNEL === "local" ? "local" : CHANNEL, "deployment.environment.name": CHANNEL === "local" ? "local" : CHANNEL,
"opencode.client": Flag.OPENCODE_CLIENT, "opencode.client": Flag.OPENCODE_CLIENT,
}, },
} }
const logs = Logger.layer( const logs = Logger.layer(
[ [
EffectLogger.logger, EffectLogger.logger,
OtlpLogger.make({ OtlpLogger.make({
@ -39,9 +38,9 @@ export namespace Observability {
}), }),
], ],
{ mergeWithExisting: false }, { mergeWithExisting: false },
).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer)) ).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer))
const traces = async () => { const traces = async () => {
const NodeSdk = await import("@effect/opentelemetry/NodeSdk") const NodeSdk = await import("@effect/opentelemetry/NodeSdk")
const OTLP = await import("@opentelemetry/exporter-trace-otlp-http") const OTLP = await import("@opentelemetry/exporter-trace-otlp-http")
const SdkBase = await import("@opentelemetry/sdk-trace-base") const SdkBase = await import("@opentelemetry/sdk-trace-base")
@ -67,9 +66,9 @@ export namespace Observability {
}), }),
), ),
})) }))
} }
export const layer = !base export const layer = !base
? EffectLogger.layer ? EffectLogger.layer
: Layer.unwrap( : Layer.unwrap(
Effect.gen(function* () { Effect.gen(function* () {
@ -77,4 +76,3 @@ export namespace Observability {
return Layer.mergeAll(trace, logs) return Layer.mergeAll(trace, logs)
}), }),
) )
}

View file

@ -3,7 +3,7 @@ import * as Context from "effect/Context"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { LocalContext } from "@/util" import { LocalContext } from "@/util"
import { InstanceRef, WorkspaceRef } from "./instance-ref" import { InstanceRef, WorkspaceRef } from "./instance-ref"
import { Observability } from "./observability" import { Observability } from "."
import { WorkspaceContext } from "@/control-plane/workspace-context" import { WorkspaceContext } from "@/control-plane/workspace-context"
import type { InstanceContext } from "@/project/instance" import type { InstanceContext } from "@/project/instance"

View file

@ -1,40 +1,39 @@
import { Cause, Deferred, Effect, Exit, Fiber, Schema, Scope, SynchronizedRef } from "effect" import { Cause, Deferred, Effect, Exit, Fiber, Schema, Scope, SynchronizedRef } from "effect"
export interface Runner<A, E = never> { export interface Runner<A, E = never> {
readonly state: Runner.State<A, E> readonly state: State<A, E>
readonly busy: boolean readonly busy: boolean
readonly ensureRunning: (work: Effect.Effect<A, E>) => Effect.Effect<A, E> readonly ensureRunning: (work: Effect.Effect<A, E>) => Effect.Effect<A, E>
readonly startShell: (work: Effect.Effect<A, E>) => Effect.Effect<A, E> readonly startShell: (work: Effect.Effect<A, E>) => Effect.Effect<A, E>
readonly cancel: Effect.Effect<void> readonly cancel: Effect.Effect<void>
} }
export namespace Runner { export class Cancelled extends Schema.TaggedErrorClass<Cancelled>()("RunnerCancelled", {}) {}
export class Cancelled extends Schema.TaggedErrorClass<Cancelled>()("RunnerCancelled", {}) {}
interface RunHandle<A, E> { interface RunHandle<A, E> {
id: number id: number
done: Deferred.Deferred<A, E | Cancelled> done: Deferred.Deferred<A, E | Cancelled>
fiber: Fiber.Fiber<A, E> fiber: Fiber.Fiber<A, E>
} }
interface ShellHandle<A, E> { interface ShellHandle<A, E> {
id: number id: number
fiber: Fiber.Fiber<A, E> fiber: Fiber.Fiber<A, E>
} }
interface PendingHandle<A, E> { interface PendingHandle<A, E> {
id: number id: number
done: Deferred.Deferred<A, E | Cancelled> done: Deferred.Deferred<A, E | Cancelled>
work: Effect.Effect<A, E> work: Effect.Effect<A, E>
} }
export type State<A, E> = export type State<A, E> =
| { readonly _tag: "Idle" } | { readonly _tag: "Idle" }
| { readonly _tag: "Running"; readonly run: RunHandle<A, E> } | { readonly _tag: "Running"; readonly run: RunHandle<A, E> }
| { readonly _tag: "Shell"; readonly shell: ShellHandle<A, E> } | { readonly _tag: "Shell"; readonly shell: ShellHandle<A, E> }
| { readonly _tag: "ShellThenRun"; readonly shell: ShellHandle<A, E>; readonly run: PendingHandle<A, E> } | { readonly _tag: "ShellThenRun"; readonly shell: ShellHandle<A, E>; readonly run: PendingHandle<A, E> }
export const make = <A, E = never>( export const make = <A, E = never>(
scope: Scope.Scope, scope: Scope.Scope,
opts?: { opts?: {
onIdle?: Effect.Effect<void> onIdle?: Effect.Effect<void>
@ -42,7 +41,7 @@ export namespace Runner {
onInterrupt?: Effect.Effect<A, E> onInterrupt?: Effect.Effect<A, E>
busy?: () => never busy?: () => never
}, },
): Runner<A, E> => { ): Runner<A, E> => {
const ref = SynchronizedRef.makeUnsafe<State<A, E>>({ _tag: "Idle" }) const ref = SynchronizedRef.makeUnsafe<State<A, E>>({ _tag: "Idle" })
const idle = opts?.onIdle ?? Effect.void const idle = opts?.onIdle ?? Effect.void
const busy = opts?.onBusy ?? Effect.void const busy = opts?.onBusy ?? Effect.void
@ -204,5 +203,4 @@ export namespace Runner {
startShell, startShell,
cancel, cancel,
} }
}
} }

View file

@ -3,7 +3,7 @@ import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/unsta
import { HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http" import { HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref" import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
import { Observability } from "@/effect/observability" import { Observability } from "@/effect"
import { memoMap } from "@/effect/run-service" import { memoMap } from "@/effect/run-service"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { InstanceBootstrap } from "@/project/bootstrap" import { InstanceBootstrap } from "@/project/bootstrap"

View file

@ -15,7 +15,7 @@ import type { SystemError } from "bun"
import type { Provider } from "@/provider" import type { Provider } from "@/provider"
import { ModelID, ProviderID } from "@/provider/schema" import { ModelID, ProviderID } from "@/provider/schema"
import { Effect } from "effect" import { Effect } from "effect"
import { EffectLogger } from "@/effect/logger" import { EffectLogger } from "@/effect"
/** 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 {

View file

@ -44,7 +44,7 @@ import { Truncate } from "@/tool/truncate"
import { decodeDataUrl } from "@/util/data-url" import { decodeDataUrl } from "@/util/data-url"
import { Process } from "@/util" import { Process } from "@/util"
import { Cause, Effect, Exit, Layer, Option, Scope, Context } from "effect" import { Cause, Effect, Exit, Layer, Option, Scope, Context } from "effect"
import { EffectLogger } from "@/effect/logger" import { EffectLogger } from "@/effect"
import { InstanceState } from "@/effect" import { InstanceState } from "@/effect"
import { TaskTool, type TaskPromptOps } from "@/tool/task" import { TaskTool, type TaskPromptOps } from "@/tool/task"
import { SessionRunState } from "./run-state" import { SessionRunState } from "./run-state"

View file

@ -1,5 +1,5 @@
import { InstanceState } from "@/effect" import { InstanceState } from "@/effect"
import { Runner } from "@/effect/runner" import { Runner } from "@/effect"
import { Effect, Layer, Scope, Context } from "effect" import { Effect, Layer, Scope, Context } from "effect"
import { Session } from "." import { Session } from "."
import { MessageV2 } from "./message-v2" import { MessageV2 } from "./message-v2"
@ -32,7 +32,7 @@ export namespace SessionRunState {
const state = yield* InstanceState.make( const state = yield* InstanceState.make(
Effect.fn("SessionRunState.state")(function* () { Effect.fn("SessionRunState.state")(function* () {
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
const runners = new Map<SessionID, Runner<MessageV2.WithParts>>() const runners = new Map<SessionID, Runner.Runner<MessageV2.WithParts>>()
yield* Effect.addFinalizer( yield* Effect.addFinalizer(
Effect.fnUntraced(function* () { Effect.fnUntraced(function* () {
yield* Effect.forEach(runners.values(), (runner) => runner.cancel, { yield* Effect.forEach(runners.values(), (runner) => runner.cancel, {

View file

@ -1,6 +1,6 @@
import path from "path" import path from "path"
import { Effect } from "effect" import { Effect } from "effect"
import { EffectLogger } from "@/effect/logger" import { EffectLogger } from "@/effect"
import { InstanceState } from "@/effect" import { InstanceState } from "@/effect"
import type { Tool } from "./tool" import type { Tool } from "./tool"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"

View file

@ -3,7 +3,7 @@ import { pathToFileURL } from "url"
import z from "zod" import z from "zod"
import { Effect } from "effect" import { Effect } from "effect"
import * as Stream from "effect/Stream" import * as Stream from "effect/Stream"
import { EffectLogger } from "@/effect/logger" import { EffectLogger } from "@/effect"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file/ripgrep"
import { Skill } from "../skill" import { Skill } from "../skill"
import { Tool } from "./tool" import { Tool } from "./tool"

View file

@ -3,7 +3,7 @@ import { Context, Effect, Layer, Logger } from "effect"
import { AppRuntime } from "../../src/effect/app-runtime" import { AppRuntime } from "../../src/effect/app-runtime"
import { EffectBridge } from "../../src/effect" import { EffectBridge } from "../../src/effect"
import { InstanceRef } from "../../src/effect/instance-ref" import { InstanceRef } from "../../src/effect/instance-ref"
import { EffectLogger } from "../../src/effect/logger" import { EffectLogger } from "../../src/effect"
import { makeRuntime } from "../../src/effect/run-service" import { makeRuntime } from "../../src/effect/run-service"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture"

View file

@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { Deferred, Effect, Exit, Fiber, Ref, Scope } from "effect" import { Deferred, Effect, Exit, Fiber, Ref, Scope } from "effect"
import { Runner } from "../../src/effect/runner" import { Runner } from "../../src/effect"
import { it } from "../lib/effect" import { it } from "../lib/effect"
describe("Runner", () => { describe("Runner", () => {