From 4d94c530406567791dbe4ab06c838a83c2e26023 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Fri, 5 Jun 2026 14:29:06 +0900 Subject: refactor(observability): pure-types contracts/logging + Span body channel; verbatim before/after -> LogRecord.body (273 tests) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit contracts/logging.ts reduced to pure types; createLogger (+ helpers) moved to kernel/src/logging/ — @dispatch/kernel still exports it (host-bin/tool-read-file unaffected). Span body channel (Option A): Logger.span / Span.child / Span.end accept an optional body string -> SpanOpenRecord.body / SpanCloseRecord.body. Large verbatim payloads now use body, not stringified attributes (store-fat-serve-thin; attributes stay thin/queryable for D9). before: run-turn emits a 'prompt' span with the verbatim messages+tools in body (small scalars in attrs). after: provider.request span carries the verbatim request in body; attrs thin, auth self-redacted. Verified: tsc -b clean, 273 tests, biome 0 warnings/0 infos. Live boot: prompt + provider.request bodies present and correlated (shared turnId); request.body no longer in attributes; auth-key leak count = 0. --- packages/kernel/src/host/host.test.ts | 113 ++++++++++++++++++++++++++++++++++ packages/kernel/src/host/host.ts | 2 +- 2 files changed, 114 insertions(+), 1 deletion(-) (limited to 'packages/kernel/src/host') diff --git a/packages/kernel/src/host/host.test.ts b/packages/kernel/src/host/host.test.ts index 7688366..430447c 100644 --- a/packages/kernel/src/host/host.test.ts +++ b/packages/kernel/src/host/host.test.ts @@ -956,5 +956,118 @@ describe("createHost", () => { expect(spanCloses[0].attributes?.result).toBe("ok"); } }); + + it("span() with body emits body on span-open record", async () => { + let extLogger: Logger | undefined; + + const ext = createExtension("ext", { + activate: (host) => { + extLogger = host.logger; + }, + }); + + const host = createHost([ext], deps); + await host.activate(); + + const span = extLogger?.span("with-body", { key: "value" }, '{"payload":"hello"}'); + span?.end(); + + const spanOpens = logSink.records.filter((r) => r.kind === "span-open"); + expect(spanOpens).toHaveLength(1); + if (spanOpens[0]?.kind === "span-open") { + expect(spanOpens[0].body).toBe('{"payload":"hello"}'); + } + }); + + it("span() without body omits body field on span-open record", async () => { + let extLogger: Logger | undefined; + + const ext = createExtension("ext", { + activate: (host) => { + extLogger = host.logger; + }, + }); + + const host = createHost([ext], deps); + await host.activate(); + + const span = extLogger?.span("no-body"); + span?.end(); + + const spanOpens = logSink.records.filter((r) => r.kind === "span-open"); + expect(spanOpens).toHaveLength(1); + if (spanOpens[0]?.kind === "span-open") { + expect(spanOpens[0].body).toBeUndefined(); + } + }); + + it("child() with body emits body on child span-open record", async () => { + let extLogger: Logger | undefined; + + const ext = createExtension("ext", { + activate: (host) => { + extLogger = host.logger; + }, + }); + + const host = createHost([ext], deps); + await host.activate(); + + const span = extLogger?.span("parent"); + const child = span?.child("child-name", { k: "v" }, '{"child":"body"}'); + child?.end(); + span?.end(); + + const spanOpens = logSink.records.filter((r) => r.kind === "span-open"); + const childOpen = spanOpens.find((r) => r.kind === "span-open" && r.name === "child-name"); + expect(childOpen).toBeDefined(); + if (childOpen?.kind === "span-open") { + expect(childOpen.body).toBe('{"child":"body"}'); + } + }); + + it("end() with body emits body on span-close record", async () => { + let extLogger: Logger | undefined; + + const ext = createExtension("ext", { + activate: (host) => { + extLogger = host.logger; + }, + }); + + const host = createHost([ext], deps); + await host.activate(); + + const span = extLogger?.span("close-body"); + span?.end({ body: '{"result":"data"}' }); + + const spanCloses = logSink.records.filter((r) => r.kind === "span-close"); + expect(spanCloses).toHaveLength(1); + if (spanCloses[0]?.kind === "span-close") { + expect(spanCloses[0].body).toBe('{"result":"data"}'); + } + }); + + it("end() without body omits body field on span-close record", async () => { + let extLogger: Logger | undefined; + + const ext = createExtension("ext", { + activate: (host) => { + extLogger = host.logger; + }, + }); + + const host = createHost([ext], deps); + await host.activate(); + + const span = extLogger?.span("no-close-body"); + span?.end(); + + const spanCloses = logSink.records.filter((r) => r.kind === "span-close"); + expect(spanCloses).toHaveLength(1); + if (spanCloses[0]?.kind === "span-close") { + expect(spanCloses[0].body).toBeUndefined(); + } + }); }); }); diff --git a/packages/kernel/src/host/host.ts b/packages/kernel/src/host/host.ts index c7ec7a9..2331625 100644 --- a/packages/kernel/src/host/host.ts +++ b/packages/kernel/src/host/host.ts @@ -19,9 +19,9 @@ import type { ServiceHandle, } from "../contracts/hooks.js"; import type { LogDeps, Logger, LogSink } from "../contracts/logging.js"; -import { createLogger } from "../contracts/logging.js"; import type { ProviderContract } from "../contracts/provider.js"; import type { ToolContract } from "../contracts/tool.js"; +import { createLogger } from "../logging/logger.js"; import { resolveActivationOrder } from "./dag.js"; import { isApiVersionCompatible } from "./version.js"; -- cgit v1.2.3