feat: add curated activity format for token-efficient agent output

Create activity curator that consolidates hundreds of tiny message chunks into clean, human-readable text. Saves tokens and improves readability dramatically.

Changes:
- Add activity-curator.ts with curateAgentActivity() function
- Consolidate agent_message_chunk fragments into complete messages
- Format tool calls with status, input/output in readable structure
- Display plans with status icons ( completed, 🔄 in_progress,  pending)
- Add priority indicators (🔴 high, 🟡 medium, 🟢 low)
- Update get_agent_activity to support format='curated' (default) or 'raw'
- Curated format returns clean markdown text instead of JSON array

Example: 363 raw updates consolidating to single clean message instead of 363 JSON objects with fragments like " it.", " fix", "ify what".

🤖 Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
Mohamed Boudra
2025-10-21 16:59:59 +02:00
parent 5b53f4a7ce
commit f2daa86d30
2 changed files with 188 additions and 22 deletions

View File

@@ -0,0 +1,140 @@
import type { AgentUpdate } from "./types.js";
/**
* Curate agent activity updates into a human-readable, token-efficient format
* Consolidates message chunks, formats tool calls, and structures output cleanly
*/
export function curateAgentActivity(updates: AgentUpdate[]): string {
const sections: string[] = [];
// Group updates by type and consolidate
let currentMessage = "";
let currentThought = "";
const toolCalls: Array<{ timestamp: Date; data: any }> = [];
const plans: Array<{ timestamp: Date; data: any }> = [];
for (const update of updates) {
const updateType = update.notification.update.sessionUpdate;
const content = (update.notification.update as any).content;
switch (updateType) {
case "agent_message_chunk":
if (content?.type === "text" && content?.text) {
currentMessage += content.text;
}
break;
case "agent_thought_chunk":
if (content?.type === "text" && content?.text) {
currentThought += content.text;
}
break;
case "tool_call":
case "tool_call_update":
toolCalls.push({
timestamp: update.timestamp,
data: update.notification.update,
});
break;
case "plan":
plans.push({
timestamp: update.timestamp,
data: (update.notification.update as any).entries,
});
break;
}
}
// Format consolidated message
if (currentMessage.trim()) {
sections.push("## Agent Response\n\n" + currentMessage.trim());
}
// Format consolidated thoughts
if (currentThought.trim()) {
sections.push(
"## Agent Thoughts\n\n```\n" + currentThought.trim() + "\n```"
);
}
// Format tool calls
if (toolCalls.length > 0) {
const toolSection = ["## Tool Calls\n"];
// Group tool calls by toolCallId to show lifecycle
const toolGroups = new Map<string, typeof toolCalls>();
for (const call of toolCalls) {
const toolCallId = call.data.toolCallId || "unknown";
if (!toolGroups.has(toolCallId)) {
toolGroups.set(toolCallId, []);
}
toolGroups.get(toolCallId)!.push(call);
}
for (const [toolCallId, calls] of toolGroups) {
const latestCall = calls[calls.length - 1];
const data = latestCall.data;
toolSection.push(`\n### ${data.title || data.kind || "Tool"}`);
toolSection.push(`- **Status**: ${data.status || "unknown"}`);
toolSection.push(`- **Call ID**: ${toolCallId.slice(0, 8)}...`);
if (data.rawInput && Object.keys(data.rawInput).length > 0) {
toolSection.push(`- **Input**: ${JSON.stringify(data.rawInput, null, 2)}`);
}
if (data.rawOutput && Object.keys(data.rawOutput).length > 0) {
toolSection.push(`- **Output**: ${JSON.stringify(data.rawOutput, null, 2)}`);
}
if (data.content && data.content.length > 0) {
toolSection.push(`- **Content**: ${formatToolContent(data.content)}`);
}
}
sections.push(toolSection.join("\n"));
}
// Format plans
if (plans.length > 0) {
const latestPlan = plans[plans.length - 1];
const planSection = ["## Plan\n"];
for (const entry of latestPlan.data) {
const icon =
entry.status === "completed" ? "✅" :
entry.status === "in_progress" ? "🔄" :
"⏳";
const priority =
entry.priority === "high" ? "🔴" :
entry.priority === "medium" ? "🟡" :
"🟢";
planSection.push(`${icon} ${priority} ${entry.content}`);
}
sections.push(planSection.join("\n"));
}
return sections.join("\n\n---\n\n") || "No activity to display.";
}
/**
* Format tool call content into readable text
*/
function formatToolContent(content: any[]): string {
return content
.map((item) => {
if (item.type === "text") {
return item.text;
}
if (item.type === "image") {
return `[Image: ${item.source || "embedded"}]`;
}
return JSON.stringify(item);
})
.join("\n");
}

