diff --git a/packages/agent-core-v2/docs/di-scope-domains.puml b/packages/agent-core-v2/docs/di-scope-domains.puml
index b334cc869..041145e42 100644
--- a/packages/agent-core-v2/docs/di-scope-domains.puml
+++ b/packages/agent-core-v2/docs/di-scope-domains.puml
@@ -202,6 +202,7 @@ llmRequester --> usage #34495E
llmRequester --> modelProvider #34495E
llmRequester --> config #34495E
toolExecutor --> toolRegistry #34495E
+toolExecutor --> record #34495E
toolExecutor --> telemetry #34495E
toolStore --> wireRecord #34495E
toolDedup --> telemetry #34495E
diff --git a/packages/agent-core-v2/docs/di-scope-domains.svg b/packages/agent-core-v2/docs/di-scope-domains.svg
index e09a6f5a6..f590a91a1 100644
--- a/packages/agent-core-v2/docs/di-scope-domains.svg
+++ b/packages/agent-core-v2/docs/di-scope-domains.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/packages/agent-core-v2/src/agent/loop/loopService.ts b/packages/agent-core-v2/src/agent/loop/loopService.ts
index 550785134..f929087e6 100644
--- a/packages/agent-core-v2/src/agent/loop/loopService.ts
+++ b/packages/agent-core-v2/src/agent/loop/loopService.ts
@@ -1,7 +1,5 @@
import { randomUUID } from 'node:crypto';
-import type { AgentEvent } from '@moonshot-ai/protocol';
-
import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
import { IAgentLLMRequesterService, type LLMRequestFinish } from '#/agent/llmRequester';
@@ -151,11 +149,8 @@ export class AgentLoopService implements IAgentLoopService {
const stepUuid = randomUUID();
const turnStep = `${turnId}.${String(currentStep)}`;
- const emit = (event: AgentEvent): void => {
- this.record.signal(event);
- };
- emit({ type: 'turn.step.started', turnId, step: currentStep, stepId: stepUuid });
+ this.record.signal({ type: 'turn.step.started', turnId, step: currentStep, stepId: stepUuid });
const emitStreamPart = this.createStreamPartHandler(turnId);
const response = await this.llmRequester.request(
@@ -164,7 +159,7 @@ export class AgentLoopService implements IAgentLoopService {
retry: {
maxAttempts: this.config.get(LOOP_CONTROL_SECTION)?.maxRetriesPerStep,
onRetry: (retry) => {
- emit({
+ this.record.signal({
type: 'turn.step.retrying',
turnId,
step: currentStep,
@@ -201,7 +196,6 @@ export class AgentLoopService implements IAgentLoopService {
const toolResults = await this.toolExecutor.execute(response.message.toolCalls, {
signal,
turnId,
- dispatchProtocolEvent: emit,
onToolResult: (toolCallId, result) => {
this.append({
...createToolMessage(toolCallId, toolResultOutputForModel(result)),
@@ -209,9 +203,6 @@ export class AgentLoopService implements IAgentLoopService {
isError: result.isError,
});
},
- onProgress: (toolCallId, update) => {
- emit({ type: 'tool.progress', turnId, toolCallId, update });
- },
});
if (toolResults.some((r) => r.stopTurn === true)) {
finishReason = 'completed';
diff --git a/packages/agent-core-v2/src/agent/toolExecutor/toolExecutor.ts b/packages/agent-core-v2/src/agent/toolExecutor/toolExecutor.ts
index fd3555ff6..3525b50f1 100644
--- a/packages/agent-core-v2/src/agent/toolExecutor/toolExecutor.ts
+++ b/packages/agent-core-v2/src/agent/toolExecutor/toolExecutor.ts
@@ -1,11 +1,9 @@
import { createDecorator } from '#/_base/di';
import type {
ToolResult,
- ToolUpdate,
ToolDidExecuteContext,
ToolWillExecuteContext,
} from '#/agent/tool';
-import type { AgentEvent } from '@moonshot-ai/protocol';
import type { ToolCall } from '#/app/llmProtocol';
import type { OrderedHookSlot } from '#/hooks';
@@ -13,8 +11,6 @@ export interface ToolExecutorExecuteOptions {
readonly signal: AbortSignal;
readonly turnId: number;
readonly onToolResult?: (toolCallId: string, result: ToolResult) => void | Promise;
- readonly dispatchProtocolEvent?: (event: AgentEvent) => void;
- readonly onProgress?: (toolCallId: string, update: ToolUpdate) => void;
}
export interface IAgentToolExecutorService {
diff --git a/packages/agent-core-v2/src/agent/toolExecutor/toolExecutorService.ts b/packages/agent-core-v2/src/agent/toolExecutor/toolExecutorService.ts
index 6a9b517aa..518878607 100644
--- a/packages/agent-core-v2/src/agent/toolExecutor/toolExecutorService.ts
+++ b/packages/agent-core-v2/src/agent/toolExecutor/toolExecutorService.ts
@@ -12,6 +12,7 @@ import {
import { PathSecurityError } from '#/_base/tools/policies/path-access';
import { isUserCancellation } from "#/_base/utils/abort";
import { isAbortError } from '#/agent/loop/errors';
+import { IAgentRecordService } from '#/agent/record';
import {
ToolAccesses,
type ExecutableTool,
@@ -20,6 +21,7 @@ import {
type ToolDidExecuteContext,
type ToolExecution,
type ToolResult,
+ type ToolUpdate,
type ToolWillExecuteContext,
} from '#/agent/tool';
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
@@ -58,6 +60,7 @@ export class AgentToolExecutorService implements IAgentToolExecutorService {
constructor(
@IAgentToolRegistryService private readonly toolRegistry: IAgentToolRegistryService,
+ @IAgentRecordService private readonly record: IAgentRecordService,
@ITelemetryService private readonly telemetry: ITelemetryService,
@ILogService private readonly log?: ILogService,
) {}
@@ -107,7 +110,7 @@ export class AgentToolExecutorService implements IAgentToolExecutorService {
const finalized = await this.finalizeToolResult(call, rawResult, options);
results.push(finalized);
- await dispatchToolResult(call, finalized, options);
+ await this.dispatchToolResult(call, finalized, options);
this.trackToolCall(call, finalized, timedResult.durationMs, options.turnId);
}
@@ -145,7 +148,7 @@ export class AgentToolExecutorService implements IAgentToolExecutorService {
output: string,
displayFields?: ToolCallDisplayFields,
): { task: ToolExecutionTask } => {
- dispatchToolCall(call, args, options, displayFields);
+ this.dispatchToolCall(call, args, options, displayFields);
return {
task: makeResolvedTask(makeErrorToolResult(call, args, output)),
};
@@ -160,7 +163,7 @@ export class AgentToolExecutorService implements IAgentToolExecutorService {
stopBatchAfterThis?: boolean;
} => {
const toolResult = this.normalizeAndMergeResult(result, call.toolName, undefined);
- dispatchToolCall(call, args, options, displayFields);
+ this.dispatchToolCall(call, args, options, displayFields);
return {
task: makeResolvedTask({
toolCall: call.toolCall,
@@ -223,7 +226,7 @@ export class AgentToolExecutorService implements IAgentToolExecutorService {
const executionMetadata = decision?.executionMetadata;
- dispatchToolCall(call, call.args, options, displayFields);
+ this.dispatchToolCall(call, call.args, options, displayFields);
return {
task: {
@@ -240,7 +243,7 @@ export class AgentToolExecutorService implements IAgentToolExecutorService {
options: ToolExecutorExecuteOptions,
): ToolExecutionTask {
const output = 'Tool skipped because a previous tool call stopped the turn.';
- dispatchToolCall(call, call.args, options);
+ this.dispatchToolCall(call, call.args, options);
return makeResolvedTask(makeErrorToolResult(call, call.args, output));
}
@@ -295,7 +298,7 @@ export class AgentToolExecutorService implements IAgentToolExecutorService {
signal,
onUpdate: (update) => {
if (signal.aborted) return;
- options.onProgress?.(call.toolCall.id, update);
+ this.dispatchToolProgress(call, update, options);
},
});
rawResult = await raceWithGraceTimeout(executePromise, signal, call.toolName);
@@ -326,6 +329,51 @@ export class AgentToolExecutorService implements IAgentToolExecutorService {
};
}
+ private dispatchToolCall(
+ call: PreflightedToolCall,
+ args: unknown,
+ options: ToolExecutorExecuteOptions,
+ displayFields?: ToolCallDisplayFields,
+ ): void {
+ this.record.signal({
+ type: 'tool.call.started',
+ turnId: options.turnId,
+ toolCallId: call.toolCall.id,
+ name: call.toolName,
+ args,
+ description: displayFields?.description,
+ display: displayFields?.display,
+ });
+ }
+
+ private async dispatchToolResult(
+ call: PreflightedToolCall,
+ result: ToolResult,
+ options: ToolExecutorExecuteOptions,
+ ): Promise {
+ await options.onToolResult?.(call.toolCall.id, result);
+ this.record.signal({
+ type: 'tool.result',
+ turnId: options.turnId,
+ toolCallId: call.toolCall.id,
+ output: result.output,
+ isError: result.isError,
+ });
+ }
+
+ private dispatchToolProgress(
+ call: RunnableToolCall,
+ update: ToolUpdate,
+ options: ToolExecutorExecuteOptions,
+ ): void {
+ this.record.signal({
+ type: 'tool.progress',
+ turnId: options.turnId,
+ toolCallId: call.toolCall.id,
+ update,
+ });
+ }
+
private async finalizeToolResult(
call: PreflightedToolCall,
result: ToolResult,
@@ -504,38 +552,6 @@ function toolCallDisplayFieldsFromExecution(
};
}
-function dispatchToolCall(
- call: PreflightedToolCall,
- args: unknown,
- options: ToolExecutorExecuteOptions,
- displayFields?: ToolCallDisplayFields,
-): void {
- options.dispatchProtocolEvent?.({
- type: 'tool.call.started',
- turnId: options.turnId,
- toolCallId: call.toolCall.id,
- name: call.toolName,
- args,
- description: displayFields?.description,
- display: displayFields?.display,
- });
-}
-
-async function dispatchToolResult(
- call: PreflightedToolCall,
- result: ToolResult,
- options: ToolExecutorExecuteOptions,
-): Promise {
- await options.onToolResult?.(call.toolCall.id, result);
- options.dispatchProtocolEvent?.({
- type: 'tool.result',
- turnId: options.turnId,
- toolCallId: call.toolCall.id,
- output: result.output,
- isError: result.isError,
- });
-}
-
function makeResolvedTask(result: PreparedToolResult): ToolExecutionTask {
return {
accesses: ToolAccesses.none(),
diff --git a/packages/agent-core-v2/test/mcp/mcp.test.ts b/packages/agent-core-v2/test/mcp/mcp.test.ts
index 76efd92f8..7e638fa4e 100644
--- a/packages/agent-core-v2/test/mcp/mcp.test.ts
+++ b/packages/agent-core-v2/test/mcp/mcp.test.ts
@@ -16,8 +16,10 @@ import { IAgentToolRegistryService } from '#/agent/toolRegistry';
import { AgentToolRegistryService } from '#/agent/toolRegistry/toolRegistryService';
import { IAgentTurnService } from '#/agent/turn';
import { IAgentProfileService } from '#/agent/profile';
+import { IAgentRecordService } from '#/agent/record';
import { createTestAgent, mcpServices, type TestAgentContext } from '../harness';
+import { stubRecord } from '../contextMemory/stubs';
import { stubTurnWithHooks } from '../turn/stubs';
import { discoverTools, executeTool, fakeMcpClient } from './stubs';
@@ -139,6 +141,7 @@ describe('AgentMcpService', () => {
on: () => toDisposable(() => {}),
});
ix.set(IAgentToolRegistryService, new SyncDescriptor(AgentToolRegistryService));
+ ix.stub(IAgentRecordService, stubRecord());
ix.set(IAgentToolExecutorService, new SyncDescriptor(AgentToolExecutorService));
ix.stub(IAgentTurnService, stubTurnWithHooks());
});
diff --git a/packages/agent-core-v2/test/toolDedup/tool-dedup.test.ts b/packages/agent-core-v2/test/toolDedup/tool-dedup.test.ts
index 0eb54ba0b..ee8a938f1 100644
--- a/packages/agent-core-v2/test/toolDedup/tool-dedup.test.ts
+++ b/packages/agent-core-v2/test/toolDedup/tool-dedup.test.ts
@@ -5,6 +5,7 @@ import { createServices } from '#/_base/di/test';
import { ITelemetryService } from '#/app/telemetry';
import type { ToolCall } from '#/app/llmProtocol';
import { IAgentLoopService } from '#/agent/loop';
+import { IAgentRecordService } from '#/agent/record';
import type { ExecutableTool, ExecutableToolContext, ToolExecution } from '#/agent/tool';
import {
IAgentToolDedupeService,
@@ -15,6 +16,7 @@ import type { ToolDedupResult } from '#/agent/toolDedupe';
import { AgentToolExecutorService, IAgentToolExecutorService } from '#/agent/toolExecutor';
import { AgentToolRegistryService, IAgentToolRegistryService } from '#/agent/toolRegistry';
import { IAgentTurnService } from '#/agent/turn';
+import { stubRecord } from '../contextMemory/stubs';
import { registerLogServices } from '../log/stubs';
import { recordingTelemetry, type TelemetryRecord } from '../telemetry/stubs';
import { stubLoopWithHooks, stubToolExecutor, stubTurnWithHooks } from '../turn/stubs';
@@ -159,6 +161,7 @@ describe('AgentToolDedupeService', () => {
additionalServices: (reg) => {
reg.defineInstance(ITelemetryService, recordingTelemetry(telemetryEvents));
reg.defineInstance(IAgentLoopService, loop);
+ reg.defineInstance(IAgentRecordService, stubRecord());
reg.defineInstance(IAgentTurnService, stubTurnWithHooks());
reg.define(IAgentToolRegistryService, AgentToolRegistryService);
reg.define(IAgentToolExecutorService, AgentToolExecutorService);
diff --git a/packages/agent-core-v2/test/toolExecutor/tool-executor.test.ts b/packages/agent-core-v2/test/toolExecutor/tool-executor.test.ts
index e52e57274..8e308a1e3 100644
--- a/packages/agent-core-v2/test/toolExecutor/tool-executor.test.ts
+++ b/packages/agent-core-v2/test/toolExecutor/tool-executor.test.ts
@@ -2,18 +2,21 @@ import type { ToolCall } from '#/app/llmProtocol/kosong';
import type { AgentEvent, ToolInputDisplay } from '@moonshot-ai/protocol';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+import { SyncDescriptor } from '#/_base/di/descriptors';
import { DisposableStore } from '#/_base/di/lifecycle';
import { createServices, type TestInstantiationService } from '#/_base/di/test';
+import { AgentRecordService, IAgentRecordService } from '#/agent/record';
import { ToolAccesses, type ExecutableTool, type ExecutableToolContext, type ExecutableToolResult, type ToolExecution, type ToolResult, type ToolUpdate } from '#/agent/tool';
import { IAgentToolExecutorService, AgentToolExecutorService, parseToolCallArguments } from '#/agent/toolExecutor';
import { IAgentToolRegistryService, AgentToolRegistryService } from '#/agent/toolRegistry';
+import { IAgentWireRecordService } from '#/agent/wireRecord';
import { ITelemetryService } from '#/app/telemetry';
+import { stubWireRecord } from '../contextMemory/stubs';
import { registerLogServices } from '../log/stubs';
import { recordingTelemetry, type TelemetryRecord } from '../telemetry/stubs';
type ToolExecutorEvent =
- | { readonly type: 'tool.result'; readonly toolCallId: string; readonly result: ToolResult }
- | { readonly type: 'tool.progress'; readonly toolCallId: string; readonly update: ToolUpdate };
+ | { readonly type: 'tool.result'; readonly toolCallId: string; readonly result: ToolResult };
let disposables: DisposableStore;
let ix: TestInstantiationService;
@@ -32,11 +35,16 @@ beforeEach(() => {
additionalServices: (reg) => {
reg.define(IAgentToolRegistryService, AgentToolRegistryService);
reg.define(IAgentToolExecutorService, AgentToolExecutorService);
+ reg.defineInstance(IAgentWireRecordService, stubWireRecord());
reg.defineInstance(ITelemetryService, recordingTelemetry(telemetryEvents));
registerLogServices(reg);
},
strict: true,
});
+ ix.set(IAgentRecordService, new SyncDescriptor(AgentRecordService, [{}]));
+ ix.get(IAgentRecordService).on((event) => {
+ protocolEvents.push(event);
+ });
executor = ix.get(IAgentToolExecutorService);
registry = ix.get(IAgentToolRegistryService);
});
@@ -359,9 +367,9 @@ describe('AgentToolExecutorService', () => {
await execute([toolCall('call_progress', 'progress', {})]);
- expect(events.filter((event) => event.type === 'tool.progress')).toEqual([
- { type: 'tool.progress', toolCallId: 'call_progress', update: updates[0] },
- { type: 'tool.progress', toolCallId: 'call_progress', update: updates[1] },
+ expect(protocolEvents.filter((event) => event.type === 'tool.progress')).toEqual([
+ { type: 'tool.progress', turnId: 0, toolCallId: 'call_progress', update: updates[0] },
+ { type: 'tool.progress', turnId: 0, toolCallId: 'call_progress', update: updates[1] },
]);
});
@@ -534,12 +542,6 @@ function execute(calls: ToolCall[], signal?: AbortSignal): Promise
onToolResult: (toolCallId, result) => {
events.push({ type: 'tool.result', toolCallId, result });
},
- dispatchProtocolEvent: (event) => {
- protocolEvents.push(event);
- },
- onProgress: (toolCallId, update) => {
- events.push({ type: 'tool.progress', toolCallId, update });
- },
});
}
@@ -568,9 +570,9 @@ function pairedToolCallIds(): { readonly calls: string[]; readonly results: stri
event.type === 'tool.call.started',
)
.map((event) => event.toolCallId),
- results: events
+ results: protocolEvents
.filter(
- (event): event is Extract =>
+ (event): event is Extract =>
event.type === 'tool.result',
)
.map((event) => event.toolCallId),