Skip to content
Warlock.js v4

Log AI calls

@warlock.js/ai does not own a logger contract. Every primitive imports the log singleton from @warlock.js/logger directly and emits structured entries through it. Configuration — channels, levels, redaction — lives entirely on the logger.

No ai.config({ logger }). No per-primitive logger: override. Configure once at app boot; the framework picks it up.

import { log, ConsoleLog, FileLog } from "@warlock.js/logger";
log.configure({
channels: [
new ConsoleLog(),
new FileLog({ chunk: "daily" }),
],
autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
});
log.setMinLevel("info");

That’s it. Every agent / workflow / supervisor running in the process emits to the configured channels.

Every framework log call uses the 4-arg positional form:

log.info("ai.agent", "trip.started", "agent starting trip", { tripIndex, model });
  • module — emitting primitive ("ai.agent", "ai.openai", "ai.workflow.<name>", "ai.supervisor.<name>").
  • action — mirrors event names without the primitive prefix ("trip.started", "tool.called").
  • message — human-readable summary.
  • context — structured bag of diagnostic fields.

action strips the prefix of the corresponding event (agent.trip.startedtrip.started) so grep filters and event handlers share vocabulary.

LevelFramework usage
debugInternals (request/response bodies, token counts per trip)
infoMilestones (agent starting, agent completed)
warnRetries, repair attempts, recoverable tool failures
errorTerminal failures surfaced via result.error
successTool-call success

Streaming token deltas are intentionally NOT logged at token granularity — trip boundaries carry the same information at readable volume.

ActionLevelContext
agent.startinginfoinput, modelName, maxTrips
trip.starteddebugtripIndex
tool.callingdebugtool name, action, tripIndex
tool.calledsuccesstool name, duration, tripIndex
tool.failedwarntool name, error code, tripIndex
repair.attemptingwarntripIndex, validation issues
agent.completedinfototalUsage, totalDuration, trip count
agent.errorerrorerror code, message, stack

workflow.starting / step.starting / step.completed / step.failed / workflow.completed / workflow.error. Module is ai.workflow.<name>.

supervisor.starting / iteration.starting / router.deciding / router.decided / agent.starting (per dispatched agent) / iteration.completed / evaluate.verdict / supervisor.completed. Module is ai.supervisor.<name>.

ai.openai (and future adapters) emit request (debug) and response (debug) per call, plus error on the wrapped AIError.

Redaction is a @warlock.js/logger feature — configure once, applies to every framework log automatically.

log.configure({
redact: {
paths: [
"context.messages", // prompts
"context.input", // user input
"context.apiKey", // defense-in-depth
],
},
});

Sensitive data — prompts, customer messages, API keys — should be redacted at the channel level. Don’t try to do it at the agent level; the logger applies redaction once for every log call regardless of source.

Events vs logs — two channels, one source

Section titled “Events vs logs — two channels, one source”
  • Events are push-model (subscribers), typed payloads, per-execution lifetime — ideal for UI streaming, SSE, metrics.
  • Logs are pull-model (written to channels), structured-string + context, persistent — ideal for grep, post-mortem.

Both fire from the same internal emit, so every event produces both a typed event payload and a structured log entry.

import { log } from "@warlock.js/logger";
beforeAll(() => log.setChannels([]));

Capture all framework log entries in a test

Section titled “Capture all framework log entries in a test”
import { log, LogChannel } from "@warlock.js/logger";
class Capture extends LogChannel {
public name = "capture";
public entries: any[] = [];
public log(data) {
this.entries.push(data);
}
}
const capture = new Capture();
log.setChannels([capture]);
  • Handle errors — what lands on the error channel.
  • @warlock.js/logger — the logger package itself.