mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
140
packages/voice-assistant/src/server/acp/activity-curator.ts
Normal file
140
packages/voice-assistant/src/server/acp/activity-curator.ts
Normal 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");
|
||||
}
|
||||
@@ -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,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user