import type { ProviderDriverKind, ThreadId } from "@t3tools/contracts"; import { causeErrorTag, errorTag } from "@t3tools/shared/observability"; import * as Cause from "effect/Cause"; import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import type * as EffectAcpProtocol from "effect-acp/protocol"; import type { EventNdjsonLogger } from "../Layers/EventNdjsonLogger.ts"; import type * as AcpSessionRuntime from "./AcpSessionRuntime.ts"; const transientProtocolUpdates = new Set(["agent_message_chunk", "agent_thought_chunk"]); function structuralMethod(value: string): string { return value.length <= 128 && /^[A-Za-z][A-Za-z0-9._:/-]*$/.test(value) ? value : "unknown"; } function summarizePayload(payload: unknown): Readonly> { if (payload === null) return { valueType: "null" }; if (typeof payload === "string") { return { valueType: "string", byteLength: new TextEncoder().encode(payload).byteLength }; } if (payload instanceof Uint8Array) { return { valueType: "bytes", byteLength: payload.byteLength }; } if (Array.isArray(payload)) { return { valueType: "array", itemCount: payload.length }; } if (typeof payload !== "object") { return { valueType: typeof payload }; } try { const record = payload as Record; return { valueType: "object", fieldCount: Object.keys(record).length, ...(typeof record._tag === "string" ? { messageTag: errorTag(record) } : {}), ...(typeof record.tag === "string" ? { method: structuralMethod(record.tag) } : {}), }; } catch { return { valueType: "object" }; } } function formatRequestLogPayload(event: AcpSessionRuntime.AcpSessionRequestLogEvent) { return { method: structuralMethod(event.method), status: event.status, request: summarizePayload(event.payload), ...(event.result !== undefined ? { result: summarizePayload(event.result) } : {}), ...(event.cause !== undefined ? { errorTag: causeErrorTag(event.cause), reasonCount: event.cause.reasons.length, } : {}), }; } function formatProtocolLogPayload(event: EffectAcpProtocol.AcpProtocolLogEvent) { return { direction: event.direction, stage: event.stage, payload: summarizePayload(event.payload), }; } function isTransientProtocolMessage(message: unknown): boolean { if (typeof message !== "object" || message === null) return false; const method = Reflect.get(message, "tag") ?? Reflect.get(message, "method"); if (method !== "session/update") return false; const payload = Reflect.get(message, "payload") ?? Reflect.get(message, "params"); if (typeof payload !== "object" || payload === null) return false; const update = Reflect.get(payload, "update"); if (typeof update !== "object" || update === null) return false; const updateType = Reflect.get(update, "sessionUpdate"); return typeof updateType === "string" && transientProtocolUpdates.has(updateType); } function rawChunkContainsOnlyTransientMessages(payload: string): boolean { const lines = payload.split("\n"); const remainder = lines.pop() ?? ""; if (remainder.trim().length > 0) return false; const messages: Array = []; for (const line of lines) { if (line.trim().length === 0) continue; try { messages.push(JSON.parse(line)); } catch { return false; } } return messages.length > 0 && messages.every(isTransientProtocolMessage); } function filterTransientProtocolLog( event: EffectAcpProtocol.AcpProtocolLogEvent, ): EffectAcpProtocol.AcpProtocolLogEvent | undefined { if (event.direction !== "incoming") return event; if (event.stage === "raw" && typeof event.payload === "string") { return rawChunkContainsOnlyTransientMessages(event.payload) ? undefined : event; } if (event.stage !== "decoded") return event; if (!Array.isArray(event.payload)) { return isTransientProtocolMessage(event.payload) ? undefined : event; } const payload = event.payload.filter((message) => !isTransientProtocolMessage(message)); return payload.length === 0 ? undefined : { ...event, payload }; } export const makeAcpNativeLoggerFactory = Effect.fn("makeAcpNativeLoggerFactory")(function* () { const crypto = yield* Crypto.Crypto; return (input: { readonly nativeEventLogger: EventNdjsonLogger | undefined; readonly provider: ProviderDriverKind; readonly threadId: ThreadId; readonly verboseProtocolLogging?: boolean; }): Pick => { const writeNativeAcpLog = (logInput: { readonly kind: "request" | "protocol"; readonly payload: unknown; }) => Effect.gen(function* () { if (!input.nativeEventLogger) return; const observedAt = DateTime.formatIso(yield* DateTime.now); yield* input.nativeEventLogger.write( { observedAt, event: { id: yield* crypto.randomUUIDv4, kind: logInput.kind, provider: input.provider, createdAt: observedAt, threadId: input.threadId, payload: logInput.payload, }, }, input.threadId, ); }).pipe( Effect.catchCause((cause) => Cause.hasInterrupts(cause) ? Effect.interrupt : Effect.logWarning("Failed to write native ACP event log.", { errorTag: causeErrorTag(cause), reasonCount: cause.reasons.length, provider: input.provider, threadId: input.threadId, }), ), ); return { requestLogger: (event: AcpSessionRuntime.AcpSessionRequestLogEvent) => writeNativeAcpLog({ kind: "request", payload: formatRequestLogPayload(event), }), ...(input.nativeEventLogger && input.verboseProtocolLogging ? { protocolLogging: { logIncoming: true, logOutgoing: true, logger: (event: EffectAcpProtocol.AcpProtocolLogEvent) => { const filtered = filterTransientProtocolLog(event); return filtered ? writeNativeAcpLog({ kind: "protocol", payload: formatProtocolLogPayload(filtered), }) : Effect.void; }, } satisfies NonNullable, } : {}), }; }; });