diff --git a/src/mgr/result.ts b/src/mgr/result.ts index 54cba7f..1525f15 100644 --- a/src/mgr/result.ts +++ b/src/mgr/result.ts @@ -6,10 +6,30 @@ const maxToolCallSummaryItems = 40; const toolCallCommandLimitChars = 600; const toolCallFieldLimitChars = 200; +const RESULT_EVENT_PAGE_LIMIT = 500; +const RESULT_EVENT_MAX_PAGES = 200; + +interface ResultEventPage { + events: RunEvent[]; + capped: boolean; + nextAfterSeq: number | null; +} + +interface AssistantReplySummary { + text: string; + seq: number | null; + source: string | null; + final: boolean; + replyAuthority: boolean; + textTruncated: boolean; + outputTruncated: boolean; +} + export async function buildRunResult(store: AgentRunStore, runId: string, commandId?: string): Promise { const run = await store.getRun(runId); const command = await selectCommand(store, runId, commandId); - const events = await store.listEvents(runId, 0, 500); + const eventPage = await listResultEvents(store, runId); + const events = eventPage.events; const scopedEvents = command ? eventsForCommand(events, command.id) : events; const jobs = await store.listRunnerJobs(runId, command?.id); const latestJob = jobs.at(-1) ?? null; @@ -34,12 +54,29 @@ export async function buildRunResult(store: AgentRunStore, runId: string, comman terminalStatus: terminal, terminalSource, completed: terminal === "completed", - reply, + reply: reply.text, + finalResponse: { + text: reply.text, + seq: reply.seq, + source: reply.source, + final: reply.final, + replyAuthority: reply.replyAuthority, + textTruncated: reply.textTruncated, + outputTruncated: reply.outputTruncated, + }, + finalAssistantSeq: reply.seq, + finalAssistantSource: reply.source, + finalAssistantTextTruncated: reply.textTruncated, + finalAssistantOutputTruncated: reply.outputTruncated, failureKind, failureMessage, blocker, lastSeq: events.at(-1)?.seq ?? 0, eventCount: events.length, + eventsCapped: eventPage.capped, + nextAfterSeq: eventPage.nextAfterSeq, + scopedEventCount: scopedEvents.length, + scopedLastSeq: scopedEvents.at(-1)?.seq ?? 0, artifactSummary: artifactSummary(scopedEvents), toolCallSummary: toolCallSummary(scopedEvents), sessionRef: sessionSummary(run), @@ -48,6 +85,21 @@ export async function buildRunResult(store: AgentRunStore, runId: string, comman }; } +async function listResultEvents(store: AgentRunStore, runId: string): Promise { + const events: RunEvent[] = []; + let afterSeq = 0; + for (let pageIndex = 0; pageIndex < RESULT_EVENT_MAX_PAGES; pageIndex += 1) { + const page = await store.listEvents(runId, afterSeq, RESULT_EVENT_PAGE_LIMIT); + if (page.length === 0) return { events, capped: false, nextAfterSeq: null }; + events.push(...page); + const nextAfterSeq = page.at(-1)?.seq ?? afterSeq; + if (nextAfterSeq <= afterSeq) return { events, capped: true, nextAfterSeq: afterSeq }; + afterSeq = nextAfterSeq; + if (page.length < RESULT_EVENT_PAGE_LIMIT) return { events, capped: false, nextAfterSeq: null }; + } + return { events, capped: true, nextAfterSeq: events.at(-1)?.seq ?? afterSeq }; +} + async function selectCommand(store: AgentRunStore, runId: string, commandId?: string): Promise { if (commandId) { const command = await store.getCommand(commandId); @@ -94,21 +146,25 @@ function messageFromEvents(events: RunEvent[]): string | null { return null; } -function assistantReply(events: RunEvent[]): string { +function assistantReply(events: RunEvent[]): AssistantReplySummary { const assistantEvents = events.filter((event) => event.type === "assistant_message"); - const authoritative = assistantEvents - .filter((event) => event.payload.replyAuthority === true || event.payload.final === true) - .map((event) => textPayload(event.payload)) - .filter((text) => text.length > 0); - const latestAuthoritative = authoritative.at(-1); - if (latestAuthoritative) return latestAuthoritative; - const completedAgentMessages = assistantEvents - .filter((event) => event.payload.source === "completed-agent-message") - .map((event) => textPayload(event.payload)) - .filter((text) => text.length > 0); - const latestCompletedAgentMessage = completedAgentMessages.at(-1); - if (latestCompletedAgentMessage) return latestCompletedAgentMessage; - return assistantEvents.map((event) => textPayload(event.payload)).filter((text) => text.length > 0).join(""); + const latestAuthoritative = [...assistantEvents].reverse().find((event) => (event.payload.replyAuthority === true || event.payload.final === true) && textPayload(event.payload).length > 0); + if (latestAuthoritative) return assistantReplySummary(latestAuthoritative); + const latestAssistant = [...assistantEvents].reverse().find((event) => textPayload(event.payload).length > 0); + if (latestAssistant) return assistantReplySummary(latestAssistant); + return { text: "", seq: null, source: null, final: false, replyAuthority: false, textTruncated: false, outputTruncated: false }; +} + +function assistantReplySummary(event: RunEvent): AssistantReplySummary { + return { + text: textPayload(event.payload), + seq: event.seq, + source: typeof event.payload.source === "string" ? event.payload.source : null, + final: event.payload.final === true, + replyAuthority: event.payload.replyAuthority === true, + textTruncated: event.payload.textTruncated === true, + outputTruncated: event.payload.outputTruncated === true, + }; } function textPayload(payload: JsonRecord): string { diff --git a/src/selftest/cases/10-manager-memory.ts b/src/selftest/cases/10-manager-memory.ts index 935640b..ec01228 100644 --- a/src/selftest/cases/10-manager-memory.ts +++ b/src/selftest/cases/10-manager-memory.ts @@ -3,9 +3,11 @@ import { startManagerServer } from "../../mgr/server.js"; import { MemoryAgentRunStore } from "../../mgr/store.js"; import { ManagerClient } from "../../mgr/client.js"; import type { SelfTestCase } from "../harness.js"; +import type { JsonRecord } from "../../common/types.js"; const selfTest: SelfTestCase = async () => { - const server = await startManagerServer({ port: 0, host: "127.0.0.1", sourceCommit: "self-test", store: new MemoryAgentRunStore() }); + const store = new MemoryAgentRunStore(); + const server = await startManagerServer({ port: 0, host: "127.0.0.1", sourceCommit: "self-test", store }); try { const client = new ManagerClient(server.baseUrl); const health = await client.get("/health/readiness") as { database?: { adapter?: string; reachable?: boolean; migrationReady?: boolean; failureKind?: string | null }; secretRefs?: { valuesPrinted?: boolean } }; @@ -14,10 +16,46 @@ const selfTest: SelfTestCase = async () => { assert.equal(health.database?.migrationReady, true); assert.equal(health.database?.failureKind, null); assert.equal(health.secretRefs?.valuesPrinted, false); - return { name: "manager-memory", tests: ["manager-memory-lifecycle"] }; + await assertLongResultUsesTerminalAssistant(client, store); + return { name: "manager-memory", tests: ["manager-memory-lifecycle", "manager-result-long-trace"] }; } finally { await new Promise((resolve) => server.server.close(() => resolve())); } }; +async function assertLongResultUsesTerminalAssistant(client: ManagerClient, store: MemoryAgentRunStore): Promise { + const run = store.createRun({ + tenantId: "unidesk", + projectId: "pikasTech/agentrun", + workspaceRef: { kind: "host-path", path: "/tmp/agentrun-selftest" }, + providerId: "G14", + backendProfile: "codex", + executionPolicy: { + sandbox: "workspace-write", + approval: "never", + timeoutMs: 1000, + network: "none", + secretScope: { allowCredentialEcho: false, providerCredentials: [] }, + }, + traceSink: null, + }); + const command = store.createCommand(run.id, { type: "turn", payload: { prompt: "exercise long result" }, idempotencyKey: "manager-result-long-trace" }); + store.appendEvent(run.id, "assistant_message", { commandId: command.id, text: "early assistant", source: "completed-agent-message" }); + for (let index = 0; index < 520; index += 1) { + store.appendEvent(run.id, "command_output", { commandId: command.id, stream: "stdout", text: `noise-${index}` }); + } + const finalAssistant = store.appendEvent(run.id, "assistant_message", { commandId: command.id, text: "final terminal assistant", source: "agent-message-delta-progress", outputTruncated: false }); + store.finishCommand(command.id, { terminalStatus: "completed", failureKind: null, failureMessage: null, threadId: "thread_long_result", turnId: "turn_long_result" }); + const result = await client.get(`/api/v1/runs/${encodeURIComponent(run.id)}/commands/${encodeURIComponent(command.id)}/result`) as JsonRecord; + assert.equal(result.reply, "final terminal assistant"); + assert.equal(result.finalAssistantSeq, finalAssistant.seq); + assert.equal(result.finalAssistantSource, "agent-message-delta-progress"); + assert.equal(result.lastSeq, finalAssistant.seq + 1); + assert.equal(result.eventCount, finalAssistant.seq + 1); + assert.equal(result.eventsCapped, false); + const finalResponse = result.finalResponse as JsonRecord; + assert.equal(finalResponse.text, "final terminal assistant"); + assert.equal(finalResponse.seq, finalAssistant.seq); +} + export default selfTest;