Merge pull request #9909 from continuedev/nate/fixes-4888

Add detailed logging for metadata endpoint requests
This commit is contained in:
Nate Sesti
2026-01-25 23:30:03 -08:00
committed by GitHub
3 changed files with 77 additions and 56 deletions

View File

@@ -1,10 +1,35 @@
import type { Session, ToolStatus } from "core/index.js"; import type { ChatHistoryItem, Session, ToolStatus } from "core/index.js";
import { services } from "../services/index.js"; import { services } from "../services/index.js";
import { streamChatResponse } from "../stream/streamChatResponse.js"; import { streamChatResponse } from "../stream/streamChatResponse.js";
import { StreamCallbacks } from "../stream/streamChatResponse.types.js"; import { StreamCallbacks } from "../stream/streamChatResponse.types.js";
import { logger } from "../util/logger.js"; import { logger } from "../util/logger.js";
/**
* Remove partial assistant message if the last message is an empty assistant message.
* Used when a response is interrupted.
*/
export function removePartialAssistantMessage(
sessionHistory: ChatHistoryItem[],
): void {
try {
const svcHistory = services.chatHistory.getHistory();
const last = svcHistory[svcHistory.length - 1];
if (last && last.message.role === "assistant" && !last.message.content) {
services.chatHistory.setHistory(svcHistory.slice(0, -1));
}
} catch {
const lastMessage = sessionHistory[sessionHistory.length - 1];
if (
lastMessage &&
lastMessage.message.role === "assistant" &&
!lastMessage.message.content
) {
sessionHistory.pop();
}
}
}
// Modified version of streamChatResponse that supports interruption // Modified version of streamChatResponse that supports interruption
export async function streamChatResponseWithInterruption( export async function streamChatResponseWithInterruption(
state: ServerState, state: ServerState,
@@ -124,3 +149,21 @@ export interface ServerState {
pendingPermission: PendingPermission | null; pendingPermission: PendingPermission | null;
systemMessage?: string; systemMessage?: string;
} }
/**
* Check if the agent should be marked as complete based on conversation history.
* The agent is complete if the last message is from the assistant and has no tool calls.
*/
export function checkAgentComplete(
history: { message: { role: string; tool_calls?: any[] } }[] | undefined,
): boolean {
if (!history || history.length === 0) {
return false;
}
const lastItem = history[history.length - 1];
if (lastItem?.message?.role !== "assistant") {
return false;
}
const toolCalls = (lastItem.message as any).tool_calls;
return !toolCalls || toolCalls.length === 0;
}

View File

@@ -40,6 +40,8 @@ import { readStdinSync } from "../util/stdin.js";
import { ExtendedCommandOptions } from "./BaseCommandOptions.js"; import { ExtendedCommandOptions } from "./BaseCommandOptions.js";
import { import {
checkAgentComplete,
removePartialAssistantMessage,
streamChatResponseWithInterruption, streamChatResponseWithInterruption,
type ServerState, type ServerState,
} from "./serve.helpers.js"; } from "./serve.helpers.js";
@@ -464,28 +466,6 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
} }
}); });
// Process messages from the queue
function removePartialAssistantMessage(state: ServerState) {
try {
const svcHistory = services.chatHistory.getHistory();
const last = svcHistory[svcHistory.length - 1];
if (last && last.message.role === "assistant" && !last.message.content) {
const trimmed = svcHistory.slice(0, -1);
services.chatHistory.setHistory(trimmed);
}
} catch {
const lastMessage =
state.session.history[state.session.history.length - 1];
if (
lastMessage &&
lastMessage.message.role === "assistant" &&
!lastMessage.message.content
) {
state.session.history.pop();
}
}
}
async function processMessages(state: ServerState, llmApi: any) { async function processMessages(state: ServerState, llmApi: any) {
let processedMessage = false; let processedMessage = false;
while (state.serverRunning) { while (state.serverRunning) {
@@ -529,21 +509,11 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
// Update metadata after successful agent turn // Update metadata after successful agent turn
try { try {
const history = services.chatHistory?.getHistory(); const history = services.chatHistory?.getHistory();
await updateAgentMetadata({
// Check if the agent should be marked as complete history,
// The agent is complete if the last message is from the assistant and has no tool calls isComplete: checkAgentComplete(history),
let isComplete = false; });
if (history && history.length > 0) {
const lastItem = history[history.length - 1];
if (lastItem?.message?.role === "assistant") {
const toolCalls = (lastItem.message as any).tool_calls;
isComplete = !toolCalls || toolCalls.length === 0;
}
}
await updateAgentMetadata({ history, isComplete });
} catch (metadataErr) { } catch (metadataErr) {
// Non-critical: log but don't fail the agent execution
logger.debug( logger.debug(
"Failed to update metadata after turn (non-critical)", "Failed to update metadata after turn (non-critical)",
metadataErr as any, metadataErr as any,
@@ -552,8 +522,7 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
} catch (e: any) { } catch (e: any) {
if (e.name === "AbortError") { if (e.name === "AbortError") {
logger.debug("Response interrupted"); logger.debug("Response interrupted");
// Remove any partial assistant message removePartialAssistantMessage(state.session.history);
removePartialAssistantMessage(state);
} else { } else {
logger.error(`Error: ${formatError(e)}`); logger.error(`Error: ${formatError(e)}`);
@@ -655,7 +624,3 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
}); });
}); });
} }
// Function moved to serve.helpers.ts - remove implementation
// async function streamChatResponseWithInterruption - moved to helpers {
// Implementation moved to serve.helpers.ts

