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

推荐订阅源

U
Unit 42
A
About on SuperTechFans
J
Java Code Geeks
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Stack Overflow Blog
Stack Overflow Blog
Recent Announcements
Recent Announcements
GbyAI
GbyAI
F
Fortinet All Blogs
Blog — PlanetScale
Blog — PlanetScale
MongoDB | Blog
MongoDB | Blog
MyScale Blog
MyScale Blog
aimingoo的专栏
aimingoo的专栏
H
Help Net Security
月光博客
月光博客
博客园 - Franky
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Apple Machine Learning Research
Apple Machine Learning Research
The Cloudflare Blog
WordPress大学
WordPress大学
S
SegmentFault 最新的问题
T
Tailwind CSS Blog
Jina AI
Jina AI
有赞技术团队
有赞技术团队
博客园_首页

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
fix(agents): emit cli runner llm lifecycle hooks (#70731)...
vincentkoc · 2026-04-24 · via Recent Commits to openclaw:main

@@ -1,9 +1,14 @@

1+

import fs from "node:fs";

2+

import os from "node:os";

3+

import path from "node:path";

4+

import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";

15

import { afterEach, describe, expect, it, vi } from "vitest";

26

import {

37

__testing as replyRunTesting,

48

createReplyOperation,

59

replyRunRegistry,

610

} from "../auto-reply/reply/reply-run-registry.js";

11+

import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";

712

import { runPreparedCliAgent } from "./cli-runner.js";

813

import {

914

createManagedRun,

@@ -15,6 +20,43 @@ import { executePreparedCliRun } from "./cli-runner/execute.js";

1520

import { resolveCliNoOutputTimeoutMs } from "./cli-runner/helpers.js";

1621

import type { PreparedCliRunContext } from "./cli-runner/types.js";

172223+

vi.mock("../plugins/hook-runner-global.js", async () => {

24+

const actual = await vi.importActual<typeof import("../plugins/hook-runner-global.js")>(

25+

"../plugins/hook-runner-global.js",

26+

);

27+

return {

28+

...actual,

29+

getGlobalHookRunner: vi.fn(() => null),

30+

};

31+

});

32+33+

const mockGetGlobalHookRunner = vi.mocked(getGlobalHookRunner);

34+35+

function createSessionFile(params?: { history?: Array<{ role: "user"; content: string }> }) {

36+

const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cli-hooks-"));

37+

const sessionFile = path.join(dir, "session.jsonl");

38+

fs.writeFileSync(

39+

sessionFile,

40+

`${JSON.stringify({

41+

type: "session",

42+

version: CURRENT_SESSION_VERSION,

43+

id: "session-test",

44+

timestamp: new Date(0).toISOString(),

45+

cwd: dir,

46+

})}\n`,

47+

"utf-8",

48+

);

49+

const sessionManager = SessionManager.open(sessionFile);

50+

for (const entry of params?.history ?? []) {

51+

sessionManager.appendMessage({

52+

role: entry.role,

53+

content: entry.content,

54+

timestamp: Date.now(),

55+

});

56+

}

57+

return { dir, sessionFile };

58+

}

59+1860

function buildPreparedContext(params?: {

1961

sessionKey?: string;

2062

cliSessionId?: string;

@@ -67,6 +109,7 @@ function buildPreparedContext(params?: {

67109

describe("runCliAgent reliability", () => {

68110

afterEach(() => {

69111

replyRunTesting.resetReplyRunRegistry();

112+

mockGetGlobalHookRunner.mockReset();

70113

});

7111472115

it("fails with timeout when no-output watchdog trips", async () => {

@@ -312,6 +355,150 @@ describe("runCliAgent reliability", () => {

312355

expect(result.meta.finalAssistantVisibleText).toBe("goodbye from cli");

313356

expect(result.meta.finalAssistantRawText).toBe("hello from cli");

314357

});

358+359+

it("emits llm_input, llm_output, and agent_end hooks for successful CLI runs", async () => {

360+

const hookRunner = {

361+

hasHooks: vi.fn((hookName: string) =>

362+

["llm_input", "llm_output", "agent_end"].includes(hookName),

363+

),

364+

runLlmInput: vi.fn(async () => undefined),

365+

runLlmOutput: vi.fn(async () => undefined),

366+

runAgentEnd: vi.fn(async () => undefined),

367+

};

368+

mockGetGlobalHookRunner.mockReturnValue(hookRunner as never);

369+

const { dir, sessionFile } = createSessionFile();

370+371+

supervisorSpawnMock.mockResolvedValueOnce(

372+

createManagedRun({

373+

reason: "exit",

374+

exitCode: 0,

375+

exitSignal: null,

376+

durationMs: 50,

377+

stdout: "hello from cli",

378+

stderr: "",

379+

timedOut: false,

380+

noOutputTimedOut: false,

381+

}),

382+

);

383+384+

try {

385+

await runPreparedCliAgent({

386+

...buildPreparedContext(),

387+

params: {

388+

...buildPreparedContext().params,

389+

sessionFile,

390+

workspaceDir: dir,

391+

sessionKey: "agent:main:main",

392+

agentId: "main",

393+

messageProvider: "acp",

394+

messageChannel: "telegram",

395+

trigger: "user",

396+

},

397+

});

398+399+

await vi.waitFor(() => {

400+

expect(hookRunner.runLlmInput).toHaveBeenCalledTimes(1);

401+

expect(hookRunner.runLlmOutput).toHaveBeenCalledTimes(1);

402+

expect(hookRunner.runAgentEnd).toHaveBeenCalledTimes(1);

403+

});

404+405+

expect(hookRunner.runLlmInput).toHaveBeenCalledWith(

406+

expect.objectContaining({

407+

runId: "run-2",

408+

sessionId: "s1",

409+

provider: "codex-cli",

410+

model: "gpt-5.4",

411+

prompt: "hi",

412+

systemPrompt: "You are a helpful assistant.",

413+

historyMessages: expect.any(Array),

414+

imagesCount: 0,

415+

}),

416+

expect.objectContaining({

417+

runId: "run-2",

418+

agentId: "main",

419+

sessionKey: "agent:main:main",

420+

sessionId: "s1",

421+

workspaceDir: dir,

422+

messageProvider: "acp",

423+

trigger: "user",

424+

channelId: "telegram",

425+

}),

426+

);

427+

expect(hookRunner.runLlmOutput).toHaveBeenCalledWith(

428+

expect.objectContaining({

429+

runId: "run-2",

430+

sessionId: "s1",

431+

provider: "codex-cli",

432+

model: "gpt-5.4",

433+

assistantTexts: ["hello from cli"],

434+

lastAssistant: expect.objectContaining({

435+

role: "assistant",

436+

content: [{ type: "text", text: "hello from cli" }],

437+

provider: "codex-cli",

438+

model: "gpt-5.4",

439+

}),

440+

}),

441+

expect.any(Object),

442+

);

443+

expect(hookRunner.runAgentEnd).toHaveBeenCalledWith(

444+

expect.objectContaining({

445+

success: true,

446+

messages: [

447+

{ role: "user", content: "hi", timestamp: expect.any(Number) },

448+

expect.objectContaining({

449+

role: "assistant",

450+

content: [{ type: "text", text: "hello from cli" }],

451+

}),

452+

],

453+

}),

454+

expect.any(Object),

455+

);

456+

} finally {

457+

fs.rmSync(dir, { recursive: true, force: true });

458+

}

459+

});

460+461+

it("emits agent_end with failure details when the CLI run fails", async () => {

462+

const hookRunner = {

463+

hasHooks: vi.fn((hookName: string) => ["llm_input", "agent_end"].includes(hookName)),

464+

runLlmInput: vi.fn(async () => undefined),

465+

runLlmOutput: vi.fn(async () => undefined),

466+

runAgentEnd: vi.fn(async () => undefined),

467+

};

468+

mockGetGlobalHookRunner.mockReturnValue(hookRunner as never);

469+470+

supervisorSpawnMock.mockResolvedValueOnce(

471+

createManagedRun({

472+

reason: "exit",

473+

exitCode: 1,

474+

exitSignal: null,

475+

durationMs: 50,

476+

stdout: "",

477+

stderr: "rate limit exceeded",

478+

timedOut: false,

479+

noOutputTimedOut: false,

480+

}),

481+

);

482+483+

await expect(runPreparedCliAgent(buildPreparedContext())).rejects.toThrow(

484+

"rate limit exceeded",

485+

);

486+487+

await vi.waitFor(() => {

488+

expect(hookRunner.runLlmInput).toHaveBeenCalledTimes(1);

489+

expect(hookRunner.runLlmOutput).not.toHaveBeenCalled();

490+

expect(hookRunner.runAgentEnd).toHaveBeenCalledTimes(1);

491+

});

492+493+

expect(hookRunner.runAgentEnd).toHaveBeenCalledWith(

494+

expect.objectContaining({

495+

success: false,

496+

error: "rate limit exceeded",

497+

messages: [{ role: "user", content: "hi", timestamp: expect.any(Number) }],

498+

}),

499+

expect.any(Object),

500+

);

501+

});

315502

});

316503317504

describe("resolveCliNoOutputTimeoutMs", () => {