feat(core): persist subagent agentId in tool call records (#25092)

This commit is contained in:
Abhi
2026-04-10 12:47:25 -04:00
committed by GitHub
parent f6c08a114b
commit 7d1de3bccc
6 changed files with 43 additions and 2 deletions
+1 -1
View File
@@ -114,7 +114,7 @@ export function createUnauthorizedToolError(toolName: string): string {
export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
readonly definition: LocalAgentDefinition<TOutput>;
private readonly agentId: string;
readonly agentId: string;
private readonly toolRegistry: ToolRegistry;
private readonly promptRegistry: PromptRegistry;
private readonly resourceRegistry: ResourceRegistry;
@@ -79,6 +79,7 @@ describe('LocalSubagentInvocation', () => {
mockExecutorInstance = {
run: vi.fn(),
definition: testDefinition,
agentId: 'test-agent-id',
} as unknown as Mocked<LocalAgentExecutor<z.ZodUnknown>>;
MockLocalAgentExecutor.create.mockResolvedValue(
+8 -1
View File
@@ -25,12 +25,14 @@ import {
isToolActivityError,
} from './types.js';
import { randomUUID } from 'node:crypto';
import type { z } from 'zod';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import {
sanitizeThoughtContent,
sanitizeToolArgs,
sanitizeErrorMessage,
} from '../utils/agent-sanitization-utils.js';
import { debugLogger } from '../utils/debugLogger.js';
const INPUT_PREVIEW_MAX_LENGTH = 50;
const DESCRIPTION_MAX_LENGTH = 200;
@@ -108,6 +110,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
updateOutput?: (output: ToolLiveOutput) => void,
): Promise<ToolResult> {
const recentActivity: SubagentActivityItem[] = [];
let executor: LocalAgentExecutor<z.ZodUnknown> | undefined;
try {
if (updateOutput) {
@@ -273,7 +276,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
}
};
const executor = await LocalAgentExecutor.create(
executor = await LocalAgentExecutor.create(
this.definition,
this.context,
onActivity,
@@ -319,11 +322,14 @@ ${output.result}`;
return {
llmContent: [{ text: resultContent }],
returnDisplay: progress,
data: { agentId: executor.agentId },
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
debugLogger.error(`Subagent '${this.definition.name}' failed:`, error);
const isAbort =
(error instanceof Error && error.name === 'AbortError') ||
errorMessage.includes('Aborted');
@@ -369,6 +375,7 @@ ${output.result}`;
return {
llmContent: `Subagent '${this.definition.name}' failed. Error: ${errorMessage}`,
returnDisplay: progress,
data: executor ? { agentId: executor.agentId } : undefined,
// We omit the 'error' property so that the UI renders our rich returnDisplay
// instead of the raw error message. The llmContent still informs the agent of the failure.
};
+4
View File
@@ -1050,6 +1050,10 @@ export class GeminiChat {
result: call.response?.responseParts || null,
status: call.status,
timestamp: new Date().toISOString(),
agentId:
typeof call.response?.data?.['agentId'] === 'string'
? call.response.data['agentId']
: undefined,
resultDisplay,
description:
'invocation' in call ? call.invocation?.getDescription() : undefined,
@@ -536,6 +536,34 @@ describe('ChatRecordingService', () => {
.toolCalls,
).toHaveLength(1);
});
it('should record agentId when provided', async () => {
chatRecordingService.recordMessage({
type: 'gemini',
content: '',
model: 'gemini-pro',
});
const toolCall: ToolCallRecord = {
id: 'tool-1',
name: 'testTool',
args: {},
status: CoreToolCallStatus.Success,
timestamp: new Date().toISOString(),
agentId: 'test-agent-id',
};
chatRecordingService.recordToolCalls('gemini-pro', [toolCall]);
const sessionFile = chatRecordingService.getConversationFilePath()!;
const conversation = (await loadConversationRecord(
sessionFile,
)) as ConversationRecord;
const geminiMsg = conversation.messages[0] as MessageRecord & {
type: 'gemini';
};
expect(geminiMsg.toolCalls).toHaveLength(1);
expect(geminiMsg.toolCalls![0].agentId).toBe('test-agent-id');
});
});
describe('deleteSession', () => {
@@ -45,6 +45,7 @@ export interface ToolCallRecord {
result?: PartListUnion | null;
status: Status;
timestamp: string;
agentId?: string;
// UI-specific fields for display purposes
displayName?: string;
description?: string;