Merge pull request #116 from pikasTech/fix/issue-113-command-result-summary

修复 command-result 长 trace 摘要过期
This commit is contained in:
Lyon
2026-06-09 21:43:48 +08:00
committed by GitHub
2 changed files with 112 additions and 18 deletions
+72 -16
View File
@@ -6,10 +6,30 @@ const maxToolCallSummaryItems = 40;
const toolCallCommandLimitChars = 600; const toolCallCommandLimitChars = 600;
const toolCallFieldLimitChars = 200; 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<JsonRecord> { export async function buildRunResult(store: AgentRunStore, runId: string, commandId?: string): Promise<JsonRecord> {
const run = await store.getRun(runId); const run = await store.getRun(runId);
const command = await selectCommand(store, runId, commandId); 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 scopedEvents = command ? eventsForCommand(events, command.id) : events;
const jobs = await store.listRunnerJobs(runId, command?.id); const jobs = await store.listRunnerJobs(runId, command?.id);
const latestJob = jobs.at(-1) ?? null; const latestJob = jobs.at(-1) ?? null;
@@ -34,12 +54,29 @@ export async function buildRunResult(store: AgentRunStore, runId: string, comman
terminalStatus: terminal, terminalStatus: terminal,
terminalSource, terminalSource,
completed: terminal === "completed", 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, failureKind,
failureMessage, failureMessage,
blocker, blocker,
lastSeq: events.at(-1)?.seq ?? 0, lastSeq: events.at(-1)?.seq ?? 0,
eventCount: events.length, eventCount: events.length,
eventsCapped: eventPage.capped,
nextAfterSeq: eventPage.nextAfterSeq,
scopedEventCount: scopedEvents.length,
scopedLastSeq: scopedEvents.at(-1)?.seq ?? 0,
artifactSummary: artifactSummary(scopedEvents), artifactSummary: artifactSummary(scopedEvents),
toolCallSummary: toolCallSummary(scopedEvents), toolCallSummary: toolCallSummary(scopedEvents),
sessionRef: sessionSummary(run), sessionRef: sessionSummary(run),
@@ -48,6 +85,21 @@ export async function buildRunResult(store: AgentRunStore, runId: string, comman
}; };
} }
async function listResultEvents(store: AgentRunStore, runId: string): Promise<ResultEventPage> {
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<CommandRecord | null> { async function selectCommand(store: AgentRunStore, runId: string, commandId?: string): Promise<CommandRecord | null> {
if (commandId) { if (commandId) {
const command = await store.getCommand(commandId); const command = await store.getCommand(commandId);
@@ -94,21 +146,25 @@ function messageFromEvents(events: RunEvent[]): string | null {
return null; return null;
} }
function assistantReply(events: RunEvent[]): string { function assistantReply(events: RunEvent[]): AssistantReplySummary {
const assistantEvents = events.filter((event) => event.type === "assistant_message"); const assistantEvents = events.filter((event) => event.type === "assistant_message");
const authoritative = assistantEvents const latestAuthoritative = [...assistantEvents].reverse().find((event) => (event.payload.replyAuthority === true || event.payload.final === true) && textPayload(event.payload).length > 0);
.filter((event) => event.payload.replyAuthority === true || event.payload.final === true) if (latestAuthoritative) return assistantReplySummary(latestAuthoritative);
.map((event) => textPayload(event.payload)) const latestAssistant = [...assistantEvents].reverse().find((event) => textPayload(event.payload).length > 0);
.filter((text) => text.length > 0); if (latestAssistant) return assistantReplySummary(latestAssistant);
const latestAuthoritative = authoritative.at(-1); return { text: "", seq: null, source: null, final: false, replyAuthority: false, textTruncated: false, outputTruncated: false };
if (latestAuthoritative) return latestAuthoritative; }
const completedAgentMessages = assistantEvents
.filter((event) => event.payload.source === "completed-agent-message") function assistantReplySummary(event: RunEvent): AssistantReplySummary {
.map((event) => textPayload(event.payload)) return {
.filter((text) => text.length > 0); text: textPayload(event.payload),
const latestCompletedAgentMessage = completedAgentMessages.at(-1); seq: event.seq,
if (latestCompletedAgentMessage) return latestCompletedAgentMessage; source: typeof event.payload.source === "string" ? event.payload.source : null,
return assistantEvents.map((event) => textPayload(event.payload)).filter((text) => text.length > 0).join(""); 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 { function textPayload(payload: JsonRecord): string {
+40 -2
View File
@@ -3,9 +3,11 @@ import { startManagerServer } from "../../mgr/server.js";
import { MemoryAgentRunStore } from "../../mgr/store.js"; import { MemoryAgentRunStore } from "../../mgr/store.js";
import { ManagerClient } from "../../mgr/client.js"; import { ManagerClient } from "../../mgr/client.js";
import type { SelfTestCase } from "../harness.js"; import type { SelfTestCase } from "../harness.js";
import type { JsonRecord } from "../../common/types.js";
const selfTest: SelfTestCase = async () => { 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 { try {
const client = new ManagerClient(server.baseUrl); 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 } }; 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?.migrationReady, true);
assert.equal(health.database?.failureKind, null); assert.equal(health.database?.failureKind, null);
assert.equal(health.secretRefs?.valuesPrinted, false); 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 { } finally {
await new Promise<void>((resolve) => server.server.close(() => resolve())); await new Promise<void>((resolve) => server.server.close(() => resolve()));
} }
}; };
async function assertLongResultUsesTerminalAssistant(client: ManagerClient, store: MemoryAgentRunStore): Promise<void> {
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; export default selfTest;