fix: persist verbose off and gate tool stream
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
- macOS: add node bridge heartbeat pings to detect half-open sockets and reconnect cleanly. (#572) — thanks @ngutman
|
- macOS: add node bridge heartbeat pings to detect half-open sockets and reconnect cleanly. (#572) — thanks @ngutman
|
||||||
- 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
|
||||||
- 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
|
||||||
- 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).
|
||||||
- Docs: add Hetzner Docker VPS guide. (#556) — thanks @Iamadig
|
- Docs: add Hetzner Docker VPS guide. (#556) — thanks @Iamadig
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
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;
|
||||||
};
|
};
|
||||||
@@ -10,6 +15,8 @@ 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 = {
|
||||||
@@ -1467,6 +1474,48 @@ 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,6 +368,7 @@ 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";
|
||||||
@@ -589,6 +590,7 @@ 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();
|
||||||
@@ -639,16 +641,20 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
`embedded run tool start: runId=${params.runId} tool=${toolName} toolCallId=${toolCallId}`,
|
`embedded run tool start: runId=${params.runId} tool=${toolName} toolCallId=${toolCallId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
emitAgentEvent({
|
const shouldEmitToolEvents = shouldEmitToolResult();
|
||||||
runId: params.runId,
|
if (shouldEmitToolEvents) {
|
||||||
stream: "tool",
|
toolEventById.add(toolCallId);
|
||||||
data: {
|
emitAgentEvent({
|
||||||
phase: "start",
|
runId: params.runId,
|
||||||
name: toolName,
|
stream: "tool",
|
||||||
toolCallId,
|
data: {
|
||||||
args: args as Record<string, unknown>,
|
phase: "start",
|
||||||
},
|
name: toolName,
|
||||||
});
|
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 },
|
||||||
@@ -656,7 +662,7 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
params.onToolResult &&
|
params.onToolResult &&
|
||||||
shouldEmitToolResult() &&
|
shouldEmitToolEvents &&
|
||||||
!toolSummaryById.has(toolCallId)
|
!toolSummaryById.has(toolCallId)
|
||||||
) {
|
) {
|
||||||
toolSummaryById.add(toolCallId);
|
toolSummaryById.add(toolCallId);
|
||||||
@@ -704,16 +710,18 @@ 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);
|
||||||
emitAgentEvent({
|
if (toolEventById.has(toolCallId)) {
|
||||||
runId: params.runId,
|
emitAgentEvent({
|
||||||
stream: "tool",
|
runId: params.runId,
|
||||||
data: {
|
stream: "tool",
|
||||||
phase: "update",
|
data: {
|
||||||
name: toolName,
|
phase: "update",
|
||||||
toolCallId,
|
name: toolName,
|
||||||
partialResult: sanitized,
|
toolCallId,
|
||||||
},
|
partialResult: sanitized,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
params.onAgentEvent?.({
|
params.onAgentEvent?.({
|
||||||
stream: "tool",
|
stream: "tool",
|
||||||
data: {
|
data: {
|
||||||
@@ -760,18 +768,22 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emitAgentEvent({
|
const shouldEmitToolEvents = toolEventById.has(toolCallId);
|
||||||
runId: params.runId,
|
if (shouldEmitToolEvents) {
|
||||||
stream: "tool",
|
emitAgentEvent({
|
||||||
data: {
|
runId: params.runId,
|
||||||
phase: "result",
|
stream: "tool",
|
||||||
name: toolName,
|
data: {
|
||||||
toolCallId,
|
phase: "result",
|
||||||
meta,
|
name: toolName,
|
||||||
isError,
|
toolCallId,
|
||||||
result: sanitizedResult,
|
meta,
|
||||||
},
|
isError,
|
||||||
});
|
result: sanitizedResult,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
toolEventById.delete(toolCallId);
|
||||||
params.onAgentEvent?.({
|
params.onAgentEvent?.({
|
||||||
stream: "tool",
|
stream: "tool",
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -390,6 +390,34 @@ describe("directive behavior", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("persists verbose off when directive is standalone", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
const storePath = path.join(home, "sessions.json");
|
||||||
|
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{ Body: "/verbose off", From: "+1222", To: "+1222" },
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: { store: storePath },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toMatch(/Verbose logging disabled\./);
|
||||||
|
const store = loadSessionStore(storePath);
|
||||||
|
const entry = Object.values(store)[0];
|
||||||
|
expect(entry?.verboseLevel).toBe("off");
|
||||||
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("shows current think level when /think has no argument", async () => {
|
it("shows current think level when /think has no argument", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
|||||||
@@ -853,8 +853,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) {
|
||||||
if (directives.verboseLevel === "off") delete sessionEntry.verboseLevel;
|
sessionEntry.verboseLevel = directives.verboseLevel;
|
||||||
else sessionEntry.verboseLevel = directives.verboseLevel;
|
|
||||||
}
|
}
|
||||||
if (directives.hasReasoningDirective && directives.reasoningLevel) {
|
if (directives.hasReasoningDirective && directives.reasoningLevel) {
|
||||||
if (directives.reasoningLevel === "off")
|
if (directives.reasoningLevel === "off")
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ describe("gateway server sessions", () => {
|
|||||||
const patched = await rpcReq<{ ok: true; key: string }>(
|
const patched = await rpcReq<{ ok: true; key: string }>(
|
||||||
ws,
|
ws,
|
||||||
"sessions.patch",
|
"sessions.patch",
|
||||||
{ key: "agent:main:main", thinkingLevel: "medium", verboseLevel: null },
|
{ key: "agent:main:main", thinkingLevel: "medium", verboseLevel: "off" },
|
||||||
);
|
);
|
||||||
expect(patched.ok).toBe(true);
|
expect(patched.ok).toBe(true);
|
||||||
expect(patched.payload?.ok).toBe(true);
|
expect(patched.payload?.ok).toBe(true);
|
||||||
@@ -186,13 +186,32 @@ describe("gateway server sessions", () => {
|
|||||||
(s) => s.key === "agent:main:main",
|
(s) => s.key === "agent:main:main",
|
||||||
);
|
);
|
||||||
expect(main2?.thinkingLevel).toBe("medium");
|
expect(main2?.thinkingLevel).toBe("medium");
|
||||||
expect(main2?.verboseLevel).toBeUndefined();
|
expect(main2?.verboseLevel).toBe("off");
|
||||||
expect(main2?.sendPolicy).toBe("deny");
|
expect(main2?.sendPolicy).toBe("deny");
|
||||||
const subagent = list2.payload?.sessions.find(
|
const subagent = list2.payload?.sessions.find(
|
||||||
(s) => s.key === "agent:main:subagent:one",
|
(s) => s.key === "agent:main:subagent:one",
|
||||||
);
|
);
|
||||||
expect(subagent?.label).toBe("Briefing");
|
expect(subagent?.label).toBe("Briefing");
|
||||||
|
|
||||||
|
const clearedVerbose = await rpcReq<{ ok: true; key: string }>(
|
||||||
|
ws,
|
||||||
|
"sessions.patch",
|
||||||
|
{ key: "agent:main:main", verboseLevel: null },
|
||||||
|
);
|
||||||
|
expect(clearedVerbose.ok).toBe(true);
|
||||||
|
|
||||||
|
const list3 = await rpcReq<{
|
||||||
|
sessions: Array<{
|
||||||
|
key: string;
|
||||||
|
verboseLevel?: string;
|
||||||
|
}>;
|
||||||
|
}>(ws, "sessions.list", {});
|
||||||
|
expect(list3.ok).toBe(true);
|
||||||
|
const main3 = list3.payload?.sessions.find(
|
||||||
|
(s) => s.key === "agent:main:main",
|
||||||
|
);
|
||||||
|
expect(main3?.verboseLevel).toBeUndefined();
|
||||||
|
|
||||||
const listByLabel = await rpcReq<{
|
const listByLabel = await rpcReq<{
|
||||||
sessions: Array<{ key: string }>;
|
sessions: Array<{ key: string }>;
|
||||||
}>(ws, "sessions.list", {
|
}>(ws, "sessions.list", {
|
||||||
|
|||||||
@@ -108,8 +108,7 @@ export async function applySessionsPatchToStore(params: {
|
|||||||
} else if (raw !== undefined) {
|
} else if (raw !== undefined) {
|
||||||
const normalized = normalizeVerboseLevel(String(raw));
|
const normalized = normalizeVerboseLevel(String(raw));
|
||||||
if (!normalized) return invalid('invalid verboseLevel (use "on"|"off")');
|
if (!normalized) return invalid('invalid verboseLevel (use "on"|"off")');
|
||||||
if (normalized === "off") delete next.verboseLevel;
|
next.verboseLevel = normalized;
|
||||||
else next.verboseLevel = normalized;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user