fix(agent-core-v2): preserve compaction hook session

This commit is contained in:
_Kerman 2026-07-08 18:14:07 +08:00
parent 65ad6ab5ac
commit 5c076183ac
5 changed files with 31 additions and 9 deletions

View file

@ -315,6 +315,7 @@ externalHooks --> config #34495E
externalHooks --> bootstrap #34495E
externalHooks --> plugin #34495E
externalHooks --> contextMemory #34495E
externalHooks --> session_context #34495E
sessionExternalHooks --> session_lifecycle #34495E
sessionExternalHooks --> agent_lifecycle #34495E
sessionExternalHooks --> config #34495E

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 273 KiB

After

Width:  |  Height:  |  Size: 274 KiB

Before After
Before After

View file

@ -10,8 +10,9 @@
* of its own). The requester-side `SubagentStart` / `SubagentStop` hooks are
* translated by the Session-scope `SessionExternalHooksService`, which observes
* the `agentLifecycle` run slots hosted on `IAgentLifecycleService`. Appends
* UserPromptSubmit hook results
* and Stop hook continuation prompts through `contextMemory`.
* UserPromptSubmit hook results and Stop hook continuation prompts through
* `contextMemory`, and passes the current session id from `sessionContext`
* into hook runner payloads.
*/
import { IInstantiationService } from '#/_base/di/instantiation';
@ -42,6 +43,7 @@ import type { ToolDidExecuteContext, ToolWillExecuteContext } from '#/agent/tool
import { IAgentToolExecutorService } from '#/agent/toolExecutor/toolExecutor';
import { IAgentTurnService } from '#/agent/turn/turn';
import { toKimiErrorPayload } from '#/errors';
import { ISessionContext } from '#/session/sessionContext/sessionContext';
import { IAgentExternalHooksService } from './externalHooks';
import { IExternalHooksRunnerService } from '#/app/externalHooksRunner/externalHooksRunner';
@ -66,6 +68,7 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter
@IAgentContextMemoryService private readonly context: IAgentContextMemoryService,
@IEventBus private readonly eventBus: IEventBus,
@IInstantiationService private readonly instantiation: IInstantiationService,
@ISessionContext private readonly sessionContext: ISessionContext,
) {
super();
this.registerListeners();
@ -82,7 +85,12 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter
// output), and throwing here would clobber that with a finalize-abort error.
// The runner mirrors the legacy fire-and-forget behavior.
try {
void this.runner.fireAndForgetTrigger(event, { matcherValue, signal, inputData });
void this.runner.fireAndForgetTrigger(event, {
matcherValue,
signal,
sessionId: this.sessionContext.sessionId,
inputData,
});
} catch {}
}
@ -222,6 +230,7 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter
const block = await this.runner.triggerBlock('PreToolUse', {
matcherValue: ctx.toolCall.name,
signal: ctx.signal,
sessionId: this.sessionContext.sessionId,
inputData: {
toolName: ctx.toolCall.name,
toolInput,
@ -260,6 +269,7 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter
const results = await this.runner.trigger('UserPromptSubmit', {
matcherValue: input,
signal,
sessionId: this.sessionContext.sessionId,
inputData: { prompt: input, isSteer: ctx.isSteer },
});
signal.throwIfAborted();
@ -331,6 +341,7 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter
const block = await this.runner.triggerBlock('Stop', {
signal: ctx.signal,
sessionId: this.sessionContext.sessionId,
inputData: { stopHookActive: false },
});
ctx.signal.throwIfAborted();
@ -343,6 +354,7 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter
await this.runner.trigger('PreCompact', {
matcherValue: ctx.trigger,
signal,
sessionId: this.sessionContext.sessionId,
inputData: {
trigger: ctx.trigger,
tokenCount: ctx.tokenCount,

View file

@ -1,4 +1,4 @@
import { existsSync, mkdtempSync, readFileSync } from 'node:fs';
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'pathe';
@ -511,14 +511,14 @@ describe('FullCompaction', () => {
const [pre, post] = readHookPayloads(hookLog);
expect(pre).toMatchObject({
hook_event_name: 'PreCompact',
session_id: 'session-hooks',
session_id: 'test-session',
cwd: dir,
trigger: 'auto',
token_count: 39,
});
expect(post).toMatchObject({
hook_event_name: 'PostCompact',
session_id: 'session-hooks',
session_id: 'test-session',
cwd: dir,
trigger: 'auto',
estimated_token_count: ctx.contextData().tokenCount,
@ -2545,6 +2545,10 @@ function messageText(message: Message | undefined): string {
}
function hookPayloadLoggerCommand(logPath: string): string {
// Write the hook script to a file and run it with node, instead of
// `node -e <json>`; cmd.exe on Windows mangles the escaped quotes in the
// inline form and corrupts the script before it can run.
const scriptPath = `${logPath}.cjs`;
const script = [
"const fs = require('node:fs');",
"let input = '';",
@ -2553,7 +2557,8 @@ function hookPayloadLoggerCommand(logPath: string): string {
` fs.appendFileSync(${JSON.stringify(logPath)}, JSON.stringify(JSON.parse(input)) + '\\n');`,
'});',
].join('');
return `node -e ${JSON.stringify(script)}`;
writeFileSync(scriptPath, script);
return `${process.execPath} ${scriptPath}`;
}
function readHookPayloads(logPath: string): Array<Record<string, unknown>> {

View file

@ -576,7 +576,11 @@ function resolveExternalHooksRunner(
function isRunnerLike(
value: Pick<IExternalHooksRunnerService, 'trigger' | 'triggerBlock' | 'fireAndForgetTrigger'>,
): value is IExternalHooksRunnerService {
return '_serviceBrand' in value;
return (
typeof value.trigger === 'function' &&
typeof value.triggerBlock === 'function' &&
typeof value.fireAndForgetTrigger === 'function'
);
}
const noopHookRunner: IExternalHooksRunnerService = {