fix: centralize verbose overrides and tool stream gating
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
- Node bridge: harden keepalive + heartbeat handling (TCP keepalive, better disconnects, and keepalive config tests). (#577) — thanks @steipete
|
- Node bridge: harden keepalive + heartbeat handling (TCP keepalive, better disconnects, and keepalive config tests). (#577) — thanks @steipete
|
||||||
- Control UI: improve mobile responsiveness. (#558) — thanks @carlulsoe
|
- Control UI: improve mobile responsiveness. (#558) — thanks @carlulsoe
|
||||||
- Control UI: persist per-session verbose off and hide tool cards unless verbose is on. (#262) — thanks @steipete
|
- Control UI: persist per-session verbose off and hide tool cards unless verbose is on. (#262) — thanks @steipete
|
||||||
|
- Gateway: centralize verbose overrides and gate tool stream events at the server. (#262) — thanks @steipete
|
||||||
- CLI: add `sandbox list` and `sandbox recreate` commands for managing Docker sandbox containers after image/config updates. (#563) — thanks @pasogott
|
- CLI: add `sandbox list` and `sandbox recreate` commands for managing Docker sandbox containers after image/config updates. (#563) — thanks @pasogott
|
||||||
- Sandbox: allow `session_status` tool in sandboxed sessions by default. — thanks @steipete
|
- Sandbox: allow `session_status` tool in sandboxed sessions by default. — thanks @steipete
|
||||||
- CLI: add `clawdbot config --section <name>` to jump straight into a wizard section (repeatable).
|
- CLI: add `clawdbot config --section <name>` to jump straight into a wizard section (repeatable).
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ read_when:
|
|||||||
## Verbose directives (/verbose or /v)
|
## Verbose directives (/verbose or /v)
|
||||||
- Levels: `on|full` or `off` (default).
|
- Levels: `on|full` or `off` (default).
|
||||||
- Directive-only message toggles session verbose and replies `Verbose logging enabled.` / `Verbose logging disabled.`; invalid levels return a hint without changing state.
|
- Directive-only message toggles session verbose and replies `Verbose logging enabled.` / `Verbose logging disabled.`; invalid levels return a hint without changing state.
|
||||||
|
- `/verbose off` stores an explicit session override; clear it via the Sessions UI by choosing `inherit`.
|
||||||
- Inline directive affects only that message; session/global defaults apply otherwise.
|
- Inline directive affects only that message; session/global defaults apply otherwise.
|
||||||
- Send `/verbose` (or `/verbose:`) with no argument to see the current verbose level.
|
- Send `/verbose` (or `/verbose:`) with no argument to see the current verbose level.
|
||||||
- When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool result back as its own metadata-only message, prefixed with `<emoji> <tool-name>: <arg>` when available (path/command); the tool output itself is not forwarded. These tool summaries are sent as soon as each tool finishes (separate bubbles), not as streaming deltas. If you toggle `/verbose on|off` while a run is in-flight, subsequent tool bubbles honor the new setting.
|
- When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool result back as its own metadata-only message, prefixed with `<emoji> <tool-name>: <arg>` when available (path/command); the tool output itself is not forwarded. These tool summaries are sent as soon as each tool finishes (separate bubbles), not as streaming deltas. If you toggle `/verbose on|off` while a run is in-flight, subsequent tool bubbles honor the new setting.
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { emitAgentEvent } from "../infra/agent-events.js";
|
|
||||||
import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js";
|
import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js";
|
||||||
|
|
||||||
vi.mock("../infra/agent-events.js", () => ({
|
|
||||||
emitAgentEvent: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
type StubSession = {
|
type StubSession = {
|
||||||
subscribe: (fn: (evt: unknown) => void) => () => void;
|
subscribe: (fn: (evt: unknown) => void) => () => void;
|
||||||
};
|
};
|
||||||
@@ -15,8 +10,6 @@ type StubSession = {
|
|||||||
type SessionEventHandler = (evt: unknown) => void;
|
type SessionEventHandler = (evt: unknown) => void;
|
||||||
|
|
||||||
describe("subscribeEmbeddedPiSession", () => {
|
describe("subscribeEmbeddedPiSession", () => {
|
||||||
const emitAgentEventMock = vi.mocked(emitAgentEvent);
|
|
||||||
|
|
||||||
it("filters to <final> and falls back when tags are malformed", () => {
|
it("filters to <final> and falls back when tags are malformed", () => {
|
||||||
let handler: ((evt: unknown) => void) | undefined;
|
let handler: ((evt: unknown) => void) | undefined;
|
||||||
const session: StubSession = {
|
const session: StubSession = {
|
||||||
@@ -1474,48 +1467,6 @@ describe("subscribeEmbeddedPiSession", () => {
|
|||||||
expect(onToolResult).not.toHaveBeenCalled();
|
expect(onToolResult).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips tool stream events when tool verbose is off", () => {
|
|
||||||
let handler: ((evt: unknown) => void) | undefined;
|
|
||||||
const session: StubSession = {
|
|
||||||
subscribe: (fn) => {
|
|
||||||
handler = fn;
|
|
||||||
return () => {};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
emitAgentEventMock.mockReset();
|
|
||||||
|
|
||||||
subscribeEmbeddedPiSession({
|
|
||||||
session: session as unknown as Parameters<
|
|
||||||
typeof subscribeEmbeddedPiSession
|
|
||||||
>[0]["session"],
|
|
||||||
runId: "run-tool-events-off",
|
|
||||||
shouldEmitToolResult: () => false,
|
|
||||||
});
|
|
||||||
|
|
||||||
handler?.({
|
|
||||||
type: "tool_execution_start",
|
|
||||||
toolName: "read",
|
|
||||||
toolCallId: "tool-evt-1",
|
|
||||||
args: { path: "/tmp/off.txt" },
|
|
||||||
});
|
|
||||||
handler?.({
|
|
||||||
type: "tool_execution_update",
|
|
||||||
toolName: "read",
|
|
||||||
toolCallId: "tool-evt-1",
|
|
||||||
partialResult: "partial",
|
|
||||||
});
|
|
||||||
handler?.({
|
|
||||||
type: "tool_execution_end",
|
|
||||||
toolName: "read",
|
|
||||||
toolCallId: "tool-evt-1",
|
|
||||||
isError: false,
|
|
||||||
result: "ok",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(emitAgentEventMock).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("emits tool summaries when shouldEmitToolResult overrides verbose", () => {
|
it("emits tool summaries when shouldEmitToolResult overrides verbose", () => {
|
||||||
let handler: ((evt: unknown) => void) | undefined;
|
let handler: ((evt: unknown) => void) | undefined;
|
||||||
const session: StubSession = {
|
const session: StubSession = {
|
||||||
|
|||||||
@@ -368,7 +368,6 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
const toolMetas: Array<{ toolName?: string; meta?: string }> = [];
|
const toolMetas: Array<{ toolName?: string; meta?: string }> = [];
|
||||||
const toolMetaById = new Map<string, string | undefined>();
|
const toolMetaById = new Map<string, string | undefined>();
|
||||||
const toolSummaryById = new Set<string>();
|
const toolSummaryById = new Set<string>();
|
||||||
const toolEventById = new Set<string>();
|
|
||||||
const blockReplyBreak = params.blockReplyBreak ?? "text_end";
|
const blockReplyBreak = params.blockReplyBreak ?? "text_end";
|
||||||
const reasoningMode = params.reasoningMode ?? "off";
|
const reasoningMode = params.reasoningMode ?? "off";
|
||||||
const includeReasoning = reasoningMode === "on";
|
const includeReasoning = reasoningMode === "on";
|
||||||
@@ -590,7 +589,6 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
toolMetas.length = 0;
|
toolMetas.length = 0;
|
||||||
toolMetaById.clear();
|
toolMetaById.clear();
|
||||||
toolSummaryById.clear();
|
toolSummaryById.clear();
|
||||||
toolEventById.clear();
|
|
||||||
messagingToolSentTexts.length = 0;
|
messagingToolSentTexts.length = 0;
|
||||||
messagingToolSentTargets.length = 0;
|
messagingToolSentTargets.length = 0;
|
||||||
pendingMessagingTexts.clear();
|
pendingMessagingTexts.clear();
|
||||||
@@ -642,19 +640,16 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const shouldEmitToolEvents = shouldEmitToolResult();
|
const shouldEmitToolEvents = shouldEmitToolResult();
|
||||||
if (shouldEmitToolEvents) {
|
emitAgentEvent({
|
||||||
toolEventById.add(toolCallId);
|
runId: params.runId,
|
||||||
emitAgentEvent({
|
stream: "tool",
|
||||||
runId: params.runId,
|
data: {
|
||||||
stream: "tool",
|
phase: "start",
|
||||||
data: {
|
name: toolName,
|
||||||
phase: "start",
|
toolCallId,
|
||||||
name: toolName,
|
args: args as Record<string, unknown>,
|
||||||
toolCallId,
|
},
|
||||||
args: args as Record<string, unknown>,
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
params.onAgentEvent?.({
|
params.onAgentEvent?.({
|
||||||
stream: "tool",
|
stream: "tool",
|
||||||
data: { phase: "start", name: toolName, toolCallId },
|
data: { phase: "start", name: toolName, toolCallId },
|
||||||
@@ -710,18 +705,16 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
const partial = (evt as AgentEvent & { partialResult?: unknown })
|
const partial = (evt as AgentEvent & { partialResult?: unknown })
|
||||||
.partialResult;
|
.partialResult;
|
||||||
const sanitized = sanitizeToolResult(partial);
|
const sanitized = sanitizeToolResult(partial);
|
||||||
if (toolEventById.has(toolCallId)) {
|
emitAgentEvent({
|
||||||
emitAgentEvent({
|
runId: params.runId,
|
||||||
runId: params.runId,
|
stream: "tool",
|
||||||
stream: "tool",
|
data: {
|
||||||
data: {
|
phase: "update",
|
||||||
phase: "update",
|
name: toolName,
|
||||||
name: toolName,
|
toolCallId,
|
||||||
toolCallId,
|
partialResult: sanitized,
|
||||||
partialResult: sanitized,
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
params.onAgentEvent?.({
|
params.onAgentEvent?.({
|
||||||
stream: "tool",
|
stream: "tool",
|
||||||
data: {
|
data: {
|
||||||
@@ -768,22 +761,18 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldEmitToolEvents = toolEventById.has(toolCallId);
|
emitAgentEvent({
|
||||||
if (shouldEmitToolEvents) {
|
runId: params.runId,
|
||||||
emitAgentEvent({
|
stream: "tool",
|
||||||
runId: params.runId,
|
data: {
|
||||||
stream: "tool",
|
phase: "result",
|
||||||
data: {
|
name: toolName,
|
||||||
phase: "result",
|
toolCallId,
|
||||||
name: toolName,
|
meta,
|
||||||
toolCallId,
|
isError,
|
||||||
meta,
|
result: sanitizedResult,
|
||||||
isError,
|
},
|
||||||
result: sanitizedResult,
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
toolEventById.delete(toolCallId);
|
|
||||||
params.onAgentEvent?.({
|
params.onAgentEvent?.({
|
||||||
stream: "tool",
|
stream: "tool",
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -346,7 +346,10 @@ export async function runReplyAgent(params: {
|
|||||||
try {
|
try {
|
||||||
const runId = crypto.randomUUID();
|
const runId = crypto.randomUUID();
|
||||||
if (sessionKey) {
|
if (sessionKey) {
|
||||||
registerAgentRunContext(runId, { sessionKey });
|
registerAgentRunContext(runId, {
|
||||||
|
sessionKey,
|
||||||
|
verboseLevel: resolvedVerboseLevel,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
||||||
let fallbackProvider = followupRun.run.provider;
|
let fallbackProvider = followupRun.run.provider;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
saveSessionStore,
|
saveSessionStore,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||||
|
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||||
import { shortenHomePath } from "../../utils.js";
|
import { shortenHomePath } from "../../utils.js";
|
||||||
import { extractModelDirective } from "../model.js";
|
import { extractModelDirective } from "../model.js";
|
||||||
import type { MsgContext } from "../templating.js";
|
import type { MsgContext } from "../templating.js";
|
||||||
@@ -853,7 +854,7 @@ export async function handleDirectiveOnly(params: {
|
|||||||
else sessionEntry.thinkingLevel = directives.thinkLevel;
|
else sessionEntry.thinkingLevel = directives.thinkLevel;
|
||||||
}
|
}
|
||||||
if (directives.hasVerboseDirective && directives.verboseLevel) {
|
if (directives.hasVerboseDirective && directives.verboseLevel) {
|
||||||
sessionEntry.verboseLevel = directives.verboseLevel;
|
applyVerboseOverride(sessionEntry, directives.verboseLevel);
|
||||||
}
|
}
|
||||||
if (directives.hasReasoningDirective && directives.reasoningLevel) {
|
if (directives.hasReasoningDirective && directives.reasoningLevel) {
|
||||||
if (directives.reasoningLevel === "off")
|
if (directives.reasoningLevel === "off")
|
||||||
@@ -1027,11 +1028,7 @@ export async function persistInlineDirectives(params: {
|
|||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
if (directives.hasVerboseDirective && directives.verboseLevel) {
|
if (directives.hasVerboseDirective && directives.verboseLevel) {
|
||||||
if (directives.verboseLevel === "off") {
|
applyVerboseOverride(sessionEntry, directives.verboseLevel);
|
||||||
delete sessionEntry.verboseLevel;
|
|
||||||
} else {
|
|
||||||
sessionEntry.verboseLevel = directives.verboseLevel;
|
|
||||||
}
|
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
if (directives.hasReasoningDirective && directives.reasoningLevel) {
|
if (directives.hasReasoningDirective && directives.reasoningLevel) {
|
||||||
|
|||||||
@@ -119,7 +119,10 @@ export function createFollowupRunner(params: {
|
|||||||
try {
|
try {
|
||||||
const runId = crypto.randomUUID();
|
const runId = crypto.randomUUID();
|
||||||
if (queued.run.sessionKey) {
|
if (queued.run.sessionKey) {
|
||||||
registerAgentRunContext(runId, { sessionKey: queued.run.sessionKey });
|
registerAgentRunContext(runId, {
|
||||||
|
sessionKey: queued.run.sessionKey,
|
||||||
|
verboseLevel: queued.run.verboseLevel,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let autoCompactionCompleted = false;
|
let autoCompactionCompleted = false;
|
||||||
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import {
|
|||||||
import { resolveOutboundTarget } from "../infra/outbound/targets.js";
|
import { resolveOutboundTarget } from "../infra/outbound/targets.js";
|
||||||
import { normalizeMainKey } from "../routing/session-key.js";
|
import { normalizeMainKey } from "../routing/session-key.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
|
import { applyVerboseOverride } from "../sessions/level-overrides.js";
|
||||||
import { resolveSendPolicy } from "../sessions/send-policy.js";
|
import { resolveSendPolicy } from "../sessions/send-policy.js";
|
||||||
import {
|
import {
|
||||||
normalizeMessageProvider,
|
normalizeMessageProvider,
|
||||||
@@ -249,10 +250,6 @@ export async function agentCommand(
|
|||||||
let sessionEntry = resolvedSessionEntry;
|
let sessionEntry = resolvedSessionEntry;
|
||||||
const runId = opts.runId?.trim() || sessionId;
|
const runId = opts.runId?.trim() || sessionId;
|
||||||
|
|
||||||
if (sessionKey) {
|
|
||||||
registerAgentRunContext(runId, { sessionKey });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.deliver === true) {
|
if (opts.deliver === true) {
|
||||||
const sendPolicy = resolveSendPolicy({
|
const sendPolicy = resolveSendPolicy({
|
||||||
cfg,
|
cfg,
|
||||||
@@ -276,6 +273,13 @@ export async function agentCommand(
|
|||||||
persistedVerbose ??
|
persistedVerbose ??
|
||||||
(agentCfg?.verboseDefault as VerboseLevel | undefined);
|
(agentCfg?.verboseDefault as VerboseLevel | undefined);
|
||||||
|
|
||||||
|
if (sessionKey) {
|
||||||
|
registerAgentRunContext(runId, {
|
||||||
|
sessionKey,
|
||||||
|
verboseLevel: resolvedVerboseLevel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const needsSkillsSnapshot = isNewSession || !sessionEntry?.skillsSnapshot;
|
const needsSkillsSnapshot = isNewSession || !sessionEntry?.skillsSnapshot;
|
||||||
const skillsSnapshot = needsSkillsSnapshot
|
const skillsSnapshot = needsSkillsSnapshot
|
||||||
? buildWorkspaceSkillSnapshot(workspaceDir, { config: cfg })
|
? buildWorkspaceSkillSnapshot(workspaceDir, { config: cfg })
|
||||||
@@ -306,10 +310,7 @@ export async function agentCommand(
|
|||||||
if (thinkOverride === "off") delete next.thinkingLevel;
|
if (thinkOverride === "off") delete next.thinkingLevel;
|
||||||
else next.thinkingLevel = thinkOverride;
|
else next.thinkingLevel = thinkOverride;
|
||||||
}
|
}
|
||||||
if (verboseOverride) {
|
applyVerboseOverride(next, verboseOverride);
|
||||||
if (verboseOverride === "off") delete next.verboseLevel;
|
|
||||||
else next.verboseLevel = verboseOverride;
|
|
||||||
}
|
|
||||||
sessionStore[sessionKey] = next;
|
sessionStore[sessionKey] = next;
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await saveSessionStore(storePath, sessionStore);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -425,8 +425,12 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
const sessionFile = resolveSessionTranscriptPath(
|
const sessionFile = resolveSessionTranscriptPath(
|
||||||
cronSession.sessionEntry.sessionId,
|
cronSession.sessionEntry.sessionId,
|
||||||
);
|
);
|
||||||
|
const resolvedVerboseLevel =
|
||||||
|
(cronSession.sessionEntry.verboseLevel as "on" | "off" | undefined) ??
|
||||||
|
(agentCfg?.verboseDefault as "on" | "off" | undefined);
|
||||||
registerAgentRunContext(cronSession.sessionEntry.sessionId, {
|
registerAgentRunContext(cronSession.sessionEntry.sessionId, {
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
|
verboseLevel: resolvedVerboseLevel,
|
||||||
});
|
});
|
||||||
const messageProvider = resolvedDelivery.provider;
|
const messageProvider = resolvedDelivery.provider;
|
||||||
const claudeSessionId = cronSession.sessionEntry.claudeCliSessionId?.trim();
|
const claudeSessionId = cronSession.sessionEntry.claudeCliSessionId?.trim();
|
||||||
@@ -464,12 +468,7 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
provider: providerOverride,
|
provider: providerOverride,
|
||||||
model: modelOverride,
|
model: modelOverride,
|
||||||
thinkLevel,
|
thinkLevel,
|
||||||
verboseLevel:
|
verboseLevel: resolvedVerboseLevel,
|
||||||
(cronSession.sessionEntry.verboseLevel as
|
|
||||||
| "on"
|
|
||||||
| "off"
|
|
||||||
| undefined) ??
|
|
||||||
(agentCfg?.verboseDefault as "on" | "off" | undefined),
|
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
runId: cronSession.sessionEntry.sessionId,
|
runId: cronSession.sessionEntry.sessionId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import type { AgentEventPayload } from "../infra/agent-events.js";
|
import { normalizeVerboseLevel } from "../auto-reply/thinking.js";
|
||||||
|
import {
|
||||||
|
type AgentEventPayload,
|
||||||
|
getAgentRunContext,
|
||||||
|
} from "../infra/agent-events.js";
|
||||||
|
import { loadSessionEntry } from "./session-utils.js";
|
||||||
import { formatForLog } from "./ws-log.js";
|
import { formatForLog } from "./ws-log.js";
|
||||||
|
|
||||||
export type ChatRunEntry = {
|
export type ChatRunEntry = {
|
||||||
@@ -185,6 +190,24 @@ export function createAgentEventHandler({
|
|||||||
bridgeSendToSession(sessionKey, "chat", payload);
|
bridgeSendToSession(sessionKey, "chat", payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shouldEmitToolEvents = (runId: string, sessionKey?: string) => {
|
||||||
|
const runContext = getAgentRunContext(runId);
|
||||||
|
const runVerbose = normalizeVerboseLevel(runContext?.verboseLevel);
|
||||||
|
if (runVerbose) return runVerbose === "on";
|
||||||
|
if (!sessionKey) return false;
|
||||||
|
try {
|
||||||
|
const { cfg, entry } = loadSessionEntry(sessionKey);
|
||||||
|
const sessionVerbose = normalizeVerboseLevel(entry?.verboseLevel);
|
||||||
|
if (sessionVerbose) return sessionVerbose === "on";
|
||||||
|
const defaultVerbose = normalizeVerboseLevel(
|
||||||
|
cfg.agents?.defaults?.verboseDefault,
|
||||||
|
);
|
||||||
|
return defaultVerbose === "on";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (evt: AgentEventPayload) => {
|
return (evt: AgentEventPayload) => {
|
||||||
const chatLink = chatRunState.registry.peek(evt.runId);
|
const chatLink = chatRunState.registry.peek(evt.runId);
|
||||||
const sessionKey =
|
const sessionKey =
|
||||||
@@ -192,6 +215,10 @@ export function createAgentEventHandler({
|
|||||||
// Include sessionKey so Control UI can filter tool streams per session.
|
// Include sessionKey so Control UI can filter tool streams per session.
|
||||||
const agentPayload = sessionKey ? { ...evt, sessionKey } : evt;
|
const agentPayload = sessionKey ? { ...evt, sessionKey } : evt;
|
||||||
const last = agentRunSeq.get(evt.runId) ?? 0;
|
const last = agentRunSeq.get(evt.runId) ?? 0;
|
||||||
|
if (evt.stream === "tool" && !shouldEmitToolEvents(evt.runId, sessionKey)) {
|
||||||
|
agentRunSeq.set(evt.runId, evt.seq);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (evt.seq !== last + 1) {
|
if (evt.seq !== last + 1) {
|
||||||
broadcast("agent", {
|
broadcast("agent", {
|
||||||
runId: evt.runId,
|
runId: evt.runId,
|
||||||
|
|||||||
@@ -785,7 +785,10 @@ describe("gateway server agent", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
registerAgentRunContext("run-tool-1", { sessionKey: "main" });
|
registerAgentRunContext("run-tool-1", {
|
||||||
|
sessionKey: "main",
|
||||||
|
verboseLevel: "on",
|
||||||
|
});
|
||||||
|
|
||||||
const agentEvtP = onceMessage(
|
const agentEvtP = onceMessage(
|
||||||
ws,
|
ws,
|
||||||
@@ -813,6 +816,66 @@ describe("gateway server agent", () => {
|
|||||||
await server.close();
|
await server.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("suppresses tool stream events when verbose is off", async () => {
|
||||||
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
|
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||||
|
await fs.writeFile(
|
||||||
|
testState.sessionStorePath,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
"agent:main:main": {
|
||||||
|
sessionId: "sess-main",
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
verboseLevel: "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const { server, ws } = await startServerWithClient();
|
||||||
|
await connectOk(ws, {
|
||||||
|
client: {
|
||||||
|
name: "webchat",
|
||||||
|
version: "1.0.0",
|
||||||
|
platform: "test",
|
||||||
|
mode: "webchat",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
registerAgentRunContext("run-tool-off", { sessionKey: "agent:main:main" });
|
||||||
|
|
||||||
|
emitAgentEvent({
|
||||||
|
runId: "run-tool-off",
|
||||||
|
stream: "tool",
|
||||||
|
data: { phase: "start", name: "read", toolCallId: "tool-1" },
|
||||||
|
});
|
||||||
|
emitAgentEvent({
|
||||||
|
runId: "run-tool-off",
|
||||||
|
stream: "assistant",
|
||||||
|
data: { text: "hello" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const evt = await onceMessage(
|
||||||
|
ws,
|
||||||
|
(o) =>
|
||||||
|
o.type === "event" &&
|
||||||
|
o.event === "agent" &&
|
||||||
|
o.payload?.runId === "run-tool-off",
|
||||||
|
8000,
|
||||||
|
);
|
||||||
|
const payload =
|
||||||
|
evt.payload && typeof evt.payload === "object"
|
||||||
|
? (evt.payload as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
expect(payload.stream).toBe("assistant");
|
||||||
|
|
||||||
|
ws.close();
|
||||||
|
await server.close();
|
||||||
|
});
|
||||||
|
|
||||||
test("agent.wait resolves after lifecycle end", async () => {
|
test("agent.wait resolves after lifecycle end", async () => {
|
||||||
const { server, ws } = await startServerWithClient();
|
const { server, ws } = await startServerWithClient();
|
||||||
await connectOk(ws);
|
await connectOk(ws);
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ import {
|
|||||||
normalizeReasoningLevel,
|
normalizeReasoningLevel,
|
||||||
normalizeThinkLevel,
|
normalizeThinkLevel,
|
||||||
normalizeUsageDisplay,
|
normalizeUsageDisplay,
|
||||||
normalizeVerboseLevel,
|
|
||||||
} from "../auto-reply/thinking.js";
|
} from "../auto-reply/thinking.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import type { SessionEntry } from "../config/sessions.js";
|
import type { SessionEntry } from "../config/sessions.js";
|
||||||
import { isSubagentSessionKey } from "../routing/session-key.js";
|
import { isSubagentSessionKey } from "../routing/session-key.js";
|
||||||
|
import {
|
||||||
|
applyVerboseOverride,
|
||||||
|
parseVerboseOverride,
|
||||||
|
} from "../sessions/level-overrides.js";
|
||||||
import { normalizeSendPolicy } from "../sessions/send-policy.js";
|
import { normalizeSendPolicy } from "../sessions/send-policy.js";
|
||||||
import { parseSessionLabel } from "../sessions/session-label.js";
|
import { parseSessionLabel } from "../sessions/session-label.js";
|
||||||
import {
|
import {
|
||||||
@@ -103,13 +106,9 @@ export async function applySessionsPatchToStore(params: {
|
|||||||
|
|
||||||
if ("verboseLevel" in patch) {
|
if ("verboseLevel" in patch) {
|
||||||
const raw = patch.verboseLevel;
|
const raw = patch.verboseLevel;
|
||||||
if (raw === null) {
|
const parsed = parseVerboseOverride(raw);
|
||||||
delete next.verboseLevel;
|
if (!parsed.ok) return invalid(parsed.error);
|
||||||
} else if (raw !== undefined) {
|
applyVerboseOverride(next, parsed.value);
|
||||||
const normalized = normalizeVerboseLevel(String(raw));
|
|
||||||
if (!normalized) return invalid('invalid verboseLevel (use "on"|"off")');
|
|
||||||
next.verboseLevel = normalized;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("reasoningLevel" in patch) {
|
if ("reasoningLevel" in patch) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type AgentEventPayload = {
|
|||||||
|
|
||||||
export type AgentRunContext = {
|
export type AgentRunContext = {
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
|
verboseLevel?: "off" | "on";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep per-run counters so streams stay strictly monotonic per runId.
|
// Keep per-run counters so streams stay strictly monotonic per runId.
|
||||||
@@ -36,6 +37,9 @@ export function registerAgentRunContext(
|
|||||||
if (context.sessionKey && existing.sessionKey !== context.sessionKey) {
|
if (context.sessionKey && existing.sessionKey !== context.sessionKey) {
|
||||||
existing.sessionKey = context.sessionKey;
|
existing.sessionKey = context.sessionKey;
|
||||||
}
|
}
|
||||||
|
if (context.verboseLevel && existing.verboseLevel !== context.verboseLevel) {
|
||||||
|
existing.verboseLevel = context.verboseLevel;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAgentRunContext(runId: string) {
|
export function getAgentRunContext(runId: string) {
|
||||||
|
|||||||
34
src/sessions/level-overrides.ts
Normal file
34
src/sessions/level-overrides.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
normalizeVerboseLevel,
|
||||||
|
type VerboseLevel,
|
||||||
|
} from "../auto-reply/thinking.js";
|
||||||
|
import type { SessionEntry } from "../config/sessions.js";
|
||||||
|
|
||||||
|
export function parseVerboseOverride(
|
||||||
|
raw: unknown,
|
||||||
|
):
|
||||||
|
| { ok: true; value: VerboseLevel | null | undefined }
|
||||||
|
| { ok: false; error: string } {
|
||||||
|
if (raw === null) return { ok: true, value: null };
|
||||||
|
if (raw === undefined) return { ok: true, value: undefined };
|
||||||
|
if (typeof raw !== "string") {
|
||||||
|
return { ok: false, error: 'invalid verboseLevel (use "on"|"off")' };
|
||||||
|
}
|
||||||
|
const normalized = normalizeVerboseLevel(raw);
|
||||||
|
if (!normalized) {
|
||||||
|
return { ok: false, error: 'invalid verboseLevel (use "on"|"off")' };
|
||||||
|
}
|
||||||
|
return { ok: true, value: normalized };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyVerboseOverride(
|
||||||
|
entry: SessionEntry,
|
||||||
|
level: VerboseLevel | null | undefined,
|
||||||
|
) {
|
||||||
|
if (level === undefined) return;
|
||||||
|
if (level === null) {
|
||||||
|
delete entry.verboseLevel;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entry.verboseLevel = level;
|
||||||
|
}
|
||||||
@@ -32,7 +32,11 @@ export type SessionsProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const THINK_LEVELS = ["", "off", "minimal", "low", "medium", "high"] as const;
|
const THINK_LEVELS = ["", "off", "minimal", "low", "medium", "high"] as const;
|
||||||
const VERBOSE_LEVELS = ["", "off", "on"] as const;
|
const VERBOSE_LEVELS = [
|
||||||
|
{ value: "", label: "inherit" },
|
||||||
|
{ value: "off", label: "off (explicit)" },
|
||||||
|
{ value: "on", label: "on" },
|
||||||
|
] as const;
|
||||||
const REASONING_LEVELS = ["", "off", "on", "stream"] as const;
|
const REASONING_LEVELS = ["", "off", "on", "stream"] as const;
|
||||||
|
|
||||||
export function renderSessions(props: SessionsProps) {
|
export function renderSessions(props: SessionsProps) {
|
||||||
@@ -178,8 +182,8 @@ function renderRow(
|
|||||||
onPatch(row.key, { verboseLevel: value || null });
|
onPatch(row.key, { verboseLevel: value || null });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${VERBOSE_LEVELS.map((level) =>
|
${VERBOSE_LEVELS.map(
|
||||||
html`<option value=${level}>${level || "inherit"}</option>`,
|
(level) => html`<option value=${level.value}>${level.label}</option>`,
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user