diff --git a/internal/cloud/code-agent-agentrun-adapter.ts b/internal/cloud/code-agent-agentrun-adapter.ts index 68736500..a910f228 100644 --- a/internal/cloud/code-agent-agentrun-adapter.ts +++ b/internal/cloud/code-agent-agentrun-adapter.ts @@ -1,4 +1,4 @@ -import { randomUUID } from "node:crypto"; +import { createHash, randomUUID } from "node:crypto"; import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts"; import { decorateCodeAgentSession } from "./code-agent-session-lifecycle.ts"; @@ -215,6 +215,7 @@ export function initialAgentRunChatResult({ params = {}, options = {}, traceId } const env = options.env ?? process.env; const timestamp = nowIso(options.now); const backendProfile = resolveAgentRunBackendProfile(env, params); + const promptEvidence = agentRunPromptMetadataFromParams(params, traceId); return { accepted: true, status: "running", @@ -234,6 +235,7 @@ export function initialAgentRunChatResult({ params = {}, options = {}, traceId } sessionMode: AGENTRUN_SESSION_MODE, implementationType: AGENTRUN_IMPLEMENTATION_TYPE, threadContinuityPolicy: THREAD_CONTINUITY_POLICY, + ...agentRunPromptFields(promptEvidence), agentRun: { adapter: ADAPTER_ID, status: "pending-dispatch", @@ -257,6 +259,7 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI const startedAt = nowIso(options.now); const ownerApiKey = await resolveOwnerApiKey({ params, options, now: () => nowIso(options.now) }); const toolCapabilities = await resolveToolCapabilities({ params, options }); + const promptEvidence = agentRunPromptMetadataFromParams(params, traceId); traceStore.ensure(traceId, agentRunTraceMeta(env, params)); traceStore.append(traceId, { @@ -267,6 +270,7 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI waitingFor: "agentrun-run-reuse-or-create", adapter: ADAPTER_ID, managerHost: new URL(managerUrl).hostname, + ...agentRunPromptEventFields(promptEvidence), valuesPrinted: false }, agentRunTraceMeta(env, params)); @@ -674,6 +678,7 @@ export async function loadPersistedAgentRunResult(traceId, options = {}) { if (!agentRun?.runId) return null; const backendProfile = firstNonEmpty(agentRun.backendProfile, "deepseek"); const status = firstNonEmpty(session?.status === "canceled" ? "canceled" : null, agentRun.terminalStatus, agentRun.commandState, agentRun.status, "running"); + const promptEvidence = agentRunPromptMetadataFromSession(session, safeId); return { accepted: true, status, @@ -683,6 +688,7 @@ export async function loadPersistedAgentRunResult(traceId, options = {}) { sessionId: safeSessionId(session?.id) || agentRun.sessionId || null, threadId: safeOpaqueId(session?.threadId) || agentRun.threadId || null, messageId: `msg_${safeId.slice(4)}`, + ...agentRunPromptFields(promptEvidence), createdAt: session?.startedAt ?? session?.updatedAt ?? nowIso(options.now), updatedAt: session?.updatedAt ?? nowIso(options.now), ownerUserId: session?.ownerUserId, @@ -879,6 +885,121 @@ function rejectRemovedResourceWorkspaceFiles(value) { throw adapterError("legacy_workspace_files_removed", "workspaceFiles/resourceWorkspaceFiles are removed; use AgentRun ResourceBundleRef kind=gitbundle with bundles[]"); } +function agentRunPromptMetadataFromParams(params = {}, traceId = null) { + return agentRunPromptMetadataFromText(firstNonEmpty(params.message, params.prompt, params.text), traceId, "hwlab-code-agent-request", { fullText: true }); +} + +function agentRunPromptMetadataFromSession(session = null, traceId = null) { + const snapshot = session?.session && typeof session.session === "object" ? session.session : null; + const fromTraceResult = agentRunPromptMetadataFromPayload(snapshot?.traceResults?.[traceId], traceId); + if (fromTraceResult) return fromTraceResult; + const messageText = agentRunPromptTextFromMessages(snapshot?.messages, traceId); + if (messageText) return agentRunPromptMetadataFromText(messageText, traceId, "agent-session-owner-message", { fullText: true }); + const preview = firstNonEmpty(snapshot?.firstUserMessagePreview); + return preview ? agentRunPromptMetadataFromText(preview, traceId, "agent-session-first-user-preview", { fullText: false }) : null; +} + +function agentRunPromptMetadataFromPayload(payload = null, traceId = null) { + if (!payload || typeof payload !== "object") return null; + const prompt = payload.prompt && typeof payload.prompt === "object" ? payload.prompt : null; + const textValue = firstNonEmpty(prompt?.text, prompt?.summary, prompt?.preview, payload.promptSummary, payload.promptPreview); + const fromText = agentRunPromptMetadataFromText(textValue, traceId, prompt?.source ?? payload.promptSource ?? "agentrun-prompt-metadata", { fullText: Boolean(prompt?.text) }); + return agentRunPromptMetadataFromParts({ + traceId, + source: prompt?.source ?? payload.promptSource ?? fromText?.prompt?.source ?? "agentrun-prompt-metadata", + promptId: firstNonEmpty(payload.promptId, prompt?.id, fromText?.promptId), + promptSha256: firstNonEmpty(payload.promptSha256, prompt?.sha256, fromText?.promptSha256), + promptSummary: firstNonEmpty(payload.promptSummary, prompt?.summary, fromText?.promptSummary), + promptPreview: firstNonEmpty(payload.promptPreview, prompt?.preview, fromText?.promptPreview), + textChars: Number.isInteger(prompt?.textChars) ? prompt.textChars : fromText?.prompt?.textChars + }); +} + +function agentRunPromptTextFromMessages(messages = [], traceId = null) { + const list = Array.isArray(messages) ? messages : []; + const resolvedTraceId = safeTraceId(traceId); + const exact = list.find((message) => String(message?.role ?? "").toLowerCase() === "user" && (!resolvedTraceId || safeTraceId(message?.traceId) === resolvedTraceId)); + const fallback = list.find((message) => String(message?.role ?? "").toLowerCase() === "user"); + return firstNonEmpty(exact?.text, exact?.content, fallback?.text, fallback?.content); +} + +function agentRunPromptMetadataFromText(value, traceId = null, source = "hwlab-code-agent-request", options = {}) { + const promptText = String(value ?? "").trim(); + if (!promptText) return null; + return agentRunPromptMetadataFromParts({ + traceId, + source, + promptSha256: options.fullText === true ? sha256Text(promptText) : undefined, + promptSummary: promptText, + promptPreview: promptText, + textChars: options.fullText === true ? promptText.length : undefined + }); +} + +function agentRunPromptMetadataFromParts({ traceId = null, source = "hwlab-code-agent-request", promptId = null, promptSha256 = null, promptSummary = null, promptPreview = null, textChars = undefined } = {}) { + const digest = firstNonEmpty(promptSha256); + const summary = clippedPromptText(firstNonEmpty(promptSummary, promptPreview), 240); + const preview = clippedPromptText(firstNonEmpty(promptPreview, promptSummary), 500); + const explicitId = firstNonEmpty(promptId); + if (!explicitId && !digest && !summary && !preview) return null; + const id = explicitId || agentRunPromptId(traceId, digest); + const prompt = compactObject({ + id, + sha256: digest || undefined, + summary, + preview, + textChars: Number.isInteger(textChars) ? textChars : undefined, + source, + valuesPrinted: false + }); + return compactObject({ + promptId: id, + promptSha256: digest || undefined, + promptSummary: summary, + promptPreview: preview, + prompt, + valuesPrinted: false + }) ?? null; +} + +function agentRunPromptFields(metadata = null) { + if (!metadata) return {}; + return compactObject({ + promptId: metadata.promptId, + promptSha256: metadata.promptSha256, + promptSummary: metadata.promptSummary, + promptPreview: metadata.promptPreview, + prompt: metadata.prompt + }) ?? {}; +} + +function agentRunPromptEventFields(metadata = null) { + if (!metadata) return {}; + return compactObject({ + promptId: metadata.promptId, + promptSha256: metadata.promptSha256, + promptSummary: metadata.promptSummary, + promptPreview: metadata.promptPreview + }) ?? {}; +} + +function agentRunPromptId(traceId = null, digest = null) { + const traceSuffix = safeTraceId(traceId)?.slice(4).replace(/[^A-Za-z0-9_.:-]/gu, "_"); + const digestSuffix = firstNonEmpty(digest)?.slice(0, 16); + const suffix = firstNonEmpty(traceSuffix, digestSuffix); + return suffix ? `prm_${suffix}` : undefined; +} + +function clippedPromptText(value, limit) { + const promptText = String(value ?? "").trim(); + if (!promptText) return undefined; + return promptText.length > limit ? `${promptText.slice(0, limit)}...` : promptText; +} + +function sha256Text(value) { + return createHash("sha256").update(String(value)).digest("hex"); +} + function adapterError(code, message, details = {}) { return Object.assign(new Error(message), { code, @@ -889,12 +1010,15 @@ function adapterError(code, message, details = {}) { function buildAgentRunCommandInput({ params, traceId, backendProfile, sessionId }) { const prompt = String(params.message ?? params.prompt ?? "").trim(); + const promptEvidence = agentRunPromptMetadataFromText(prompt, traceId, "agentrun-command-payload", { fullText: true }); const threadId = safeOpaqueId(params.threadId); return { type: "turn", payload: { prompt, message: prompt, + ...agentRunPromptEventFields(promptEvidence), + ...(promptEvidence?.prompt ? { promptMetadata: promptEvidence.prompt } : {}), traceId, projectId: firstNonEmpty(params.projectId) || null, conversationId: safeConversationId(params.conversationId) || null, @@ -913,12 +1037,15 @@ function buildAgentRunCommandInput({ params, traceId, backendProfile, sessionId function buildAgentRunSteerCommandInput({ params, traceId, steerTraceId, mapped }) { const prompt = String(params.message ?? params.prompt ?? params.text ?? "").trim(); + const promptEvidence = agentRunPromptMetadataFromText(prompt, steerTraceId, "agentrun-steer-command-payload", { fullText: true }); return { type: "steer", payload: { prompt, message: prompt, text: prompt, + ...agentRunPromptEventFields(promptEvidence), + ...(promptEvidence?.prompt ? { promptMetadata: promptEvidence.prompt } : {}), traceId: steerTraceId, targetTraceId: traceId, targetCommandId: mapped.agentRun.commandId, diff --git a/internal/cloud/code-agent-trace-store.ts b/internal/cloud/code-agent-trace-store.ts index 80125045..b6a70f14 100644 --- a/internal/cloud/code-agent-trace-store.ts +++ b/internal/cloud/code-agent-trace-store.ts @@ -346,7 +346,10 @@ function normalizeTraceEvent(event, { traceId, seq, now, fallbackRunnerKind } = exitCode: Number.isInteger(event.exitCode) ? event.exitCode : undefined, durationMs: typeof event.durationMs === "number" ? Math.max(0, Math.trunc(event.durationMs)) : undefined, outputBytes: typeof event.outputBytes === "number" ? Math.max(0, Math.trunc(event.outputBytes)) : undefined, + promptId: safeText(event.promptId, 180), + promptSha256: safeText(event.promptSha256, 80), promptSummary: safeText(event.promptSummary, 240), + promptPreview: safeText(event.promptPreview, 500), outputSummary: safeText(event.outputSummary, 400), stdoutSummary: safeText(event.stdoutSummary, 600), stderrSummary: safeText(event.stderrSummary, 600), diff --git a/internal/cloud/server-code-agent-http.ts b/internal/cloud/server-code-agent-http.ts index 471ca46a..069a598b 100644 --- a/internal/cloud/server-code-agent-http.ts +++ b/internal/cloud/server-code-agent-http.ts @@ -1,5 +1,5 @@ -import { randomUUID } from "node:crypto"; +import { createHash, randomUUID } from "node:crypto"; import { M3_IO_CONTROL_ROUTE, M3_STATUS_ROUTE, describeM3StatusLive, handleM3IoControl } from "./m3-io-control.ts"; import { @@ -1803,6 +1803,7 @@ function codeAgentSessionOwnerEvidence(payload = {}, params = {}) { const traceSummary = codeAgentTraceSummaryEvidence(payload, traceId, finalResponse); const messages = codeAgentConversationMessagesEvidence(payload, params, traceId, finalResponse); const firstUserMessagePreview = messages.find((message) => message.role === "user")?.text?.slice(0, 240) ?? null; + const promptEvidence = codeAgentPromptMetadata(payload, params, traceId); const traceResult = codeAgentTraceResultEvidence(payload, params, traceId, finalResponse, traceSummary); return { provider: payload.provider ?? null, @@ -1817,6 +1818,7 @@ function codeAgentSessionOwnerEvidence(payload = {}, params = {}) { threadId: payload.session?.threadId ?? payload.sessionReuse?.threadId ?? payload.threadId ?? params.threadId ?? null, traceId, projectId: payload.projectId ?? params.projectId ?? null, + ...codeAgentPromptFields(promptEvidence), finalResponse, traceSummary, ...(traceResult ? { traceResults: { [traceResult.traceId]: traceResult } } : {}), @@ -1838,6 +1840,7 @@ function codeAgentTraceResultEvidence(payload = {}, params = {}, traceId = null, const conversationId = safeConversationId(payload.conversationId ?? agentRun?.conversationId ?? resultSession?.conversationId ?? sessionReuse?.conversationId ?? params.conversationId) || null; const sessionId = safeSessionId(payload.sessionId ?? resultSession?.sessionId ?? sessionReuse?.sessionId ?? params.sessionId) || null; const threadId = safeOpaqueId(payload.threadId ?? resultSession?.threadId ?? sessionReuse?.threadId ?? params.threadId ?? agentRun?.threadId) || null; + const promptEvidence = codeAgentPromptMetadata(payload, params, resolvedTraceId); if (!agentRun && !finalResponse && !traceSummary) return null; return { traceId: resolvedTraceId, @@ -1845,6 +1848,7 @@ function codeAgentTraceResultEvidence(payload = {}, params = {}, traceId = null, conversationId, sessionId, threadId, + ...codeAgentPromptFields(promptEvidence), messageId: finalResponse?.messageId ?? payload.messageId ?? null, createdAt: payload.createdAt ?? finalResponse?.createdAt ?? null, updatedAt: payload.updatedAt ?? finalResponse?.updatedAt ?? null, @@ -1857,6 +1861,105 @@ function codeAgentTraceResultEvidence(payload = {}, params = {}, traceId = null, }; } +function codeAgentPromptMetadata(payload = {}, params = {}, traceId = null) { + const prompt = payload?.prompt && typeof payload.prompt === "object" ? payload.prompt : null; + const rawPrompt = firstNonEmptyValue( + params.message, + params.prompt, + params.text, + payload.userMessage, + typeof payload.prompt === "string" ? payload.prompt : null + ); + const fromText = codeAgentPromptMetadataFromText(rawPrompt, traceId, "code-agent-request", { fullText: Boolean(rawPrompt) }); + return codeAgentPromptMetadataFromParts({ + traceId, + source: prompt?.source ?? payload.promptSource ?? fromText?.prompt?.source ?? "code-agent-request", + promptId: firstNonEmptyValue(payload.promptId, prompt?.id, fromText?.promptId), + promptSha256: firstNonEmptyValue(payload.promptSha256, prompt?.sha256, fromText?.promptSha256), + promptSummary: firstNonEmptyValue(payload.promptSummary, prompt?.summary, fromText?.promptSummary), + promptPreview: firstNonEmptyValue(payload.promptPreview, prompt?.preview, fromText?.promptPreview), + textChars: Number.isInteger(prompt?.textChars) ? prompt.textChars : fromText?.prompt?.textChars + }); +} + +function codeAgentPromptMetadataFromText(value, traceId = null, source = "code-agent-request", options = {}) { + const promptText = String(value ?? "").trim(); + if (!promptText) return null; + return codeAgentPromptMetadataFromParts({ + traceId, + source, + promptSha256: options.fullText === true ? sha256Text(promptText) : undefined, + promptSummary: promptText, + promptPreview: promptText, + textChars: options.fullText === true ? promptText.length : undefined + }); +} + +function codeAgentPromptMetadataFromParts({ traceId = null, source = "code-agent-request", promptId = null, promptSha256 = null, promptSummary = null, promptPreview = null, textChars = undefined } = {}) { + const digest = firstNonEmptyValue(promptSha256); + const summary = clippedPromptText(firstNonEmptyValue(promptSummary, promptPreview), 240); + const preview = clippedPromptText(firstNonEmptyValue(promptPreview, promptSummary), 500); + const explicitId = firstNonEmptyValue(promptId); + if (!explicitId && !digest && !summary && !preview) return null; + const id = explicitId || codeAgentPromptId(traceId, digest); + const prompt = compactCodeAgentObject({ + id, + sha256: digest || undefined, + summary, + preview, + textChars: Number.isInteger(textChars) ? textChars : undefined, + source, + valuesPrinted: false + }); + return compactCodeAgentObject({ + promptId: id, + promptSha256: digest || undefined, + promptSummary: summary, + promptPreview: preview, + prompt, + valuesPrinted: false + }) ?? null; +} + +function codeAgentPromptFields(metadata = null) { + if (!metadata) return {}; + return compactCodeAgentObject({ + promptId: metadata.promptId, + promptSha256: metadata.promptSha256, + promptSummary: metadata.promptSummary, + promptPreview: metadata.promptPreview, + prompt: metadata.prompt + }) ?? {}; +} + +function codeAgentPromptId(traceId = null, digest = null) { + const traceSuffix = safeTraceId(traceId)?.slice(4).replace(/[^A-Za-z0-9_.:-]/gu, "_"); + const digestSuffix = firstNonEmptyValue(digest)?.slice(0, 16); + const suffix = firstNonEmptyValue(traceSuffix, digestSuffix); + return suffix ? `prm_${suffix}` : undefined; +} + +function clippedPromptText(value, limit) { + const promptText = String(value ?? "").trim(); + if (!promptText) return undefined; + return promptText.length > limit ? `${promptText.slice(0, limit)}...` : promptText; +} + +function sha256Text(value) { + return createHash("sha256").update(String(value)).digest("hex"); +} + +function compactCodeAgentObject(value) { + const result = {}; + for (const [key, item] of Object.entries(value ?? {})) { + if (item === undefined || item === null) continue; + if (Array.isArray(item) && item.length === 0) continue; + if (typeof item === "object" && !Array.isArray(item) && Object.keys(item).length === 0) continue; + result[key] = item; + } + return Object.keys(result).length > 0 ? result : undefined; +} + function codeAgentBillingReservationEvidence(value = null) { if (!value || typeof value !== "object") return null; const reservationId = textValue(value.reservationId); @@ -2212,6 +2315,7 @@ function traceSnapshotWithTerminalEvidence(snapshot, result, traceId, refreshErr conversationId: evidence.conversationId, sessionId: evidence.sessionId, threadId: evidence.threadId, + ...codeAgentPromptFields(evidence), agentRun: evidence.agentRun, finalResponse: evidence.finalResponse, traceSummary: evidence.traceSummary, @@ -2228,6 +2332,7 @@ function traceSnapshotWithTerminalEvidence(snapshot, result, traceId, refreshErr conversationId: evidence.conversationId, sessionId: evidence.sessionId, threadId: evidence.threadId, + ...codeAgentPromptFields(evidence), agentRun: evidence.agentRun, traceSummary: evidence.traceSummary, finalResponse: evidence.finalResponse, @@ -2243,6 +2348,7 @@ function traceSnapshotWithAttachedTerminalEvidence(snapshot, evidence, refreshEr conversationId: snapshot.conversationId ?? evidence.conversationId, sessionId: snapshot.sessionId ?? evidence.sessionId, threadId: snapshot.threadId ?? evidence.threadId, + ...codeAgentPromptFields(evidence), agentRun: snapshot.agentRun ?? evidence.agentRun, finalResponse: snapshot.finalResponse ?? evidence.finalResponse, traceSummary: snapshot.traceSummary ?? evidence.traceSummary, @@ -2265,6 +2371,7 @@ function agentRunTerminalTraceEvidence(result, traceId) { if (!result || typeof result !== "object") return null; const storedSummary = result.traceSummary && typeof result.traceSummary === "object" && result.traceSummary.source === "agentrun-command-result" ? result.traceSummary : null; const finalResponse = agentRunTerminalFinalResponse(result, traceId); + const promptEvidence = codeAgentPromptMetadata(result, {}, traceId); const agentRun = result.agentRun && typeof result.agentRun === "object" ? { adapter: result.agentRun.adapter ?? null, runId: result.agentRun.runId ?? null, @@ -2294,6 +2401,7 @@ function agentRunTerminalTraceEvidence(result, traceId) { conversationId: safeConversationId(result.conversationId) || null, sessionId: safeSessionId(result.sessionId) || null, threadId: safeOpaqueId(result.threadId) || null, + ...codeAgentPromptFields(promptEvidence), agentRun, finalResponse, traceSummary @@ -2405,6 +2513,7 @@ function terminalEvidencePayload(evidence) { conversationId: evidence.conversationId, sessionId: evidence.sessionId, threadId: evidence.threadId, + ...codeAgentPromptFields(evidence), agentRun: evidence.agentRun, traceSummary: evidence.traceSummary, finalResponse: evidence.finalResponse,