Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions extensions/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,10 +387,10 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
server.close(async () => {
telemetryService.stopActiveTime();

// Update metadata one final time before exiting
// Update metadata one final time before exiting (with completion flag)
try {
const history = services.chatHistory?.getHistory();
await updateAgentMetadata(history);
await updateAgentMetadata({ history, isComplete: true });
} catch (err) {
logger.debug("Failed to update metadata (non-critical)", err as any);
}
Expand Down Expand Up @@ -596,10 +596,10 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
server.close(async () => {
telemetryService.stopActiveTime();

// Update metadata one final time before exiting
// Update metadata one final time before exiting (with completion flag)
try {
const history = services.chatHistory?.getHistory();
await updateAgentMetadata(history);
await updateAgentMetadata({ history, isComplete: true });
} catch (err) {
logger.debug("Failed to update metadata (non-critical)", err as any);
}
Expand Down Expand Up @@ -628,10 +628,10 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
server.close(async () => {
telemetryService.stopActiveTime();

// Update metadata one final time before exiting
// Update metadata one final time before exiting (with completion flag)
try {
const history = services.chatHistory?.getHistory();
await updateAgentMetadata(history);
await updateAgentMetadata({ history, isComplete: true });
} catch (err) {
logger.debug("Failed to update metadata (non-critical)", err as any);
}
Expand Down
142 changes: 93 additions & 49 deletions extensions/cli/src/util/exit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,101 @@ import {
postAgentMetadata,
} from "./metadata.js";

/**
* Options for updating agent metadata
*/
export interface UpdateAgentMetadataOptions {
/** Chat history to extract summary from */
history?: ChatHistoryItem[];
/** Set to true when the agent is exiting/completing */
isComplete?: boolean;
}

/**
* Collect diff stats and add to metadata
* @returns Whether there were any changes
*/
async function collectDiffStats(
metadata: Record<string, any>,
): Promise<boolean> {
try {
const { diff, repoFound } = await getGitDiffSnapshot();
if (repoFound && diff) {
const { additions, deletions } = calculateDiffStats(diff);
if (additions > 0 || deletions > 0) {
metadata.additions = additions;
metadata.deletions = deletions;
return true;
}
}
} catch (err) {
logger.debug("Failed to calculate diff stats (non-critical)", err as any);
}
return false;
}

/**
* Extract summary from conversation history and add to metadata
*/
function collectSummary(
metadata: Record<string, any>,
history: ChatHistoryItem[] | undefined,
): void {
if (!history || history.length === 0) {
return;
}
try {
const summary = extractSummary(history);
if (summary) {
metadata.summary = summary;
}
} catch (err) {
logger.debug(
"Failed to extract conversation summary (non-critical)",
err as any,
);
}
}

/**
* Extract session usage and add to metadata
*/
function collectSessionUsage(metadata: Record<string, any>): void {
try {
const usage = getSessionUsage();
if (usage.totalCost > 0) {
metadata.usage = {
totalCost: parseFloat(usage.totalCost.toFixed(6)),
promptTokens: usage.promptTokens,
completionTokens: usage.completionTokens,
...(usage.promptTokensDetails?.cachedTokens && {
cachedTokens: usage.promptTokensDetails.cachedTokens,
}),
...(usage.promptTokensDetails?.cacheWriteTokens && {
cacheWriteTokens: usage.promptTokensDetails.cacheWriteTokens,
}),
};
}
} catch (err) {
logger.debug("Failed to get session usage (non-critical)", err as any);
}
}

/**
* Update agent session metadata in control plane
* Collects diff stats and conversation summary, posts to control plane
* This is called both during execution (after each turn) and before exit
*
* @param history - Chat history to extract summary from (optional, will fetch if not provided)
* @param options - Options including history and completion state
*/
export async function updateAgentMetadata(
history?: ChatHistoryItem[],
options?: ChatHistoryItem[] | UpdateAgentMetadataOptions,
): Promise<void> {
// Support both old signature (history array) and new signature (options object)
const { history, isComplete } = Array.isArray(options)
? { history: options, isComplete: false }
: { history: options?.history, isComplete: options?.isComplete ?? false };

try {
const agentId = getAgentIdFromArgs();
if (!agentId) {
Expand All @@ -32,57 +117,16 @@ export async function updateAgentMetadata(
}

const metadata: Record<string, any> = {};
const hasChanges = await collectDiffStats(metadata);

// Calculate diff stats
try {
const { diff, repoFound } = await getGitDiffSnapshot();
if (repoFound && diff) {
const { additions, deletions } = calculateDiffStats(diff);
if (additions > 0 || deletions > 0) {
metadata.additions = additions;
metadata.deletions = deletions;
}
}
} catch (err) {
logger.debug("Failed to calculate diff stats (non-critical)", err as any);
}

// Extract summary from conversation
if (history && history.length > 0) {
try {
const summary = extractSummary(history);
if (summary) {
metadata.summary = summary;
}
} catch (err) {
logger.debug(
"Failed to extract conversation summary (non-critical)",
err as any,
);
}
if (isComplete) {
metadata.isComplete = true;
metadata.hasChanges = hasChanges;
}

// Extract session usage (cost and token counts)
try {
const usage = getSessionUsage();
if (usage.totalCost > 0) {
metadata.usage = {
totalCost: parseFloat(usage.totalCost.toFixed(6)),
promptTokens: usage.promptTokens,
completionTokens: usage.completionTokens,
...(usage.promptTokensDetails?.cachedTokens && {
cachedTokens: usage.promptTokensDetails.cachedTokens,
}),
...(usage.promptTokensDetails?.cacheWriteTokens && {
cacheWriteTokens: usage.promptTokensDetails.cacheWriteTokens,
}),
};
}
} catch (err) {
logger.debug("Failed to get session usage (non-critical)", err as any);
}
collectSummary(metadata, history);
collectSessionUsage(metadata);

// Post metadata if we have any
if (Object.keys(metadata).length > 0) {
await postAgentMetadata(agentId, metadata);
}
Expand Down
Loading