feat: add raw stream logging flags

This commit is contained in:
Peter Steinberger
2026-01-09 03:45:14 +00:00
parent 69546d563d
commit 8e27ea7371
8 changed files with 149 additions and 8 deletions

View File

@@ -2,6 +2,7 @@
## Unreleased
- Debugging: add raw model stream logging flags and document gateway watch mode.
- CLI: improve `logs` output (pretty/plain/JSONL), add gateway unreachable hint, and document logging.
- WhatsApp: route queued replies to the original sender instead of the bot's own number. (#534) — thanks @mcinteerj
- Models: add OAuth expiry checks in doctor, expanded `models status` auth output (missing auth + `--check` exit codes). (#538) — thanks @latitudeki5223

86
docs/debugging.md Normal file
View File

@@ -0,0 +1,86 @@
---
summary: "Debugging tools: watch mode, raw model streams, and tracing reasoning leakage"
read_when:
- You need to inspect raw model output for reasoning leakage
- You want to run the Gateway in watch mode while iterating
- You need a repeatable debugging workflow
---
# Debugging
This page covers debugging helpers for streaming output, especially when a
provider mixes reasoning into normal text.
## Gateway watch mode
For fast iteration, run the gateway under the file watcher:
```bash
pnpm gateway:watch --force
```
This maps to:
```bash
tsx watch src/entry.ts gateway --force
```
Add any gateway CLI flags after `gateway:watch` and they will be passed through
on each restart.
## Raw stream logging (Clawdbot)
Clawdbot can log the **raw assistant stream** before any filtering/formatting.
This is the best way to see whether reasoning is arriving as plain text deltas
(or as separate thinking blocks).
Enable it via CLI:
```bash
pnpm gateway:watch --force --raw-stream
```
Optional path override:
```bash
pnpm gateway:watch --force --raw-stream --raw-stream-path ~/.clawdbot/logs/raw-stream.jsonl
```
Equivalent env vars:
```bash
CLAWDBOT_RAW_STREAM=1
CLAWDBOT_RAW_STREAM_PATH=~/.clawdbot/logs/raw-stream.jsonl
```
Default file:
`~/.clawdbot/logs/raw-stream.jsonl`
## Raw chunk logging (pi-mono)
To capture **raw OpenAI-compat chunks** before they are parsed into blocks,
pi-mono exposes a separate logger:
```bash
PI_RAW_STREAM=1
```
Optional path:
```bash
PI_RAW_STREAM_PATH=~/.pi-mono/logs/raw-openai-completions.jsonl
```
Default file:
`~/.pi-mono/logs/raw-openai-completions.jsonl`
> Note: this is only emitted by processes using pi-monos
> `openai-completions` provider.
## Safety notes
- Raw stream logs can include full prompts, tool output, and user data.
- Keep logs local and delete them after debugging.
- If you share logs, scrub secrets and PII first.

View File

@@ -1,8 +1,11 @@
import fs from "node:fs";
import path from "node:path";
import type { AgentEvent, AgentMessage } from "@mariozechner/pi-agent-core";
import type { AssistantMessage } from "@mariozechner/pi-ai";
import type { AgentSession } from "@mariozechner/pi-coding-agent";
import type { ReasoningLevel } from "../auto-reply/thinking.js";
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
import { resolveStateDir } from "../config/paths.js";
import { emitAgentEvent } from "../infra/agent-events.js";
import { createSubsystemLogger } from "../logging.js";
import { splitMediaFromOutput } from "../media/parse.js";
@@ -23,6 +26,31 @@ const THINKING_OPEN_GLOBAL_RE = /<\s*think(?:ing)?\s*>/gi;
const THINKING_CLOSE_GLOBAL_RE = /<\s*\/\s*think(?:ing)?\s*>/gi;
const TOOL_RESULT_MAX_CHARS = 8000;
const log = createSubsystemLogger("agent/embedded");
const RAW_STREAM_ENABLED = process.env.CLAWDBOT_RAW_STREAM === "1";
const RAW_STREAM_PATH =
process.env.CLAWDBOT_RAW_STREAM_PATH?.trim() ||
path.join(resolveStateDir(), "logs", "raw-stream.jsonl");
let rawStreamReady = false;
const appendRawStream = (payload: Record<string, unknown>) => {
if (!RAW_STREAM_ENABLED) return;
if (!rawStreamReady) {
rawStreamReady = true;
try {
fs.mkdirSync(path.dirname(RAW_STREAM_PATH), { recursive: true });
} catch {
// ignore raw stream mkdir failures
}
}
try {
void fs.promises.appendFile(
RAW_STREAM_PATH,
`${JSON.stringify(payload)}\n`,
);
} catch {
// ignore raw stream write failures
}
};
export type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";
@@ -664,6 +692,15 @@ export function subscribeEmbeddedPiSession(params: {
typeof assistantRecord?.content === "string"
? assistantRecord.content
: "";
appendRawStream({
ts: Date.now(),
event: "assistant_text_stream",
runId: params.runId,
sessionId: (params.session as { id?: string }).id,
evtType,
delta,
content,
});
let chunk = "";
if (evtType === "text_delta") {
chunk = delta;
@@ -756,6 +793,14 @@ export function subscribeEmbeddedPiSession(params: {
if (msg?.role === "assistant") {
const assistantMessage = msg as AssistantMessage;
const rawText = extractAssistantText(assistantMessage);
appendRawStream({
ts: Date.now(),
event: "assistant_message_end",
runId: params.runId,
sessionId: (params.session as { id?: string }).id,
rawText,
rawThinking: extractAssistantThinking(assistantMessage),
});
const cleaned = params.enforceFinalTag
? stripThinkingSegments(stripUnpairedThinkingTags(rawText))
: stripThinkingSegments(rawText);

View File

@@ -599,7 +599,7 @@ describe("directive behavior", () => {
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("Elevated mode disabled.");
expect(text).toContain("status agent:main:main");
expect(text).toContain("Session: agent:main:main");
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
});
});

View File

@@ -555,7 +555,6 @@ export async function handleCommands(params: {
const reply = await buildStatusReply({
cfg,
command,
provider: command.provider,
sessionEntry,
sessionKey,
sessionScope,

View File

@@ -65,7 +65,6 @@ describe("buildStatusMessage", () => {
},
},
},
},
} as ClawdbotConfig,
agent: {
model: "anthropic/pi:opus",
@@ -248,7 +247,6 @@ describe("buildStatusMessage", () => {
},
},
},
},
} as ClawdbotConfig,
agent: { model: "anthropic/claude-opus-4-5" },
sessionEntry: { sessionId: "c1", updatedAt: 0, inputTokens: 10 },

View File

@@ -296,7 +296,10 @@ export function buildStatusMessage(args: StatusArgs): string {
const activationLine = activationParts.filter(Boolean).join(" · ");
const authMode = resolveModelAuthMode(provider, args.config);
const showCost = authMode === "api-key";
const authLabelValue =
args.modelAuth ??
(authMode && authMode !== "unknown" ? authMode : undefined);
const showCost = authLabelValue === "api-key" || authLabelValue === "mixed";
const costConfig = showCost
? resolveModelCostConfig({
provider,
@@ -319,9 +322,6 @@ export function buildStatusMessage(args: StatusArgs): string {
const costLabel = showCost && hasUsage ? formatUsd(cost) : undefined;
const modelLabel = model ? `${provider}/${model}` : "unknown";
const authLabelValue =
args.modelAuth ??
(authMode && authMode !== "unknown" ? authMode : undefined);
const authLabel = authLabelValue ? ` · 🔑 ${authLabelValue}` : "";
const modelLine = `🧠 Model: ${modelLabel}${authLabel}`;
const commit = resolveCommitHash();

View File

@@ -50,6 +50,8 @@ type GatewayRunOpts = {
verbose?: boolean;
wsLog?: unknown;
compact?: boolean;
rawStream?: boolean;
rawStreamPath?: unknown;
};
type GatewayRunParams = {
@@ -300,6 +302,14 @@ async function runGatewayCommand(
}
setGatewayWsLogStyle(wsLogStyle);
if (opts.rawStream) {
process.env.CLAWDBOT_RAW_STREAM = "1";
}
const rawStreamPath = toOptionString(opts.rawStreamPath);
if (rawStreamPath) {
process.env.CLAWDBOT_RAW_STREAM_PATH = rawStreamPath;
}
const cfg = loadConfig();
const portOverride = parsePort(opts.port);
if (opts.port !== undefined && portOverride === null) {
@@ -565,6 +575,8 @@ function addGatewayRunCommand(
"auto",
)
.option("--compact", 'Alias for "--ws-log compact"', false)
.option("--raw-stream", "Log raw model stream events to jsonl", false)
.option("--raw-stream-path <path>", "Raw stream jsonl path")
.action(async (opts) => {
await runGatewayCommand(opts, params);
});