refactor(core): replace legacy logger with Effect logging (#31310)

This commit is contained in:
Dax
2026-06-08 15:41:56 -04:00
committed by GitHub
parent af9dff96df
commit a5ddb7f291
152 changed files with 698 additions and 2243 deletions
+6 -7
View File
@@ -5,7 +5,6 @@ import { SessionID, MessageID, PartID } from "./schema"
import { Provider } from "@/provider/provider"
import { MessageV2 } from "./message-v2"
import { Token } from "@/util/token"
import { Log } from "@opencode-ai/core/util/log"
import { SessionProcessor } from "./processor"
import { Agent } from "@/agent/agent"
import { Plugin } from "@/plugin"
@@ -26,8 +25,6 @@ import { ModelV2 } from "@opencode-ai/core/model"
import { EventV2 } from "@opencode-ai/core/event"
import { buildPrompt } from "@opencode-ai/core/session/compaction"
const log = Log.create({ service: "session.compaction" })
export const Event = {
Compacted: EventV2.define({
type: "session.compacted",
@@ -237,7 +234,9 @@ export const layer = Layer.effect(
estimate,
})
if (split) keep = split
else if (!keep) log.info("tail fallback", { budget, size, total })
else if (!keep) {
yield* Effect.logInfo("tail fallback", { budget, size, total })
}
break
}
@@ -253,7 +252,7 @@ export const layer = Layer.effect(
const prune = Effect.fn("SessionCompaction.prune")(function* (input: { sessionID: SessionID }) {
const cfg = yield* config.get()
if (!cfg.compaction?.prune) return
log.info("pruning")
yield* Effect.logInfo("pruning")
const msgs = yield* session
.messages({ sessionID: input.sessionID })
@@ -284,7 +283,7 @@ export const layer = Layer.effect(
}
}
log.info("found", { pruned, total })
yield* Effect.logInfo("found", { pruned, total })
if (pruned > PRUNE_MINIMUM) {
for (const part of toPrune) {
if (part.state.status === "completed") {
@@ -292,7 +291,7 @@ export const layer = Layer.effect(
yield* session.updatePart(part)
}
}
log.info("pruned", { count: toPrune.length })
yield* Effect.logInfo("pruned", { count: toPrune.length })
}
})
+30 -43
View File
@@ -2,7 +2,6 @@ import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Provider } from "@/provider/provider"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Log } from "@opencode-ai/core/util/log"
import { Context, Effect, Layer } from "effect"
import * as Stream from "effect/Stream"
import { streamText, wrapLanguageModel, type ModelMessage, type Tool } from "ai"
@@ -29,7 +28,6 @@ import { LLMAISDK } from "./llm/ai-sdk"
import { LLMNativeRuntime } from "./llm/native-runtime"
import { LLMRequestPrep } from "./llm/request"
const log = Log.create({ service: "llm" })
export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX
export type StreamInput = {
@@ -83,17 +81,13 @@ const live: Layer.Layer<
const flags = yield* RuntimeFlags.Service
const run = Effect.fn("LLM.run")(function* (input: StreamRequest) {
const l = log
.clone()
.tag("providerID", input.model.providerID)
.tag("modelID", input.model.id)
.tag("session.id", input.sessionID)
.tag("small", (input.small ?? false).toString())
.tag("agent", input.agent.name)
.tag("mode", input.agent.mode)
l.info("stream", {
modelID: input.model.id,
yield* Effect.logInfo("stream", {
providerID: input.model.providerID,
modelID: input.model.id,
"session.id": input.sessionID,
small: (input.small ?? false).toString(),
agent: input.agent.name,
mode: input.agent.mode,
})
const [language, cfg, item, info] = yield* Effect.all(
@@ -245,36 +239,38 @@ const live: Layer.Layer<
abort: input.abort,
})
if (native.type === "supported") {
yield* Effect.logInfo("llm runtime selected").pipe(
Effect.annotateLogs({
"llm.runtime": "native",
"llm.provider": input.model.providerID,
"llm.model": input.model.id,
}),
)
yield* Effect.logInfo("llm runtime selected", {
"llm.runtime": "native",
"llm.provider": input.model.providerID,
"llm.model": input.model.id,
})
return {
type: "native" as const,
stream: native.stream,
}
}
yield* Effect.logInfo("llm runtime selected").pipe(
Effect.annotateLogs({
"llm.runtime": "ai-sdk",
"llm.provider": input.model.providerID,
"llm.model": input.model.id,
"llm.native_unsupported_reason": native.reason,
}),
)
l.info("native runtime unavailable; falling back to ai-sdk", { reason: native.reason })
}
yield* Effect.logInfo("llm runtime selected").pipe(
Effect.annotateLogs({
yield* Effect.logInfo("llm runtime selected", {
"llm.runtime": "ai-sdk",
"llm.provider": input.model.providerID,
"llm.model": input.model.id,
}),
)
"llm.native_unsupported_reason": native.reason,
})
yield* Effect.logInfo("native runtime unavailable; falling back to ai-sdk", {
providerID: input.model.providerID,
modelID: input.model.id,
"session.id": input.sessionID,
small: (input.small ?? false).toString(),
agent: input.agent.name,
mode: input.agent.mode,
reason: native.reason,
})
}
yield* Effect.logInfo("llm runtime selected", {
"llm.runtime": "ai-sdk",
"llm.provider": input.model.providerID,
"llm.model": input.model.id,
})
// Default runtime path: AI SDK owns provider execution and tool dispatch;
// LLMAISDK.toLLMEvents below normalizes fullStream parts for the processor.
return {
@@ -282,18 +278,9 @@ const live: Layer.Layer<
result: streamText({
// Copilot returns the authoritative billed amount only in provider-specific response fields.
includeRawChunks: input.model.providerID.includes("github-copilot"),
onError(error) {
l.error("stream error", {
error,
})
},
async experimental_repairToolCall(failed) {
const lower = failed.toolCall.toolName.toLowerCase()
if (lower !== failed.toolCall.toolName && prepared.tools[lower]) {
l.info("repairing tool call", {
tool: failed.toolCall.toolName,
repaired: lower,
})
return {
...failed.toolCall,
toolName: lower,
+1 -2
View File
@@ -37,7 +37,6 @@ import { isMedia } from "@/util/media"
import type { SystemError } from "bun"
import type { Provider } from "@/provider/provider"
import { Effect, Schema } from "effect"
import * as EffectLogger from "@opencode-ai/core/effect/logger"
/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
interface FetchDecompressionError extends Error {
@@ -431,7 +430,7 @@ export function toModelMessages(
model: Provider.Model,
options?: { stripMedia?: boolean; toolOutputMaxChars?: number },
): Promise<ModelMessage[]> {
return Effect.runPromise(toModelMessagesEffect(input, model, options).pipe(Effect.provide(EffectLogger.layer)))
return Effect.runPromise(toModelMessagesEffect(input, model, options))
}
export const page = Effect.fn("MessageV2.page")(function* (input: {
+10 -6
View File
@@ -20,7 +20,6 @@ import { SessionSummary } from "./summary"
import type { Provider } from "@/provider/provider"
import { Question } from "@/question"
import { errorMessage } from "@/util/error"
import { Log } from "@opencode-ai/core/util/log"
import { isRecord } from "@/util/record"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Database } from "@opencode-ai/core/database/database"
@@ -34,8 +33,6 @@ import { toolFileSourceFromUri, Usage, type LLMEvent } from "@opencode-ai/llm"
import { ToolOutput } from "@opencode-ai/core/tool-output"
const DOOM_LOOP_THRESHOLD = 3
const log = Log.create({ service: "session.processor" })
export type Result = "compact" | "stop" | "continue"
export interface Handle {
@@ -131,7 +128,6 @@ export const layer = Layer.effect(
}
const mirrorAssistant = flags.experimentalEventSystem && !input.assistantMessage.summary
let aborted = false
const slog = log.clone().tag("session.id", input.sessionID).tag("messageID", input.assistantMessage.id)
const parse = (e: unknown) =>
MessageV2.fromError(e, {
@@ -918,7 +914,12 @@ export const layer = Layer.effect(
})
const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) {
slog.error("process", { error: errorMessage(e), stack: e instanceof Error ? e.stack : undefined })
yield* Effect.logError("process", {
"session.id": input.sessionID,
messageID: input.assistantMessage.id,
error: errorMessage(e),
stack: e instanceof Error ? e.stack : undefined,
})
const error = parse(e)
yield* flushV2Fragments()
if (SessionV1.ContextOverflowError.isInstance(error)) {
@@ -956,7 +957,10 @@ export const layer = Layer.effect(
})
const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) {
slog.info("process")
yield* Effect.logInfo("process", {
"session.id": input.sessionID,
messageID: input.assistantMessage.id,
})
ctx.needsCompaction = false
ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true
+26 -24
View File
@@ -4,7 +4,6 @@ import { SessionV1 } from "@opencode-ai/core/v1/session"
import os from "os"
import { SessionID, MessageID, PartID } from "./schema"
import { MessageV2 } from "./message-v2"
import { Log } from "@opencode-ai/core/util/log"
import { SessionRevert } from "./revert"
import { Session } from "./session"
import { Agent } from "../agent/agent"
@@ -43,7 +42,6 @@ import { Image } from "@/image/image"
import { decodeDataUrl } from "@/util/data-url"
import { Process } from "@/util/process"
import { Cause, Effect, Exit, Latch, Layer, Option, Scope, Context, Schema, Types } from "effect"
import * as EffectLogger from "@opencode-ai/core/effect/logger"
import { InstanceState } from "@/effect/instance-state"
import { TaskTool, type TaskPromptOps } from "@/tool/task"
import { SessionRunState } from "./run-state"
@@ -80,9 +78,6 @@ IMPORTANT:
const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested structured output. You MUST use the StructuredOutput tool to provide your final response. Do NOT respond with plain text - you MUST call the StructuredOutput tool with your answer formatted according to the schema.`
const log = Log.create({ service: "session.prompt" })
const elog = EffectLogger.create({ service: "session.prompt" })
function isOrphanedInterruptedTool(part: SessionV1.ToolPart) {
// cleanup() marks abandoned tool_use blocks this way after retries/aborts.
// They are not pending work and must not trigger an assistant-prefill request.
@@ -141,7 +136,7 @@ export const layer = Layer.effect(
})
const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) {
yield* elog.info("cancel", { sessionID })
yield* Effect.logInfo("cancel", { "session.id": sessionID })
yield* state.cancel(sessionID)
})
@@ -282,7 +277,7 @@ export const layer = Layer.effect(
const t = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
yield* sessions
.setTitle({ sessionID: input.session.id, title: t })
.pipe(Effect.catchCause((cause) => elog.error("failed to generate title", { error: Cause.squash(cause) })))
.pipe(Effect.catchCause((cause) => Effect.logError("failed to generate title", { error: Cause.squash(cause) })))
})
const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input: {
@@ -384,8 +379,11 @@ export const layer = Layer.effect(
Effect.catchCause((cause) => {
const defect = Cause.squash(cause)
error = defect instanceof Error ? defect : new Error(String(defect))
log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
return Effect.void
return Effect.logError("subtask execution failed", {
error,
agent: task.agent,
description: task.description,
})
}),
Effect.onInterrupt(() =>
Effect.gen(function* () {
@@ -761,7 +759,7 @@ export const layer = Layer.effect(
if (part.type === "file") {
if (part.source?.type === "resource") {
const { clientName, uri } = part.source
log.info("mcp resource", { clientName, uri, mime: part.mime })
yield* Effect.logInfo("mcp resource", { clientName, uri, mime: part.mime })
const pieces: Draft<SessionV1.Part>[] = [
{
messageID: info.id,
@@ -799,7 +797,7 @@ export const layer = Layer.effect(
pieces.push({ ...part, messageID: info.id, sessionID: input.sessionID })
} else {
const error = Cause.squash(exit.cause)
log.error("failed to read MCP resource", { error, clientName, uri })
yield* Effect.logError("failed to read MCP resource", { error, clientName, uri })
const message = error instanceof Error ? error.message : String(error)
pieces.push({
messageID: info.id,
@@ -835,7 +833,7 @@ export const layer = Layer.effect(
}
break
case "file:": {
log.info("file", { mime: part.mime })
yield* Effect.logInfo("file", { mime: part.mime })
const filepath = fileURLToPath(part.url)
const mime = (yield* fsys.isDir(filepath)) ? "application/x-directory" : part.mime
@@ -918,7 +916,7 @@ export const layer = Layer.effect(
}
} else {
const error = Cause.squash(exit.cause)
log.error("failed to read file", { error })
yield* Effect.logError("failed to read file", { error, filepath })
const message = error instanceof Error ? error.message : String(error)
yield* events.publish(Session.Event.Error, {
sessionID: input.sessionID,
@@ -940,7 +938,7 @@ export const layer = Layer.effect(
const exit = yield* execRead(args).pipe(Effect.exit)
if (Exit.isFailure(exit)) {
const error = Cause.squash(exit.cause)
log.error("failed to read directory", { error })
yield* Effect.logError("failed to read directory", { error, filepath })
const message = error instanceof Error ? error.message : String(error)
yield* events.publish(Session.Event.Error, {
sessionID: input.sessionID,
@@ -1065,7 +1063,7 @@ export const layer = Layer.effect(
const parsed = decodeMessageInfo(info, { errors: "all", propertyOrder: "original" })
if (Exit.isFailure(parsed)) {
log.error("invalid user message before save", {
yield* Effect.logError("invalid user message before save", {
sessionID: input.sessionID,
messageID: info.id,
agent: info.agent,
@@ -1073,10 +1071,10 @@ export const layer = Layer.effect(
cause: Cause.pretty(parsed.cause),
})
}
parts.forEach((part, index) => {
for (const [index, part] of parts.entries()) {
const p = decodeMessagePart(part, { errors: "all", propertyOrder: "original" })
if (Exit.isSuccess(p)) return
log.error("invalid user part before save", {
if (Exit.isSuccess(p)) continue
yield* Effect.logError("invalid user part before save", {
sessionID: input.sessionID,
messageID: info.id,
partID: part.id,
@@ -1085,7 +1083,7 @@ export const layer = Layer.effect(
cause: Cause.pretty(p.cause),
part,
})
})
}
yield* sessions.updateMessage(info)
for (const part of parts) yield* sessions.updatePart(part)
@@ -1217,14 +1215,13 @@ export const layer = Layer.effect(
const runLoop: (sessionID: SessionID) => Effect.Effect<SessionV1.WithParts> = Effect.fn("SessionPrompt.run")(
function* (sessionID: SessionID) {
const ctx = yield* InstanceState.context
const slog = elog.with({ sessionID })
let structured: unknown
let step = 0
const session = yield* sessions.get(sessionID).pipe(Effect.orDie)
while (true) {
yield* status.set(sessionID, { type: "busy" })
yield* slog.info("loop", { step })
yield* Effect.logInfo("loop", { "session.id": sessionID, step })
let msgs = yield* MessageV2.filterCompactedEffect(sessionID).pipe(
Effect.provideService(Database.Service, database),
@@ -1255,13 +1252,14 @@ export const layer = Layer.effect(
(part): part is SessionV1.ToolPart => part.type === "tool" && isOrphanedInterruptedTool(part),
)
if (orphan) {
yield* slog.warn("loop exit with orphaned interrupted tool", {
yield* Effect.logWarning("loop exit with orphaned interrupted tool", {
"session.id": sessionID,
messageID: lastAssistant.id,
tool: orphan.tool,
callID: orphan.callID,
})
}
yield* slog.info("exiting loop")
yield* Effect.logInfo("exiting loop", { "session.id": sessionID })
break
}
@@ -1486,7 +1484,11 @@ export const layer = Layer.effect(
})
const command = Effect.fn("SessionPrompt.command")(function* (input: CommandInput) {
yield* elog.info("command", { sessionID: input.sessionID, command: input.command, agent: input.agent })
yield* Effect.logInfo("command", {
"session.id": input.sessionID,
command: input.command,
agent: input.agent,
})
const cmd = yield* commands.get(input.command)
if (!cmd) {
const available = (yield* commands.list()).map((c) => c.name)
+1 -4
View File
@@ -3,15 +3,12 @@ import { SessionV1 } from "@opencode-ai/core/v1/session"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Snapshot } from "../snapshot"
import { Storage } from "@/storage/storage"
import { Log } from "@opencode-ai/core/util/log"
import { Session } from "./session"
import { MessageV2 } from "./message-v2"
import { SessionID, MessageID, PartID } from "./schema"
import { SessionRunState } from "./run-state"
import { SessionSummary } from "./summary"
const log = Log.create({ service: "session.revert" })
export const RevertInput = Schema.Struct({
sessionID: SessionID,
messageID: MessageID,
@@ -90,7 +87,7 @@ export const layer = Layer.effect(
})
const unrevert = Effect.fn("SessionRevert.unrevert")(function* (input: { sessionID: SessionID }) {
log.info("unreverting", input)
yield* Effect.logInfo("unreverting", { sessionID: input.sessionID })
yield* state.assertNotBusy(input.sessionID)
const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
if (!session.revert) return session
+3 -5
View File
@@ -28,7 +28,6 @@ import { or } from "drizzle-orm"
import type { SQL } from "drizzle-orm"
import { PartTable, SessionTable } from "@opencode-ai/core/session/sql"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { Log } from "@opencode-ai/core/util/log"
import { MessageV2 } from "./message-v2"
import type { InstanceContext } from "../project/instance-context"
import { InstanceState } from "@/effect/instance-state"
@@ -46,7 +45,6 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
const log = Log.create({ service: "session" })
const runtime = makeRuntime(Database.Service, Database.defaultLayer)
const parentTitlePrefix = "New session - "
@@ -573,7 +571,7 @@ export const layer: Layer.Layer<
updated: Date.now(),
},
}
log.info("created", result)
yield* Effect.logInfo("created", result)
yield* events.publish(SessionV1.Event.Created, { sessionID: result.id, info: result })
@@ -664,8 +662,8 @@ export const layer: Layer.Layer<
yield* events.publish(SessionV1.Event.Deleted, { sessionID, info: session })
yield* events.remove(sessionID)
} catch (e) {
log.error(e)
} catch (error) {
yield* Effect.logError("failed to remove session", { sessionID, error })
}
})
-4
View File
@@ -17,13 +17,10 @@ import { MessageV2 } from "./message-v2"
import { Session } from "./session"
import { SessionProcessor } from "./processor"
import { PartID } from "./schema"
import { Log } from "@opencode-ai/core/util/log"
import { EffectBridge } from "@/effect/bridge"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
const log = Log.create({ service: "session.tools" })
export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
agent: Agent.Info
model: Provider.Model
@@ -33,7 +30,6 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
messages: SessionV1.WithParts[]
promptOps: TaskPromptOps
}) {
using _ = log.time("resolveTools")
const tools: Record<string, AITool> = {}
const run = yield* EffectBridge.make()
const plugin = yield* Plugin.Service