feat: unwrap effect namespaces to flat exports + barrel (#22745)
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,67 +1,65 @@
|
|||||||
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 =>
|
|
||||||
Object.fromEntries(Object.entries(input ?? {}).filter((entry) => entry[1] !== undefined && entry[1] !== null))
|
|
||||||
|
|
||||||
const text = (input: unknown): string => {
|
|
||||||
if (Array.isArray(input)) return input.map((item) => String(item)).join(" ")
|
|
||||||
return input === undefined ? "" : String(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
const call = (run: (msg?: unknown) => Effect.Effect<void>, base: Fields, msg?: unknown, extra?: Fields) => {
|
|
||||||
const ann = clean({ ...base, ...extra })
|
|
||||||
const fx = run(msg)
|
|
||||||
return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx
|
|
||||||
}
|
|
||||||
|
|
||||||
export const logger = Logger.make((opts) => {
|
|
||||||
const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations))
|
|
||||||
const now = opts.date.getTime()
|
|
||||||
for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) {
|
|
||||||
extra[`logSpan.${key}`] = `${now - start}ms`
|
|
||||||
}
|
|
||||||
if (opts.cause.reasons.length > 0) {
|
|
||||||
extra.cause = Cause.pretty(opts.cause)
|
|
||||||
}
|
|
||||||
|
|
||||||
const svc = typeof extra.service === "string" ? extra.service : undefined
|
|
||||||
if (svc) delete extra.service
|
|
||||||
const log = svc ? Log.create({ service: svc }) : Log.Default
|
|
||||||
const msg = text(opts.message)
|
|
||||||
|
|
||||||
switch (opts.logLevel) {
|
|
||||||
case "Trace":
|
|
||||||
case "Debug":
|
|
||||||
return log.debug(msg, extra)
|
|
||||||
case "Warn":
|
|
||||||
return log.warn(msg, extra)
|
|
||||||
case "Error":
|
|
||||||
case "Fatal":
|
|
||||||
return log.error(msg, extra)
|
|
||||||
default:
|
|
||||||
return log.info(msg, extra)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const layer = Logger.layer([logger], { mergeWithExisting: false })
|
|
||||||
|
|
||||||
export const create = (base: Fields = {}): Handle => ({
|
|
||||||
debug: (msg, extra) => call((item) => Effect.logDebug(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),
|
|
||||||
error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra),
|
|
||||||
with: (extra) => create({ ...base, ...extra }),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clean = (input?: Fields): Fields =>
|
||||||
|
Object.fromEntries(Object.entries(input ?? {}).filter((entry) => entry[1] !== undefined && entry[1] !== null))
|
||||||
|
|
||||||
|
const text = (input: unknown): string => {
|
||||||
|
if (Array.isArray(input)) return input.map((item) => String(item)).join(" ")
|
||||||
|
return input === undefined ? "" : String(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
const call = (run: (msg?: unknown) => Effect.Effect<void>, base: Fields, msg?: unknown, extra?: Fields) => {
|
||||||
|
const ann = clean({ ...base, ...extra })
|
||||||
|
const fx = run(msg)
|
||||||
|
return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logger = Logger.make((opts) => {
|
||||||
|
const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations))
|
||||||
|
const now = opts.date.getTime()
|
||||||
|
for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) {
|
||||||
|
extra[`logSpan.${key}`] = `${now - start}ms`
|
||||||
|
}
|
||||||
|
if (opts.cause.reasons.length > 0) {
|
||||||
|
extra.cause = Cause.pretty(opts.cause)
|
||||||
|
}
|
||||||
|
|
||||||
|
const svc = typeof extra.service === "string" ? extra.service : undefined
|
||||||
|
if (svc) delete extra.service
|
||||||
|
const log = svc ? Log.create({ service: svc }) : Log.Default
|
||||||
|
const msg = text(opts.message)
|
||||||
|
|
||||||
|
switch (opts.logLevel) {
|
||||||
|
case "Trace":
|
||||||
|
case "Debug":
|
||||||
|
return log.debug(msg, extra)
|
||||||
|
case "Warn":
|
||||||
|
return log.warn(msg, extra)
|
||||||
|
case "Error":
|
||||||
|
case "Fatal":
|
||||||
|
return log.error(msg, extra)
|
||||||
|
default:
|
||||||
|
return log.info(msg, extra)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const layer = Logger.layer([logger], { mergeWithExisting: false })
|
||||||
|
|
||||||
|
export const create = (base: Fields = {}): Handle => ({
|
||||||
|
debug: (msg, extra) => call((item) => Effect.logDebug(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),
|
||||||
|
error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra),
|
||||||
|
with: (extra) => create({ ...base, ...extra }),
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,80 +1,78 @@
|
|||||||
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("=")
|
||||||
acc[key] = value.join("=")
|
acc[key] = value.join("=")
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
{} as Record<string, string>,
|
{} as Record<string, string>,
|
||||||
)
|
)
|
||||||
: 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({
|
||||||
url: `${base}/v1/logs`,
|
url: `${base}/v1/logs`,
|
||||||
resource,
|
resource,
|
||||||
|
headers,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
{ mergeWithExisting: false },
|
||||||
|
).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer))
|
||||||
|
|
||||||
|
const traces = async () => {
|
||||||
|
const NodeSdk = await import("@effect/opentelemetry/NodeSdk")
|
||||||
|
const OTLP = await import("@opentelemetry/exporter-trace-otlp-http")
|
||||||
|
const SdkBase = await import("@opentelemetry/sdk-trace-base")
|
||||||
|
|
||||||
|
// @effect/opentelemetry creates a NodeTracerProvider but never calls
|
||||||
|
// register(), so the global @opentelemetry/api context manager stays
|
||||||
|
// as the no-op default. Non-Effect code (like the AI SDK) that calls
|
||||||
|
// tracer.startActiveSpan() relies on context.active() to find the
|
||||||
|
// parent span — without a real context manager every span starts a
|
||||||
|
// new trace. Registering AsyncLocalStorageContextManager fixes this.
|
||||||
|
const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks")
|
||||||
|
const { context } = await import("@opentelemetry/api")
|
||||||
|
const mgr = new AsyncLocalStorageContextManager()
|
||||||
|
mgr.enable()
|
||||||
|
context.setGlobalContextManager(mgr)
|
||||||
|
|
||||||
|
return NodeSdk.layer(() => ({
|
||||||
|
resource,
|
||||||
|
spanProcessor: new SdkBase.BatchSpanProcessor(
|
||||||
|
new OTLP.OTLPTraceExporter({
|
||||||
|
url: `${base}/v1/traces`,
|
||||||
headers,
|
headers,
|
||||||
}),
|
}),
|
||||||
],
|
),
|
||||||
{ mergeWithExisting: false },
|
}))
|
||||||
).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer))
|
|
||||||
|
|
||||||
const traces = async () => {
|
|
||||||
const NodeSdk = await import("@effect/opentelemetry/NodeSdk")
|
|
||||||
const OTLP = await import("@opentelemetry/exporter-trace-otlp-http")
|
|
||||||
const SdkBase = await import("@opentelemetry/sdk-trace-base")
|
|
||||||
|
|
||||||
// @effect/opentelemetry creates a NodeTracerProvider but never calls
|
|
||||||
// register(), so the global @opentelemetry/api context manager stays
|
|
||||||
// as the no-op default. Non-Effect code (like the AI SDK) that calls
|
|
||||||
// tracer.startActiveSpan() relies on context.active() to find the
|
|
||||||
// parent span — without a real context manager every span starts a
|
|
||||||
// new trace. Registering AsyncLocalStorageContextManager fixes this.
|
|
||||||
const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks")
|
|
||||||
const { context } = await import("@opentelemetry/api")
|
|
||||||
const mgr = new AsyncLocalStorageContextManager()
|
|
||||||
mgr.enable()
|
|
||||||
context.setGlobalContextManager(mgr)
|
|
||||||
|
|
||||||
return NodeSdk.layer(() => ({
|
|
||||||
resource,
|
|
||||||
spanProcessor: new SdkBase.BatchSpanProcessor(
|
|
||||||
new OTLP.OTLPTraceExporter({
|
|
||||||
url: `${base}/v1/traces`,
|
|
||||||
headers,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const layer = !base
|
|
||||||
? EffectLogger.layer
|
|
||||||
: Layer.unwrap(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const trace = yield* Effect.promise(traces)
|
|
||||||
return Layer.mergeAll(trace, logs)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const layer = !base
|
||||||
|
? EffectLogger.layer
|
||||||
|
: Layer.unwrap(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const trace = yield* Effect.promise(traces)
|
||||||
|
return Layer.mergeAll(trace, logs)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -1,208 +1,206 @@
|
|||||||
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> {
|
||||||
|
id: number
|
||||||
|
fiber: Fiber.Fiber<A, E>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendingHandle<A, E> {
|
||||||
|
id: number
|
||||||
|
done: Deferred.Deferred<A, E | Cancelled>
|
||||||
|
work: Effect.Effect<A, E>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type State<A, E> =
|
||||||
|
| { readonly _tag: "Idle" }
|
||||||
|
| { readonly _tag: "Running"; readonly run: RunHandle<A, E> }
|
||||||
|
| { readonly _tag: "Shell"; readonly shell: ShellHandle<A, E> }
|
||||||
|
| { readonly _tag: "ShellThenRun"; readonly shell: ShellHandle<A, E>; readonly run: PendingHandle<A, E> }
|
||||||
|
|
||||||
|
export const make = <A, E = never>(
|
||||||
|
scope: Scope.Scope,
|
||||||
|
opts?: {
|
||||||
|
onIdle?: Effect.Effect<void>
|
||||||
|
onBusy?: Effect.Effect<void>
|
||||||
|
onInterrupt?: Effect.Effect<A, E>
|
||||||
|
busy?: () => never
|
||||||
|
},
|
||||||
|
): Runner<A, E> => {
|
||||||
|
const ref = SynchronizedRef.makeUnsafe<State<A, E>>({ _tag: "Idle" })
|
||||||
|
const idle = opts?.onIdle ?? Effect.void
|
||||||
|
const busy = opts?.onBusy ?? Effect.void
|
||||||
|
const onInterrupt = opts?.onInterrupt
|
||||||
|
let ids = 0
|
||||||
|
|
||||||
|
const state = () => SynchronizedRef.getUnsafe(ref)
|
||||||
|
const next = () => {
|
||||||
|
ids += 1
|
||||||
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ShellHandle<A, E> {
|
const complete = (done: Deferred.Deferred<A, E | Cancelled>, exit: Exit.Exit<A, E>) =>
|
||||||
id: number
|
Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)
|
||||||
fiber: Fiber.Fiber<A, E>
|
? Deferred.fail(done, new Cancelled()).pipe(Effect.asVoid)
|
||||||
}
|
: Deferred.done(done, exit).pipe(Effect.asVoid)
|
||||||
|
|
||||||
interface PendingHandle<A, E> {
|
const idleIfCurrent = () =>
|
||||||
id: number
|
SynchronizedRef.modify(ref, (st) => [st._tag === "Idle" ? idle : Effect.void, st] as const).pipe(Effect.flatten)
|
||||||
done: Deferred.Deferred<A, E | Cancelled>
|
|
||||||
work: Effect.Effect<A, E>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type State<A, E> =
|
const finishRun = (id: number, done: Deferred.Deferred<A, E | Cancelled>, exit: Exit.Exit<A, E>) =>
|
||||||
| { readonly _tag: "Idle" }
|
SynchronizedRef.modify(
|
||||||
| { readonly _tag: "Running"; readonly run: RunHandle<A, E> }
|
ref,
|
||||||
| { readonly _tag: "Shell"; readonly shell: ShellHandle<A, E> }
|
(st) =>
|
||||||
| { readonly _tag: "ShellThenRun"; readonly shell: ShellHandle<A, E>; readonly run: PendingHandle<A, E> }
|
[
|
||||||
|
Effect.gen(function* () {
|
||||||
|
if (st._tag === "Running" && st.run.id === id) yield* idle
|
||||||
|
yield* complete(done, exit)
|
||||||
|
}),
|
||||||
|
st._tag === "Running" && st.run.id === id ? ({ _tag: "Idle" } as const) : st,
|
||||||
|
] as const,
|
||||||
|
).pipe(Effect.flatten)
|
||||||
|
|
||||||
export const make = <A, E = never>(
|
const startRun = (work: Effect.Effect<A, E>, done: Deferred.Deferred<A, E | Cancelled>) =>
|
||||||
scope: Scope.Scope,
|
Effect.gen(function* () {
|
||||||
opts?: {
|
const id = next()
|
||||||
onIdle?: Effect.Effect<void>
|
const fiber = yield* work.pipe(
|
||||||
onBusy?: Effect.Effect<void>
|
Effect.onExit((exit) => finishRun(id, done, exit)),
|
||||||
onInterrupt?: Effect.Effect<A, E>
|
Effect.forkIn(scope),
|
||||||
busy?: () => never
|
|
||||||
},
|
|
||||||
): Runner<A, E> => {
|
|
||||||
const ref = SynchronizedRef.makeUnsafe<State<A, E>>({ _tag: "Idle" })
|
|
||||||
const idle = opts?.onIdle ?? Effect.void
|
|
||||||
const busy = opts?.onBusy ?? Effect.void
|
|
||||||
const onInterrupt = opts?.onInterrupt
|
|
||||||
let ids = 0
|
|
||||||
|
|
||||||
const state = () => SynchronizedRef.getUnsafe(ref)
|
|
||||||
const next = () => {
|
|
||||||
ids += 1
|
|
||||||
return ids
|
|
||||||
}
|
|
||||||
|
|
||||||
const complete = (done: Deferred.Deferred<A, E | Cancelled>, exit: Exit.Exit<A, E>) =>
|
|
||||||
Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)
|
|
||||||
? Deferred.fail(done, new Cancelled()).pipe(Effect.asVoid)
|
|
||||||
: Deferred.done(done, exit).pipe(Effect.asVoid)
|
|
||||||
|
|
||||||
const idleIfCurrent = () =>
|
|
||||||
SynchronizedRef.modify(ref, (st) => [st._tag === "Idle" ? idle : Effect.void, st] as const).pipe(Effect.flatten)
|
|
||||||
|
|
||||||
const finishRun = (id: number, done: Deferred.Deferred<A, E | Cancelled>, exit: Exit.Exit<A, E>) =>
|
|
||||||
SynchronizedRef.modify(
|
|
||||||
ref,
|
|
||||||
(st) =>
|
|
||||||
[
|
|
||||||
Effect.gen(function* () {
|
|
||||||
if (st._tag === "Running" && st.run.id === id) yield* idle
|
|
||||||
yield* complete(done, exit)
|
|
||||||
}),
|
|
||||||
st._tag === "Running" && st.run.id === id ? ({ _tag: "Idle" } as const) : st,
|
|
||||||
] as const,
|
|
||||||
).pipe(Effect.flatten)
|
|
||||||
|
|
||||||
const startRun = (work: Effect.Effect<A, E>, done: Deferred.Deferred<A, E | Cancelled>) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const id = next()
|
|
||||||
const fiber = yield* work.pipe(
|
|
||||||
Effect.onExit((exit) => finishRun(id, done, exit)),
|
|
||||||
Effect.forkIn(scope),
|
|
||||||
)
|
|
||||||
return { id, done, fiber } satisfies RunHandle<A, E>
|
|
||||||
})
|
|
||||||
|
|
||||||
const finishShell = (id: number) =>
|
|
||||||
SynchronizedRef.modifyEffect(
|
|
||||||
ref,
|
|
||||||
Effect.fnUntraced(function* (st) {
|
|
||||||
if (st._tag === "Shell" && st.shell.id === id) return [idle, { _tag: "Idle" }] as const
|
|
||||||
if (st._tag === "ShellThenRun" && st.shell.id === id) {
|
|
||||||
const run = yield* startRun(st.run.work, st.run.done)
|
|
||||||
return [Effect.void, { _tag: "Running", run }] as const
|
|
||||||
}
|
|
||||||
return [Effect.void, st] as const
|
|
||||||
}),
|
|
||||||
).pipe(Effect.flatten)
|
|
||||||
|
|
||||||
const stopShell = (shell: ShellHandle<A, E>) => Fiber.interrupt(shell.fiber)
|
|
||||||
|
|
||||||
const ensureRunning = (work: Effect.Effect<A, E>) =>
|
|
||||||
SynchronizedRef.modifyEffect(
|
|
||||||
ref,
|
|
||||||
Effect.fnUntraced(function* (st) {
|
|
||||||
switch (st._tag) {
|
|
||||||
case "Running":
|
|
||||||
case "ShellThenRun":
|
|
||||||
return [Deferred.await(st.run.done), st] as const
|
|
||||||
case "Shell": {
|
|
||||||
const run = {
|
|
||||||
id: next(),
|
|
||||||
done: yield* Deferred.make<A, E | Cancelled>(),
|
|
||||||
work,
|
|
||||||
} satisfies PendingHandle<A, E>
|
|
||||||
return [Deferred.await(run.done), { _tag: "ShellThenRun", shell: st.shell, run }] as const
|
|
||||||
}
|
|
||||||
case "Idle": {
|
|
||||||
const done = yield* Deferred.make<A, E | Cancelled>()
|
|
||||||
const run = yield* startRun(work, done)
|
|
||||||
return [Deferred.await(done), { _tag: "Running", run }] as const
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
).pipe(
|
|
||||||
Effect.flatten,
|
|
||||||
Effect.catch(
|
|
||||||
(e): Effect.Effect<A, E> => (e instanceof Cancelled ? (onInterrupt ?? Effect.die(e)) : Effect.fail(e as E)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
return { id, done, fiber } satisfies RunHandle<A, E>
|
||||||
|
})
|
||||||
|
|
||||||
const startShell = (work: Effect.Effect<A, E>) =>
|
const finishShell = (id: number) =>
|
||||||
SynchronizedRef.modifyEffect(
|
SynchronizedRef.modifyEffect(
|
||||||
ref,
|
ref,
|
||||||
Effect.fnUntraced(function* (st) {
|
Effect.fnUntraced(function* (st) {
|
||||||
if (st._tag !== "Idle") {
|
if (st._tag === "Shell" && st.shell.id === id) return [idle, { _tag: "Idle" }] as const
|
||||||
return [
|
if (st._tag === "ShellThenRun" && st.shell.id === id) {
|
||||||
Effect.sync(() => {
|
const run = yield* startRun(st.run.work, st.run.done)
|
||||||
if (opts?.busy) opts.busy()
|
return [Effect.void, { _tag: "Running", run }] as const
|
||||||
throw new Error("Runner is busy")
|
}
|
||||||
}),
|
return [Effect.void, st] as const
|
||||||
st,
|
}),
|
||||||
] as const
|
).pipe(Effect.flatten)
|
||||||
|
|
||||||
|
const stopShell = (shell: ShellHandle<A, E>) => Fiber.interrupt(shell.fiber)
|
||||||
|
|
||||||
|
const ensureRunning = (work: Effect.Effect<A, E>) =>
|
||||||
|
SynchronizedRef.modifyEffect(
|
||||||
|
ref,
|
||||||
|
Effect.fnUntraced(function* (st) {
|
||||||
|
switch (st._tag) {
|
||||||
|
case "Running":
|
||||||
|
case "ShellThenRun":
|
||||||
|
return [Deferred.await(st.run.done), st] as const
|
||||||
|
case "Shell": {
|
||||||
|
const run = {
|
||||||
|
id: next(),
|
||||||
|
done: yield* Deferred.make<A, E | Cancelled>(),
|
||||||
|
work,
|
||||||
|
} satisfies PendingHandle<A, E>
|
||||||
|
return [Deferred.await(run.done), { _tag: "ShellThenRun", shell: st.shell, run }] as const
|
||||||
}
|
}
|
||||||
yield* busy
|
case "Idle": {
|
||||||
const id = next()
|
const done = yield* Deferred.make<A, E | Cancelled>()
|
||||||
const fiber = yield* work.pipe(Effect.ensuring(finishShell(id)), Effect.forkChild)
|
const run = yield* startRun(work, done)
|
||||||
const shell = { id, fiber } satisfies ShellHandle<A, E>
|
return [Deferred.await(done), { _tag: "Running", run }] as const
|
||||||
return [
|
}
|
||||||
Effect.gen(function* () {
|
}
|
||||||
const exit = yield* Fiber.await(fiber)
|
}),
|
||||||
if (Exit.isSuccess(exit)) return exit.value
|
).pipe(
|
||||||
if (Cause.hasInterruptsOnly(exit.cause) && onInterrupt) return yield* onInterrupt
|
Effect.flatten,
|
||||||
return yield* Effect.failCause(exit.cause)
|
Effect.catch(
|
||||||
}),
|
(e): Effect.Effect<A, E> => (e instanceof Cancelled ? (onInterrupt ?? Effect.die(e)) : Effect.fail(e as E)),
|
||||||
{ _tag: "Shell", shell },
|
),
|
||||||
] as const
|
)
|
||||||
}),
|
|
||||||
).pipe(Effect.flatten)
|
|
||||||
|
|
||||||
const cancel = SynchronizedRef.modify(ref, (st) => {
|
const startShell = (work: Effect.Effect<A, E>) =>
|
||||||
switch (st._tag) {
|
SynchronizedRef.modifyEffect(
|
||||||
case "Idle":
|
ref,
|
||||||
return [Effect.void, st] as const
|
Effect.fnUntraced(function* (st) {
|
||||||
case "Running":
|
if (st._tag !== "Idle") {
|
||||||
return [
|
return [
|
||||||
Effect.gen(function* () {
|
Effect.sync(() => {
|
||||||
yield* Fiber.interrupt(st.run.fiber)
|
if (opts?.busy) opts.busy()
|
||||||
yield* Deferred.await(st.run.done).pipe(Effect.exit, Effect.asVoid)
|
throw new Error("Runner is busy")
|
||||||
yield* idleIfCurrent()
|
|
||||||
}),
|
}),
|
||||||
{ _tag: "Idle" } as const,
|
st,
|
||||||
] as const
|
] as const
|
||||||
case "Shell":
|
}
|
||||||
return [
|
yield* busy
|
||||||
Effect.gen(function* () {
|
const id = next()
|
||||||
yield* stopShell(st.shell)
|
const fiber = yield* work.pipe(Effect.ensuring(finishShell(id)), Effect.forkChild)
|
||||||
yield* idleIfCurrent()
|
const shell = { id, fiber } satisfies ShellHandle<A, E>
|
||||||
}),
|
return [
|
||||||
{ _tag: "Idle" } as const,
|
Effect.gen(function* () {
|
||||||
] as const
|
const exit = yield* Fiber.await(fiber)
|
||||||
case "ShellThenRun":
|
if (Exit.isSuccess(exit)) return exit.value
|
||||||
return [
|
if (Cause.hasInterruptsOnly(exit.cause) && onInterrupt) return yield* onInterrupt
|
||||||
Effect.gen(function* () {
|
return yield* Effect.failCause(exit.cause)
|
||||||
yield* Deferred.fail(st.run.done, new Cancelled()).pipe(Effect.asVoid)
|
}),
|
||||||
yield* stopShell(st.shell)
|
{ _tag: "Shell", shell },
|
||||||
yield* idleIfCurrent()
|
] as const
|
||||||
}),
|
}),
|
||||||
{ _tag: "Idle" } as const,
|
).pipe(Effect.flatten)
|
||||||
] as const
|
|
||||||
}
|
|
||||||
}).pipe(Effect.flatten)
|
|
||||||
|
|
||||||
return {
|
const cancel = SynchronizedRef.modify(ref, (st) => {
|
||||||
get state() {
|
switch (st._tag) {
|
||||||
return state()
|
case "Idle":
|
||||||
},
|
return [Effect.void, st] as const
|
||||||
get busy() {
|
case "Running":
|
||||||
return state()._tag !== "Idle"
|
return [
|
||||||
},
|
Effect.gen(function* () {
|
||||||
ensureRunning,
|
yield* Fiber.interrupt(st.run.fiber)
|
||||||
startShell,
|
yield* Deferred.await(st.run.done).pipe(Effect.exit, Effect.asVoid)
|
||||||
cancel,
|
yield* idleIfCurrent()
|
||||||
|
}),
|
||||||
|
{ _tag: "Idle" } as const,
|
||||||
|
] as const
|
||||||
|
case "Shell":
|
||||||
|
return [
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* stopShell(st.shell)
|
||||||
|
yield* idleIfCurrent()
|
||||||
|
}),
|
||||||
|
{ _tag: "Idle" } as const,
|
||||||
|
] as const
|
||||||
|
case "ShellThenRun":
|
||||||
|
return [
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* Deferred.fail(st.run.done, new Cancelled()).pipe(Effect.asVoid)
|
||||||
|
yield* stopShell(st.shell)
|
||||||
|
yield* idleIfCurrent()
|
||||||
|
}),
|
||||||
|
{ _tag: "Idle" } as const,
|
||||||
|
] as const
|
||||||
}
|
}
|
||||||
|
}).pipe(Effect.flatten)
|
||||||
|
|
||||||
|
return {
|
||||||
|
get state() {
|
||||||
|
return state()
|
||||||
|
},
|
||||||
|
get busy() {
|
||||||
|
return state()._tag !== "Idle"
|
||||||
|
},
|
||||||
|
ensureRunning,
|
||||||
|
startShell,
|
||||||
|
cancel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user