View File

@@ -1,5 +1,7 @@
import type { ChatHistoryItem } from "core/index.js"; import type { ChatHistoryItem } from "core/index.js";
import { env } from "../env.js";
import { import {
post, post,
ApiRequestError, ApiRequestError,
@@ -124,6 +126,9 @@ export async function postAgentMetadata(
agentId: string, agentId: string,
metadata: Record<string, any>, metadata: Record<string, any>,
): Promise<void> { ): Promise<void> {
const endpoint = `agents/${agentId}/metadata`;
const fullUrl = new URL(endpoint, env.apiBase).toString();
if (!agentId) { if (!agentId) {
logger.debug("No agent ID provided, skipping metadata update"); logger.debug("No agent ID provided, skipping metadata update");
return; return;
@@ -134,36 +139,44 @@ export async function postAgentMetadata(
return; return;
} }
try { const startTime = Date.now();
logger.debug("Posting metadata to control plane", { logger.info(`[metadata] POST ${fullUrl}`, {
agentId, agentId,
metadata, metadataKeys: Object.keys(metadata),
}); });
logger.info("[metadata] Request body", { metadata });
const response = await post(`agents/${agentId}/metadata`, { metadata }); try {
const response = await post(endpoint, { metadata });
const duration = Date.now() - startTime;
if (response.ok) { if (response.ok) {
logger.info("Successfully posted metadata to control plane"); logger.info(
`[metadata] Success: ${response.status} (${duration}ms) ${fullUrl}`,
);
} else { } else {
logger.warn( logger.warn(
`Unexpected response when posting metadata: ${response.status}`, `[metadata] Unexpected response: ${response.status} (${duration}ms) ${fullUrl}`,
); );
} }
} catch (error) { } catch (error) {
const duration = Date.now() - startTime;
// Non-critical: Log but don't fail the entire agent execution // Non-critical: Log but don't fail the entire agent execution
if (error instanceof AuthenticationRequiredError) { if (error instanceof AuthenticationRequiredError) {
logger.debug( logger.info(
"Authentication required for metadata update (skipping)", `[metadata] Auth required (skipping) (${duration}ms) ${fullUrl}`,
error.message,
); );
} else if (error instanceof ApiRequestError) { } else if (error instanceof ApiRequestError) {
logger.warn( logger.warn(
`Failed to post metadata: ${error.status} ${error.response || error.statusText}`, `[metadata] Failed: ${error.status} ${error.statusText} (${duration}ms) ${fullUrl}`,
{ response: error.response },
); );
} else { } else {
const errorMessage = const errorMessage =
error instanceof Error ? error.message : String(error); error instanceof Error ? error.message : String(error);
logger.warn(`Error posting metadata: ${errorMessage}`); logger.warn(
`[metadata] Error: ${errorMessage} (${duration}ms) ${fullUrl}`,
);
} }
} }
} }