mirror of
https://github.com/getpaseo/paseo.git
synced 2026-08-14 12:23:16 +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 { resolve } from "path";
|
||||||
import { AgentManager } from "./agent-manager.js";
|
import { AgentManager } from "./agent-manager.js";
|
||||||
import type { SessionNotification } from "@agentclientprotocol/sdk";
|
import type { SessionNotification } from "@agentclientprotocol/sdk";
|
||||||
|
import { curateAgentActivity } from "./activity-curator.js";
|
||||||
|
|
||||||
export interface AgentMcpServerOptions {
|
export interface AgentMcpServerOptions {
|
||||||
agentManager: AgentManager;
|
agentManager: AgentManager;
|
||||||
@@ -308,50 +309,75 @@ export async function createAgentMcpServer(
|
|||||||
{
|
{
|
||||||
title: "Get Agent Activity",
|
title: "Get Agent Activity",
|
||||||
description:
|
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: {
|
inputSchema: {
|
||||||
agentId: z.string().describe("Agent ID to query"),
|
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
|
limit: z
|
||||||
.number()
|
.number()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.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: {
|
outputSchema: {
|
||||||
agentId: z.string(),
|
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(
|
updates: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
timestamp: z.string(),
|
timestamp: z.string(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
data: z.any(),
|
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);
|
const updates = agentManager.getAgentUpdates(agentId);
|
||||||
|
|
||||||
// Get the most recent updates if limit is specified
|
if (format === "curated") {
|
||||||
const selectedUpdates = limit
|
// Return curated, human-readable format
|
||||||
? updates.slice(-limit).reverse()
|
const curatedText = curateAgentActivity(updates);
|
||||||
: updates;
|
|
||||||
|
|
||||||
const result = {
|
return {
|
||||||
agentId,
|
content: [],
|
||||||
updateCount: updates.length,
|
structuredContent: {
|
||||||
updates: selectedUpdates.map((update) => ({
|
agentId,
|
||||||
timestamp: update.timestamp.toISOString(),
|
format: "curated" as const,
|
||||||
type: getUpdateType(update.notification),
|
updateCount: updates.length,
|
||||||
data: update.notification,
|
content: curatedText,
|
||||||
})),
|
updates: null,
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Return raw format (old behavior)
|
||||||
|
const selectedUpdates = limit
|
||||||
|
? updates.slice(-limit).reverse()
|
||||||
|
: updates;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [],
|
content: [],
|
||||||
structuredContent: result,
|
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