feat: add raw stream logging flags
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Unreleased
|
## 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.
|
- 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
|
- 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
|
- 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
86
docs/debugging.md
Normal 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-mono’s
|
||||||
|
> `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.
|
||||||
@@ -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 { AgentEvent, AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||||
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
||||||
import type { ReasoningLevel } from "../auto-reply/thinking.js";
|
import type { ReasoningLevel } from "../auto-reply/thinking.js";
|
||||||
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
||||||
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
import { emitAgentEvent } from "../infra/agent-events.js";
|
import { emitAgentEvent } from "../infra/agent-events.js";
|
||||||
import { createSubsystemLogger } from "../logging.js";
|
import { createSubsystemLogger } from "../logging.js";
|
||||||
import { splitMediaFromOutput } from "../media/parse.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 THINKING_CLOSE_GLOBAL_RE = /<\s*\/\s*think(?:ing)?\s*>/gi;
|
||||||
const TOOL_RESULT_MAX_CHARS = 8000;
|
const TOOL_RESULT_MAX_CHARS = 8000;
|
||||||
const log = createSubsystemLogger("agent/embedded");
|
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";
|
export type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";
|
||||||
|
|
||||||
@@ -664,6 +692,15 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
typeof assistantRecord?.content === "string"
|
typeof assistantRecord?.content === "string"
|
||||||
? assistantRecord.content
|
? 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 = "";
|
let chunk = "";
|
||||||
if (evtType === "text_delta") {
|
if (evtType === "text_delta") {
|
||||||
chunk = delta;
|
chunk = delta;
|
||||||
@@ -756,6 +793,14 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
if (msg?.role === "assistant") {
|
if (msg?.role === "assistant") {
|
||||||
const assistantMessage = msg as AssistantMessage;
|
const assistantMessage = msg as AssistantMessage;
|
||||||
const rawText = extractAssistantText(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
|
const cleaned = params.enforceFinalTag
|
||||||
? stripThinkingSegments(stripUnpairedThinkingTags(rawText))
|
? stripThinkingSegments(stripUnpairedThinkingTags(rawText))
|
||||||
: stripThinkingSegments(rawText);
|
: stripThinkingSegments(rawText);
|
||||||
|
|||||||
@@ -599,7 +599,7 @@ describe("directive behavior", () => {
|
|||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("Elevated mode disabled.");
|
expect(text).toContain("Elevated mode disabled.");
|
||||||
expect(text).toContain("status agent:main:main");
|
expect(text).toContain("Session: agent:main:main");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -555,7 +555,6 @@ export async function handleCommands(params: {
|
|||||||
const reply = await buildStatusReply({
|
const reply = await buildStatusReply({
|
||||||
cfg,
|
cfg,
|
||||||
command,
|
command,
|
||||||
provider: command.provider,
|
|
||||||
sessionEntry,
|
sessionEntry,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
sessionScope,
|
sessionScope,
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ describe("buildStatusMessage", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
} as ClawdbotConfig,
|
} as ClawdbotConfig,
|
||||||
agent: {
|
agent: {
|
||||||
model: "anthropic/pi:opus",
|
model: "anthropic/pi:opus",
|
||||||
@@ -248,7 +247,6 @@ describe("buildStatusMessage", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
} as ClawdbotConfig,
|
} as ClawdbotConfig,
|
||||||
agent: { model: "anthropic/claude-opus-4-5" },
|
agent: { model: "anthropic/claude-opus-4-5" },
|
||||||
sessionEntry: { sessionId: "c1", updatedAt: 0, inputTokens: 10 },
|
sessionEntry: { sessionId: "c1", updatedAt: 0, inputTokens: 10 },
|
||||||
|
|||||||
@@ -296,7 +296,10 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
const activationLine = activationParts.filter(Boolean).join(" · ");
|
const activationLine = activationParts.filter(Boolean).join(" · ");
|
||||||
|
|
||||||
const authMode = resolveModelAuthMode(provider, args.config);
|
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
|
const costConfig = showCost
|
||||||
? resolveModelCostConfig({
|
? resolveModelCostConfig({
|
||||||
provider,
|
provider,
|
||||||
@@ -319,9 +322,6 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
const costLabel = showCost && hasUsage ? formatUsd(cost) : undefined;
|
const costLabel = showCost && hasUsage ? formatUsd(cost) : undefined;
|
||||||
|
|
||||||
const modelLabel = model ? `${provider}/${model}` : "unknown";
|
const modelLabel = model ? `${provider}/${model}` : "unknown";
|
||||||
const authLabelValue =
|
|
||||||
args.modelAuth ??
|
|
||||||
(authMode && authMode !== "unknown" ? authMode : undefined);
|
|
||||||
const authLabel = authLabelValue ? ` · 🔑 ${authLabelValue}` : "";
|
const authLabel = authLabelValue ? ` · 🔑 ${authLabelValue}` : "";
|
||||||
const modelLine = `🧠 Model: ${modelLabel}${authLabel}`;
|
const modelLine = `🧠 Model: ${modelLabel}${authLabel}`;
|
||||||
const commit = resolveCommitHash();
|
const commit = resolveCommitHash();
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ type GatewayRunOpts = {
|
|||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
wsLog?: unknown;
|
wsLog?: unknown;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
|
rawStream?: boolean;
|
||||||
|
rawStreamPath?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GatewayRunParams = {
|
type GatewayRunParams = {
|
||||||
@@ -300,6 +302,14 @@ async function runGatewayCommand(
|
|||||||
}
|
}
|
||||||
setGatewayWsLogStyle(wsLogStyle);
|
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 cfg = loadConfig();
|
||||||
const portOverride = parsePort(opts.port);
|
const portOverride = parsePort(opts.port);
|
||||||
if (opts.port !== undefined && portOverride === null) {
|
if (opts.port !== undefined && portOverride === null) {
|
||||||
@@ -565,6 +575,8 @@ function addGatewayRunCommand(
|
|||||||
"auto",
|
"auto",
|
||||||
)
|
)
|
||||||
.option("--compact", 'Alias for "--ws-log compact"', false)
|
.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) => {
|
.action(async (opts) => {
|
||||||
await runGatewayCommand(opts, params);
|
await runGatewayCommand(opts, params);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user