惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
Google DeepMind News
Google DeepMind News
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
MongoDB | Blog
MongoDB | Blog
I
InfoQ
N
Netflix TechBlog - Medium
T
Tailwind CSS Blog
量子位
博客园 - 叶小钗
月光博客
月光博客
IT之家
IT之家
G
Google Developers Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
小众软件
小众软件
S
SegmentFault 最新的问题
Engineering at Meta
Engineering at Meta
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
爱范儿
爱范儿
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
宝玉的分享
宝玉的分享

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
feat(diagnostics): emit tool execution events · openclaw/...
vincentkoc · 2026-04-24 · via Recent Commits to openclaw:main

@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";

22

import {

33

onDiagnosticEvent,

44

resetDiagnosticEventsForTest,

5+

type DiagnosticEventPayload,

56

type DiagnosticToolLoopEvent,

67

} from "../infra/diagnostic-events.js";

78

import { resetDiagnosticSessionStateForTest } from "../logging/diagnostic-session-state.js";

@@ -85,6 +86,22 @@ describe("before_tool_call loop detection behavior", () => {

8586

}

8687

}

878889+

async function withToolExecutionEvents(

90+

run: (emitted: DiagnosticEventPayload[]) => Promise<void>,

91+

) {

92+

const emitted: DiagnosticEventPayload[] = [];

93+

const stop = onDiagnosticEvent((evt) => {

94+

if (evt.type.startsWith("tool.execution.")) {

95+

emitted.push(evt);

96+

}

97+

});

98+

try {

99+

await run(emitted);

100+

} finally {

101+

stop();

102+

}

103+

}

104+88105

function createPingPongTools(options?: { withProgress?: boolean }) {

89106

const readExecute = options?.withProgress

90107

? vi.fn().mockImplementation(async (toolCallId: string) => ({

@@ -331,6 +348,115 @@ describe("before_tool_call loop detection behavior", () => {

331348

});

332349

});

333350

});

351+352+

it("emits diagnostic tool execution events without parameter values", async () => {

353+

const trace = {

354+

traceId: "4bf92f3577b34da6a3ce929d0e0e4736",

355+

spanId: "00f067aa0ba902b7",

356+

traceFlags: "01",

357+

};

358+

const execute = vi.fn().mockResolvedValue({

359+

content: [{ type: "text", text: "ok" }],

360+

});

361+

const tool = wrapToolWithBeforeToolCallHook({ name: "bash", execute } as any, {

362+

agentId: "main",

363+

sessionKey: "session-key",

364+

sessionId: "session-id",

365+

runId: "run-1",

366+

trace,

367+

loopDetection: { enabled: false },

368+

});

369+370+

await withToolExecutionEvents(async (emitted) => {

371+

await tool.execute(

372+

"tool-call-1",

373+

{ command: "pwd", token: "sk-1234567890abcdef1234567890abcdef" },

374+

undefined,

375+

undefined,

376+

);

377+378+

expect(emitted.map((evt) => evt.type)).toEqual([

379+

"tool.execution.started",

380+

"tool.execution.completed",

381+

]);

382+

expect(emitted[0]).toMatchObject({

383+

type: "tool.execution.started",

384+

runId: "run-1",

385+

sessionKey: "session-key",

386+

sessionId: "session-id",

387+

toolName: "exec",

388+

toolCallId: "tool-call-1",

389+

paramsSummary: {

390+

kind: "object",

391+

},

392+

trace,

393+

});

394+

expect(emitted[0]?.trace).not.toBe(trace);

395+

expect(Object.isFrozen(emitted[0]?.trace)).toBe(true);

396+

expect(emitted[1]).toMatchObject({

397+

type: "tool.execution.completed",

398+

durationMs: expect.any(Number),

399+

});

400+

expect(JSON.stringify(emitted)).not.toContain("sk-1234567890abcdef1234567890abcdef");

401+

expect(JSON.stringify(emitted)).not.toContain("pwd");

402+

});

403+

});

404+405+

it("emits diagnostic tool execution error events with redacted errors", async () => {

406+

const execute = vi

407+

.fn()

408+

.mockRejectedValue(new Error("failed with key sk-1234567890abcdef1234567890abcdef"));

409+

const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {

410+

agentId: "main",

411+

sessionKey: "session-key",

412+

loopDetection: { enabled: false },

413+

});

414+415+

await withToolExecutionEvents(async (emitted) => {

416+

await expect(

417+

tool.execute("tool-call-error", { path: "/tmp/file" }, undefined, undefined),

418+

).rejects.toThrow("failed with key");

419+420+

expect(emitted.map((evt) => evt.type)).toEqual([

421+

"tool.execution.started",

422+

"tool.execution.error",

423+

]);

424+

expect(emitted[1]).toMatchObject({

425+

type: "tool.execution.error",

426+

toolName: "read",

427+

toolCallId: "tool-call-error",

428+

durationMs: expect.any(Number),

429+

errorCategory: "Error",

430+

});

431+

expect(JSON.stringify(emitted[1])).not.toContain("sk-1234567890abcdef1234567890abcdef");

432+

});

433+

});

434+435+

it("summarizes hostile object params without enumerating keys", async () => {

436+

const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "ok" }] });

437+

const tool = wrapToolWithBeforeToolCallHook({ name: "bash", execute } as any, {

438+

agentId: "main",

439+

sessionKey: "session-key",

440+

loopDetection: { enabled: false },

441+

});

442+

const params = new Proxy(

443+

{},

444+

{

445+

ownKeys() {

446+

throw new Error("should not enumerate params");

447+

},

448+

},

449+

);

450+451+

await withToolExecutionEvents(async (emitted) => {

452+

await tool.execute("tool-call-proxy", params, undefined, undefined);

453+454+

expect(emitted[0]).toMatchObject({

455+

type: "tool.execution.started",

456+

paramsSummary: { kind: "object" },

457+

});

458+

});

459+

});

334460

});

335461336462

describe("before_tool_call requireApproval handling", () => {