fix: wire anthropic payload log diagnostics (#1501) (thanks @parubets)
This commit is contained in:
parent
07bc85b7fb
commit
e85abaca2b
@ -12,6 +12,7 @@ Docs: https://docs.clawd.bot
|
||||
- CLI: add live auth probes to `clawdbot models status` for per-profile verification.
|
||||
- CLI: add `clawdbot system` for system events + heartbeat controls; remove standalone `wake`.
|
||||
- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3.
|
||||
- Agents: add diagnostics-configured Anthropic payload logging. (#1501) Thanks @parubets.
|
||||
- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc.
|
||||
- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc.
|
||||
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
|
||||
|
||||
@ -95,6 +95,29 @@ Console logs are **TTY-aware** and formatted for readability:
|
||||
|
||||
Console formatting is controlled by `logging.consoleStyle`.
|
||||
|
||||
## Anthropic payload log (debugging)
|
||||
|
||||
For Anthropic-only runs, you can enable a dedicated JSONL log that captures the
|
||||
exact request payload (as sent) plus per-run usage stats. This log includes full
|
||||
prompt/message data; treat it as sensitive.
|
||||
|
||||
```json
|
||||
{
|
||||
"diagnostics": {
|
||||
"anthropicPayloadLog": {
|
||||
"enabled": true,
|
||||
"filePath": "/path/to/anthropic-payload.jsonl"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Defaults + overrides:
|
||||
|
||||
- Default path: `$CLAWDBOT_STATE_DIR/logs/anthropic-payload.jsonl`
|
||||
- Env enable: `CLAWDBOT_ANTHROPIC_PAYLOAD_LOG=1`
|
||||
- Env path: `CLAWDBOT_ANTHROPIC_PAYLOAD_LOG_FILE=/path/to/anthropic-payload.jsonl`
|
||||
|
||||
## Configuring logging
|
||||
|
||||
All logging configuration lives under `logging` in `~/.clawdbot/clawdbot.json`.
|
||||
|
||||
145
src/agents/anthropic-payload-log.test.ts
Normal file
145
src/agents/anthropic-payload-log.test.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { createAnthropicPayloadLogger } from "./anthropic-payload-log.js";
|
||||
|
||||
describe("createAnthropicPayloadLogger", () => {
|
||||
it("returns null when diagnostics payload logging is disabled", () => {
|
||||
const logger = createAnthropicPayloadLogger({
|
||||
cfg: {} as ClawdbotConfig,
|
||||
env: {},
|
||||
modelApi: "anthropic-messages",
|
||||
});
|
||||
|
||||
expect(logger).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when model api is not anthropic", () => {
|
||||
const logger = createAnthropicPayloadLogger({
|
||||
cfg: {
|
||||
diagnostics: {
|
||||
anthropicPayloadLog: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
modelApi: "openai",
|
||||
writer: {
|
||||
filePath: "memory",
|
||||
write: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(logger).toBeNull();
|
||||
});
|
||||
|
||||
it("honors diagnostics config and expands file paths", () => {
|
||||
const lines: string[] = [];
|
||||
const logger = createAnthropicPayloadLogger({
|
||||
cfg: {
|
||||
diagnostics: {
|
||||
anthropicPayloadLog: {
|
||||
enabled: true,
|
||||
filePath: "~/.clawdbot/logs/anthropic-payload.jsonl",
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
modelApi: "anthropic-messages",
|
||||
writer: {
|
||||
filePath: "memory",
|
||||
write: (line) => lines.push(line),
|
||||
},
|
||||
});
|
||||
|
||||
expect(logger).not.toBeNull();
|
||||
expect(logger?.filePath).toBe(resolveUserPath("~/.clawdbot/logs/anthropic-payload.jsonl"));
|
||||
|
||||
logger?.recordUsage([
|
||||
{
|
||||
role: "assistant",
|
||||
usage: {
|
||||
input: 12,
|
||||
},
|
||||
} as unknown as {
|
||||
role: string;
|
||||
usage: { input: number };
|
||||
},
|
||||
]);
|
||||
|
||||
const event = JSON.parse(lines[0]?.trim() ?? "{}") as Record<string, unknown>;
|
||||
expect(event.stage).toBe("usage");
|
||||
expect(event.usage).toEqual({ input: 12 });
|
||||
});
|
||||
|
||||
it("records request payloads and forwards onPayload", async () => {
|
||||
const lines: string[] = [];
|
||||
let forwarded: unknown;
|
||||
const logger = createAnthropicPayloadLogger({
|
||||
cfg: {
|
||||
diagnostics: {
|
||||
anthropicPayloadLog: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
modelApi: "anthropic-messages",
|
||||
writer: {
|
||||
filePath: "memory",
|
||||
write: (line) => lines.push(line),
|
||||
},
|
||||
});
|
||||
|
||||
const streamFn = ((_, __, options) => {
|
||||
options?.onPayload?.({ hello: "world" });
|
||||
return Promise.resolve(undefined);
|
||||
}) as StreamFn;
|
||||
|
||||
const wrapped = logger?.wrapStreamFn(streamFn);
|
||||
await wrapped?.(
|
||||
{ api: "anthropic-messages" } as unknown as { api: string },
|
||||
{},
|
||||
{
|
||||
onPayload: (payload) => {
|
||||
forwarded = payload;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const event = JSON.parse(lines[0]?.trim() ?? "{}") as Record<string, unknown>;
|
||||
expect(event.stage).toBe("request");
|
||||
expect(event.payload).toEqual({ hello: "world" });
|
||||
expect(event.payloadDigest).toBeTruthy();
|
||||
expect(forwarded).toEqual({ hello: "world" });
|
||||
});
|
||||
|
||||
it("records errors when usage is missing", () => {
|
||||
const lines: string[] = [];
|
||||
const logger = createAnthropicPayloadLogger({
|
||||
cfg: {
|
||||
diagnostics: {
|
||||
anthropicPayloadLog: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
modelApi: "anthropic-messages",
|
||||
writer: {
|
||||
filePath: "memory",
|
||||
write: (line) => lines.push(line),
|
||||
},
|
||||
});
|
||||
|
||||
logger?.recordUsage([], new Error("boom"));
|
||||
|
||||
const event = JSON.parse(lines[0]?.trim() ?? "{}") as Record<string, unknown>;
|
||||
expect(event.stage).toBe("usage");
|
||||
expect(event.error).toContain("boom");
|
||||
});
|
||||
});
|
||||
@ -5,7 +5,9 @@ import path from "node:path";
|
||||
import type { AgentMessage, StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { parseBooleanValue } from "../utils/boolean.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
@ -33,6 +35,11 @@ type PayloadLogConfig = {
|
||||
filePath: string;
|
||||
};
|
||||
|
||||
type PayloadLogInit = {
|
||||
cfg?: ClawdbotConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
type PayloadLogWriter = {
|
||||
filePath: string;
|
||||
write: (line: string) => void;
|
||||
@ -41,9 +48,12 @@ type PayloadLogWriter = {
|
||||
const writers = new Map<string, PayloadLogWriter>();
|
||||
const log = createSubsystemLogger("agent/anthropic-payload");
|
||||
|
||||
function resolvePayloadLogConfig(env: NodeJS.ProcessEnv): PayloadLogConfig {
|
||||
const enabled = parseBooleanValue(env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG) ?? false;
|
||||
const fileOverride = env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG_FILE?.trim();
|
||||
function resolvePayloadLogConfig(params: PayloadLogInit): PayloadLogConfig {
|
||||
const env = params.env ?? process.env;
|
||||
const config = params.cfg?.diagnostics?.anthropicPayloadLog;
|
||||
const envEnabled = parseBooleanValue(env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG);
|
||||
const enabled = envEnabled ?? config?.enabled ?? false;
|
||||
const fileOverride = config?.filePath?.trim() || env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG_FILE?.trim();
|
||||
const filePath = fileOverride
|
||||
? resolveUserPath(fileOverride)
|
||||
: path.join(resolveStateDir(env), "logs", "anthropic-payload.jsonl");
|
||||
@ -112,11 +122,13 @@ function findLastAssistantUsage(messages: AgentMessage[]): Record<string, unknow
|
||||
|
||||
export type AnthropicPayloadLogger = {
|
||||
enabled: true;
|
||||
filePath: string;
|
||||
wrapStreamFn: (streamFn: StreamFn) => StreamFn;
|
||||
recordUsage: (messages: AgentMessage[], error?: unknown) => void;
|
||||
};
|
||||
|
||||
export function createAnthropicPayloadLogger(params: {
|
||||
cfg?: ClawdbotConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
runId?: string;
|
||||
sessionId?: string;
|
||||
@ -125,12 +137,14 @@ export function createAnthropicPayloadLogger(params: {
|
||||
modelId?: string;
|
||||
modelApi?: string | null;
|
||||
workspaceDir?: string;
|
||||
writer?: PayloadLogWriter;
|
||||
}): AnthropicPayloadLogger | null {
|
||||
const env = params.env ?? process.env;
|
||||
const cfg = resolvePayloadLogConfig(env);
|
||||
const cfg = resolvePayloadLogConfig({ env, cfg: params.cfg });
|
||||
if (!cfg.enabled) return null;
|
||||
if (params.modelApi !== "anthropic-messages") return null;
|
||||
|
||||
const writer = getWriter(cfg.filePath);
|
||||
const writer = params.writer ?? getWriter(cfg.filePath);
|
||||
const base: Omit<PayloadLogEvent, "ts" | "stage"> = {
|
||||
runId: params.runId,
|
||||
sessionId: params.sessionId,
|
||||
@ -163,7 +177,7 @@ export function createAnthropicPayloadLogger(params: {
|
||||
options?.onPayload?.(payload);
|
||||
};
|
||||
return streamFn(model, context, {
|
||||
...(options ?? {}),
|
||||
...options,
|
||||
onPayload: nextOnPayload,
|
||||
});
|
||||
};
|
||||
@ -178,7 +192,7 @@ export function createAnthropicPayloadLogger(params: {
|
||||
...base,
|
||||
ts: new Date().toISOString(),
|
||||
stage: "usage",
|
||||
error: String(error),
|
||||
error: formatErrorMessage(error),
|
||||
});
|
||||
}
|
||||
return;
|
||||
@ -188,7 +202,7 @@ export function createAnthropicPayloadLogger(params: {
|
||||
ts: new Date().toISOString(),
|
||||
stage: "usage",
|
||||
usage,
|
||||
error: error ? String(error) : undefined,
|
||||
error: error ? formatErrorMessage(error) : undefined,
|
||||
});
|
||||
log.info("anthropic usage", {
|
||||
runId: params.runId,
|
||||
@ -197,6 +211,6 @@ export function createAnthropicPayloadLogger(params: {
|
||||
});
|
||||
};
|
||||
|
||||
log.info("anthropic payload logger enabled", { filePath: writer.filePath });
|
||||
return { enabled: true, wrapStreamFn, recordUsage };
|
||||
log.info("anthropic payload logger enabled", { filePath: cfg.filePath });
|
||||
return { enabled: true, filePath: cfg.filePath, wrapStreamFn, recordUsage };
|
||||
}
|
||||
|
||||
@ -460,6 +460,7 @@ export async function runEmbeddedAttempt(
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
const anthropicPayloadLogger = createAnthropicPayloadLogger({
|
||||
cfg: params.config,
|
||||
env: process.env,
|
||||
runId: params.runId,
|
||||
sessionId: activeSession.sessionId,
|
||||
|
||||
@ -121,6 +121,8 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"diagnostics.cacheTrace.includeMessages": "Cache Trace Include Messages",
|
||||
"diagnostics.cacheTrace.includePrompt": "Cache Trace Include Prompt",
|
||||
"diagnostics.cacheTrace.includeSystem": "Cache Trace Include System",
|
||||
"diagnostics.anthropicPayloadLog.enabled": "Anthropic Payload Log Enabled",
|
||||
"diagnostics.anthropicPayloadLog.filePath": "Anthropic Payload Log File Path",
|
||||
"agents.list.*.identity.avatar": "Identity Avatar",
|
||||
"gateway.remote.url": "Remote Gateway URL",
|
||||
"gateway.remote.sshTarget": "Remote Gateway SSH Target",
|
||||
@ -390,6 +392,10 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"Include full message payloads in trace output (default: true).",
|
||||
"diagnostics.cacheTrace.includePrompt": "Include prompt text in trace output (default: true).",
|
||||
"diagnostics.cacheTrace.includeSystem": "Include system prompt in trace output (default: true).",
|
||||
"diagnostics.anthropicPayloadLog.enabled":
|
||||
"Log Anthropic request payloads + usage for embedded runs (default: false).",
|
||||
"diagnostics.anthropicPayloadLog.filePath":
|
||||
"JSONL output path for Anthropic payload logs (default: $CLAWDBOT_STATE_DIR/logs/anthropic-payload.jsonl).",
|
||||
"tools.exec.applyPatch.enabled":
|
||||
"Experimental. Enables apply_patch for OpenAI models when allowed by tool policy.",
|
||||
"tools.exec.applyPatch.allowModels":
|
||||
|
||||
@ -133,10 +133,16 @@ export type DiagnosticsCacheTraceConfig = {
|
||||
includeSystem?: boolean;
|
||||
};
|
||||
|
||||
export type DiagnosticsAnthropicPayloadLogConfig = {
|
||||
enabled?: boolean;
|
||||
filePath?: string;
|
||||
};
|
||||
|
||||
export type DiagnosticsConfig = {
|
||||
enabled?: boolean;
|
||||
otel?: DiagnosticsOtelConfig;
|
||||
cacheTrace?: DiagnosticsCacheTraceConfig;
|
||||
anthropicPayloadLog?: DiagnosticsAnthropicPayloadLogConfig;
|
||||
};
|
||||
|
||||
export type WebReconnectConfig = {
|
||||
|
||||
@ -86,6 +86,13 @@ export const ClawdbotSchema = z
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
anthropicPayloadLog: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
filePath: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user