@@ -3,12 +3,14 @@ import type { AssistantMessage, Usage } from "@mariozechner/pi-ai";
33import { SessionManager } from "@mariozechner/pi-coding-agent" ;
44import {
55 formatErrorMessage ,
6+ formatToolProgressOutput ,
67 inferToolMetaFromArgs ,
78 normalizeUsage ,
89 runAgentHarnessAfterCompactionHook ,
910 runAgentHarnessBeforeCompactionHook ,
1011 type EmbeddedRunAttemptParams ,
1112 type EmbeddedRunAttemptResult ,
13+ TOOL_PROGRESS_OUTPUT_MAX_CHARS ,
1214 formatToolAggregate ,
1315 type 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 ;
5860
61+ const MAX_TOOL_OUTPUT_DELTA_MESSAGES_PER_ITEM = 20 ;
62+
5963export class CodexAppServerEventProjector {
6064 private readonly assistantTextByItem = new Map < string , string > ( ) ;
6165 private readonly assistantItemOrder : string [ ] = [ ] ;
@@ -66,6 +70,11 @@ export class CodexAppServerEventProjector {
6670 private readonly activeCompactionItemIds = new Set < string > ( ) ;
6771 private readonly toolResultSummaryItemIds = new Set < string > ( ) ;
6872 private 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+ > ( ) ;
6978 private readonly toolMetas = new Map < string , { toolName : string ; meta ?: string } > ( ) ;
7079 private assistantStarted = false ;
7180 private reasoningStarted = false ;
@@ -489,10 +498,44 @@ export class CodexAppServerEventProjector {
489498 if ( ! itemId || ! delta || ! this . shouldEmitToolOutput ( ) ) {
490499 return ;
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 ) ;
492532 this . 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 {
588631 if ( this . toolResultOutputItemIds . has ( itemId ) ) {
589632 return ;
590633 }
634+ if ( this . toolResultOutputStreamedItemIds . has ( itemId ) ) {
635+ return ;
636+ }
591637 const toolName = itemName ( item ) ;
592638 const output = itemOutputText ( item ) ;
593639 if ( ! toolName || ! output ) {
@@ -596,12 +642,16 @@ export class CodexAppServerEventProjector {
596642 this . emitToolResultMessage ( {
597643 itemId,
598644 text : formatToolOutput ( toolName , itemMeta ( item ) , output ) ,
599- output : true ,
645+ finalOutput : true ,
600646 } ) ;
601647 }
602648
603- 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 ) {
605655 this . toolResultOutputItemIds . add ( params . itemId ) ;
606656 }
607657 try {
@@ -934,7 +984,10 @@ function itemName(item: CodexThreadItem): string | undefined {
934984
935985function itemMeta ( item : CodexThreadItem ) : string | undefined {
936986 if ( 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 }
939992 if ( item . type === "webSearch" && typeof item . query === "string" ) {
940993 return item . query ;
@@ -995,11 +1048,30 @@ function formatToolSummary(toolName: string, meta?: string): string {
9951048}
9961049
9971050function 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 ) {
10001053 return 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}
10041076
10051077function readItemString ( item : CodexThreadItem , key : string ) : string | undefined {
0 commit comments