Skip to content

Commit f963623

Browse files
committed
fix comments and rebase
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
1 parent 59d684d commit f963623

5 files changed

Lines changed: 155 additions & 5 deletions

File tree

docs/architecture/subagent-viewing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ When a parent agent delegates to a subagent via `KAgentRemoteA2ATool`, the subag
1414

1515
### 1. Session ID stamping (Python, parent agent)
1616

17-
`KAgentRemoteA2ATool` pre-generates a `context_id` (UUID) in `__init__` before the tool runs. The event converter stamps this ID as `adk_subagent_session_id` metadata onto the `function_call` DataPart, so the UI knows the subagent session ID as soon as the LLM emits the call — before the tool actually executes.
17+
`KAgentRemoteA2ATool` pre-generates a `context_id` (UUID) in `__init__` before the tool runs. The event converter stamps this ID as `kagent_subagent_session_id` metadata onto the `function_call` DataPart, so the UI knows the subagent session ID as soon as the LLM emits the call — before the tool actually executes.
1818

1919
### 2. User ID passthrough (Python, A2A interceptor)
2020

ui/src/app/actions/sessions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export async function getSubagentSessionWithEvents(
107107

108108
const session = sessionResp.data?.session;
109109
if (!session) {
110-
return { message: "Subagent session not found" };
110+
return { message: "Subagent session not found", error: "Subagent session not found" };
111111
}
112112
return {
113113
message: "Session with events fetched successfully",

ui/src/components/chat/AgentCallDisplay.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function SubagentActivityPanel({ sessionId, isComplete }: SubagentActivityPanelP
4141

4242
useEffect(() => {
4343
let cancelled = false;
44-
let timeoutId: ReturnType<typeof setTimeout>;
44+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
4545

4646
const fetchEvents = async () => {
4747
try {
@@ -78,7 +78,12 @@ function SubagentActivityPanel({ sessionId, isComplete }: SubagentActivityPanelP
7878
};
7979

8080
fetchEvents();
81-
return () => { cancelled = true; clearTimeout(timeoutId); };
81+
return () => {
82+
cancelled = true;
83+
if (timeoutId) {
84+
clearTimeout(timeoutId);
85+
}
86+
};
8287
}, [sessionId, isComplete]);
8388

8489
if (error) {

ui/src/lib/__tests__/messageHandlers.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,135 @@ describe('createMessageHandlers test', () => {
354354
});
355355
});
356356

357+
describe('subagent_session_id propagation', () => {
358+
// Shared handler factory for status-update / artifact-update tests
359+
function makeHandlers() {
360+
const emitted: Message[] = [];
361+
const handlers = createMessageHandlers({
362+
setMessages: (updater) => {
363+
const next = updater(emitted);
364+
emitted.length = 0;
365+
emitted.push(...next);
366+
},
367+
setIsStreaming: () => {},
368+
setStreamingContent: () => {},
369+
setChatStatus: () => {},
370+
agentContext: { namespace: 'kagent', agentName: 'testagent' },
371+
});
372+
return { emitted, handlers };
373+
}
374+
375+
test('status-update: agent function_call with kagent_subagent_session_id in DataPart metadata emits toolCallData with subagent_session_id', () => {
376+
const { emitted, handlers } = makeHandlers();
377+
378+
const statusUpdateCall: any = {
379+
kind: 'status-update', contextId: 'ctx', taskId: 'task', final: false,
380+
status: {
381+
state: 'working',
382+
message: {
383+
role: 'agent',
384+
parts: [{
385+
kind: 'data',
386+
data: { id: 'agent_call_1', name: 'kagent__NS__k8s_agent', args: { request: 'list pods' } },
387+
metadata: { kagent_type: 'function_call', kagent_subagent_session_id: 'sess-abc-123' },
388+
}],
389+
},
390+
},
391+
};
392+
handlers.handleMessageEvent(statusUpdateCall);
393+
394+
expect(emitted.length).toBe(1);
395+
const meta = emitted[0].metadata as ADKMetadata;
396+
expect(meta.originalType).toBe('ToolCallRequestEvent');
397+
expect(meta.toolCallData).toHaveLength(1);
398+
expect(meta.toolCallData![0].subagent_session_id).toBe('sess-abc-123');
399+
});
400+
401+
test('status-update: agent function_response with subagent_session_id in response dict emits toolResultData with subagent_session_id', () => {
402+
const { emitted, handlers } = makeHandlers();
403+
404+
const statusUpdateResp: any = {
405+
kind: 'status-update', contextId: 'ctx', taskId: 'task', final: false,
406+
status: {
407+
state: 'working',
408+
message: {
409+
role: 'agent',
410+
parts: [{
411+
kind: 'data',
412+
data: {
413+
id: 'agent_call_1',
414+
name: 'kagent__NS__k8s_agent',
415+
response: { result: 'done', subagent_session_id: 'sess-abc-123' },
416+
},
417+
metadata: { kagent_type: 'function_response' },
418+
}],
419+
},
420+
},
421+
};
422+
handlers.handleMessageEvent(statusUpdateResp);
423+
424+
const execMsg = emitted.find(m => (m.metadata as ADKMetadata)?.originalType === 'ToolCallExecutionEvent');
425+
expect(execMsg).toBeDefined();
426+
const resultData = (execMsg!.metadata as ADKMetadata).toolResultData!;
427+
expect(resultData).toHaveLength(1);
428+
expect(resultData[0].subagent_session_id).toBe('sess-abc-123');
429+
});
430+
431+
test('extractMessagesFromTasks: agent function_call DataPart with kagent_subagent_session_id emits toolCallData with subagent_session_id', () => {
432+
const tasks = [{
433+
contextId: 'ctx',
434+
id: 'task',
435+
history: [{
436+
kind: 'message',
437+
messageId: 'msg-1',
438+
role: 'agent',
439+
parts: [{
440+
kind: 'data',
441+
data: { id: 'agent_call_3', name: 'kagent__NS__k8s_agent', args: { request: 'list nodes' } },
442+
metadata: { kagent_type: 'function_call', kagent_subagent_session_id: 'sess-history-456' },
443+
}],
444+
metadata: {},
445+
}],
446+
}] as unknown as Task[];
447+
448+
const messages = extractMessagesFromTasks(tasks);
449+
expect(messages).toHaveLength(1);
450+
const meta = messages[0].metadata as ADKMetadata;
451+
expect(meta.originalType).toBe('ToolCallRequestEvent');
452+
expect(meta.toolCallData).toHaveLength(1);
453+
expect(meta.toolCallData![0].subagent_session_id).toBe('sess-history-456');
454+
});
455+
456+
test('extractMessagesFromTasks: agent function_response DataPart with subagent_session_id in response dict emits toolResultData with subagent_session_id', () => {
457+
const tasks = [{
458+
contextId: 'ctx',
459+
id: 'task',
460+
history: [{
461+
kind: 'message',
462+
messageId: 'msg-3',
463+
role: 'agent',
464+
parts: [{
465+
kind: 'data',
466+
data: {
467+
id: 'agent_call_3',
468+
name: 'kagent__NS__k8s_agent',
469+
response: { result: 'nodes listed', subagent_session_id: 'sess-history-456' },
470+
},
471+
metadata: { kagent_type: 'function_response' },
472+
}],
473+
metadata: {},
474+
}],
475+
}] as unknown as Task[];
476+
477+
const messages = extractMessagesFromTasks(tasks);
478+
expect(messages).toHaveLength(1);
479+
const meta = messages[0].metadata as ADKMetadata;
480+
expect(meta.originalType).toBe('ToolCallExecutionEvent');
481+
expect(meta.toolResultData).toHaveLength(1);
482+
expect(meta.toolResultData![0].subagent_session_id).toBe('sess-history-456');
483+
});
484+
});
485+
357486
describe('getMetadataValue', () => {
358487
test('reads kagent_ prefixed key', () => {
359488
expect(getMetadataValue({ kagent_type: 'function_call' }, 'type')).toBe('function_call');

ui/src/lib/messageHandlers.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,19 +84,34 @@ export function extractMessagesFromTasks(tasks: Task[]): Message[] {
8484
// the function_response and are stamped on this card below.
8585
// Regular tool calls use the message's own invocation stats.
8686
const toolStats = isAgentToolName(toolData.name) ? undefined : msgStats;
87+
const fcSubagentSessionId = isAgentToolName(toolData.name)
88+
? getMetadataValue<string>(partMeta, "subagent_session_id")
89+
: undefined;
8790
messages.push(createMessage("", source, {
8891
originalType: "ToolCallRequestEvent",
8992
contextId: msgContextId,
9093
taskId: msgTaskId,
9194
additionalMetadata: {
92-
toolCallData: [{ id: toolData.id, name: toolData.name, args: (toolData.args as Record<string, unknown>) || {} }],
95+
toolCallData: [{
96+
id: toolData.id,
97+
name: toolData.name,
98+
args: (toolData.args as Record<string, unknown>) || {},
99+
...(fcSubagentSessionId ? { subagent_session_id: fcSubagentSessionId } : {}),
100+
}],
93101
...(toolStats && { tokenStats: toolStats }),
94102
},
95103
}));
96104
hasConvertedParts = true;
97105

98106
} else if (partType === "function_response") {
99107
const toolData = dp.data as unknown as ToolResponseData;
108+
let frSubagentSessionId: string | undefined;
109+
if (isAgentToolName(toolData.name)) {
110+
const responseObj = toolData.response as Record<string, unknown> | undefined;
111+
if (responseObj && typeof responseObj.subagent_session_id === "string") {
112+
frSubagentSessionId = responseObj.subagent_session_id;
113+
}
114+
}
100115
messages.push(createMessage("", source, {
101116
originalType: "ToolCallExecutionEvent",
102117
contextId: msgContextId,
@@ -107,6 +122,7 @@ export function extractMessagesFromTasks(tasks: Task[]): Message[] {
107122
name: toolData.name,
108123
content: normalizeToolResultToText(toolData),
109124
is_error: toolData.response?.isError || false,
125+
...(frSubagentSessionId ? { subagent_session_id: frSubagentSessionId } : {}),
110126
}],
111127
},
112128
}));

0 commit comments

Comments
 (0)