




























@@ -3,12 +3,14 @@ import type { AssistantMessage, Usage } from "@mariozechner/pi-ai";
33import { SessionManager } from "@mariozechner/pi-coding-agent";
44import {
55formatErrorMessage,
6+formatToolProgressOutput,
67inferToolMetaFromArgs,
78normalizeUsage,
89runAgentHarnessAfterCompactionHook,
910runAgentHarnessBeforeCompactionHook,
1011type EmbeddedRunAttemptParams,
1112type EmbeddedRunAttemptResult,
13+TOOL_PROGRESS_OUTPUT_MAX_CHARS,
1214formatToolAggregate,
1315type MessagingToolSend,
1416} from "openclaw/plugin-sdk/agent-harness-runtime";
@@ -56,6 +58,8 @@ const CURRENT_TOKEN_USAGE_KEYS = [
5658"last_token_usage",
5759] as const;
586061+const MAX_TOOL_OUTPUT_DELTA_MESSAGES_PER_ITEM = 20;
62+5963export class CodexAppServerEventProjector {
6064private readonly assistantTextByItem = new Map<string, string>();
6165private readonly assistantItemOrder: string[] = [];
@@ -66,6 +70,11 @@ export class CodexAppServerEventProjector {
6670private readonly activeCompactionItemIds = new Set<string>();
6771private readonly toolResultSummaryItemIds = new Set<string>();
6872private readonly toolResultOutputItemIds = new Set<string>();
73+private readonly toolResultOutputStreamedItemIds = new Set<string>();
74+private readonly toolResultOutputDeltaState = new Map<
75+string,
76+{ chars: number; messages: number; truncated: boolean }
77+>();
6978private readonly toolMetas = new Map<string, { toolName: string; meta?: string }>();
7079private assistantStarted = false;
7180private reasoningStarted = false;
@@ -489,10 +498,44 @@ export class CodexAppServerEventProjector {
489498if (!itemId || !delta || !this.shouldEmitToolOutput()) {
490499return;
491500}
501+const state = this.toolResultOutputDeltaState.get(itemId) ?? {
502+chars: 0,
503+messages: 0,
504+truncated: false,
505+};
506+if (state.truncated) {
507+return;
508+}
509+const remainingChars = Math.max(0, TOOL_PROGRESS_OUTPUT_MAX_CHARS - state.chars);
510+const remainingMessages = Math.max(0, MAX_TOOL_OUTPUT_DELTA_MESSAGES_PER_ITEM - state.messages);
511+if (remainingChars === 0 || remainingMessages === 0) {
512+state.truncated = true;
513+this.toolResultOutputDeltaState.set(itemId, state);
514+this.emitToolResultMessage({
515+ itemId,
516+text: formatToolOutput(toolName, undefined, "(output truncated)"),
517+});
518+return;
519+}
520+const chunk = delta.length > remainingChars ? delta.slice(0, remainingChars) : delta;
521+state.chars += chunk.length;
522+state.messages += 1;
523+const reachedLimit =
524+delta.length > remainingChars ||
525+state.chars >= TOOL_PROGRESS_OUTPUT_MAX_CHARS ||
526+state.messages >= MAX_TOOL_OUTPUT_DELTA_MESSAGES_PER_ITEM;
527+if (reachedLimit) {
528+state.truncated = true;
529+}
530+this.toolResultOutputDeltaState.set(itemId, state);
531+this.toolResultOutputStreamedItemIds.add(itemId);
492532this.emitToolResultMessage({
493533 itemId,
494-text: formatToolOutput(toolName, undefined, delta),
495-output: true,
534+text: formatToolOutput(
535+toolName,
536+undefined,
537+reachedLimit ? `${chunk}\n...(truncated)...` : chunk,
538+),
496539});
497540}
498541@@ -588,6 +631,9 @@ export class CodexAppServerEventProjector {
588631if (this.toolResultOutputItemIds.has(itemId)) {
589632return;
590633}
634+if (this.toolResultOutputStreamedItemIds.has(itemId)) {
635+return;
636+}
591637const toolName = itemName(item);
592638const output = itemOutputText(item);
593639if (!toolName || !output) {
@@ -596,12 +642,16 @@ export class CodexAppServerEventProjector {
596642this.emitToolResultMessage({
597643 itemId,
598644text: formatToolOutput(toolName, itemMeta(item), output),
599-output: true,
645+finalOutput: true,
600646});
601647}
602648603-private emitToolResultMessage(params: { itemId: string; text: string; output?: boolean }): void {
604-if (params.output) {
649+private emitToolResultMessage(params: {
650+itemId: string;
651+text: string;
652+finalOutput?: boolean;
653+}): void {
654+if (params.finalOutput) {
605655this.toolResultOutputItemIds.add(params.itemId);
606656}
607657try {
@@ -934,7 +984,10 @@ function itemName(item: CodexThreadItem): string | undefined {
934984935985function itemMeta(item: CodexThreadItem): string | undefined {
936986if (item.type === "commandExecution" && typeof item.command === "string") {
937-return item.command;
987+return inferToolMetaFromArgs("exec", {
988+command: item.command,
989+cwd: typeof item.cwd === "string" ? item.cwd : undefined,
990+});
938991}
939992if (item.type === "webSearch" && typeof item.query === "string") {
940993return item.query;
@@ -995,11 +1048,30 @@ function formatToolSummary(toolName: string, meta?: string): string {
9951048}
99610499971050function formatToolOutput(toolName: string, meta: string | undefined, output: string): string {
998-const trimmed = output.trim();
999-if (!trimmed) {
1051+const formattedOutput = formatToolProgressOutput(output);
1052+if (!formattedOutput) {
10001053return formatToolSummary(toolName, meta);
10011054}
1002-return `${formatToolSummary(toolName, meta)}\n\`\`\`txt\n${trimmed}\n\`\`\``;
1055+const fence = markdownFenceForText(formattedOutput);
1056+return `${formatToolSummary(toolName, meta)}\n${fence}txt\n${formattedOutput}\n${fence}`;
1057+}
1058+1059+function markdownFenceForText(text: string): string {
1060+return "`".repeat(Math.max(3, longestBacktickRun(text) + 1));
1061+}
1062+1063+function longestBacktickRun(value: string): number {
1064+let longest = 0;
1065+let current = 0;
1066+for (const char of value) {
1067+if (char === "`") {
1068+current += 1;
1069+longest = Math.max(longest, current);
1070+continue;
1071+}
1072+current = 0;
1073+}
1074+return longest;
10031075}
1004107610051077function readItemString(item: CodexThreadItem, key: string): string | undefined {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。