View File

@@ -4,6 +4,7 @@ import { homedir } from "os";
import { resolve } from "path";
import { AgentManager } from "./agent-manager.js";
import type { SessionNotification } from "@agentclientprotocol/sdk";
import { curateAgentActivity } from "./activity-curator.js";
export interface AgentMcpServerOptions {
agentManager: AgentManager;
@@ -308,50 +309,75 @@ export async function createAgentMcpServer(
{
title: "Get Agent Activity",
description:
"Get the complete activity log for an agent, including all messages, tool calls, and results. This shows you everything the agent has done - what it said, what tools it called, and what output it produced. Essential for understanding what a completed or failed agent actually accomplished.",
"Get the agent's activity in a human-readable, token-efficient format. Consolidates message chunks, formats tool calls, and structures plans. By default returns curated text, but you can request raw updates with format='raw'.",
inputSchema: {
agentId: z.string().describe("Agent ID to query"),
format: z
.enum(["curated", "raw"])
.optional()
.default("curated")
.describe(
"Output format: 'curated' (default) for clean human-readable text, 'raw' for detailed JSON updates"
),
limit: z
.number()
.optional()
.describe(
"Maximum number of updates to return (most recent first). Omit to get all updates."
"Maximum number of updates to include (most recent first). Only applies to 'raw' format. Omit to get all updates."
),
},
outputSchema: {
agentId: z.string(),
updateCount: z.number().describe("Total number of updates"),
format: z.enum(["curated", "raw"]),
updateCount: z.number().describe("Total number of updates available"),
content: z.string().describe("Formatted activity content (if curated) or empty string (if raw)"),
updates: z.array(
z.object({
timestamp: z.string(),
type: z.string(),
data: z.any(),
})
),
).nullable().describe("Raw updates array (if raw format) or null (if curated)"),
},
},
async ({ agentId, limit }) => {
async ({ agentId, format = "curated", limit }) => {
const updates = agentManager.getAgentUpdates(agentId);
// Get the most recent updates if limit is specified
const selectedUpdates = limit
? updates.slice(-limit).reverse()
: updates;
if (format === "curated") {
// Return curated, human-readable format
const curatedText = curateAgentActivity(updates);
const result = {
agentId,
updateCount: updates.length,
updates: selectedUpdates.map((update) => ({
timestamp: update.timestamp.toISOString(),
type: getUpdateType(update.notification),
data: update.notification,
})),
};
return {
content: [],
structuredContent: {
agentId,
format: "curated" as const,
updateCount: updates.length,
content: curatedText,
updates: null,
},
};
} else {
// Return raw format (old behavior)
const selectedUpdates = limit
? updates.slice(-limit).reverse()
: updates;
return {
content: [],
structuredContent: result,
};
return {
content: [],
structuredContent: {
agentId,
format: "raw" as const,
updateCount: updates.length,
content: "",
updates: selectedUpdates.map((update) => ({
timestamp: update.timestamp.toISOString(),
type: getUpdateType(update.notification),
data: update.notification,
})),
},
};
}
}
);