This is an automated email from the ASF dual-hosted git repository. hsluoyz pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git
commit 0ddd45549f82cc34980cbbbbcdf9f5ca8709b2ba Author: Yang Luo <[email protected]> AuthorDate: Mon Sep 7 00:05:10 2026 +0800 fix(chat): keep Codex's passing notices out of the reply --- agentsession/event.go | 6 +++++- agentsession/parse_codex.go | 5 ++++- imbridge/render.go | 19 ++++++++++++++----- web/src/pages/ChatPage.tsx | 14 +++++++++++++- web/src/types.ts | 2 +- 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/agentsession/event.go b/agentsession/event.go index 57a663e..9483510 100644 --- a/agentsession/event.go +++ b/agentsession/event.go @@ -30,6 +30,10 @@ const ( EventUsage EventType = "usage" EventDone EventType = "done" EventError EventType = "error" + // EventNotice is something the agent mentioned in passing - a setting it + // ignored, a limit it worked around. The turn carried on, so it is kept out + // of the answer and shown beside it. + EventNotice EventType = "notice" ) // Event is one piece of what an agent said, in a shape every agent is flattened @@ -44,7 +48,7 @@ type Event struct { CreatedTime string `json:"createdTime"` // Text is the answer for EventText and EventThinking, and the message for - // EventError. + // EventError and EventNotice. Text string `json:"text,omitempty"` ToolName string `json:"toolName,omitempty"` diff --git a/agentsession/parse_codex.go b/agentsession/parse_codex.go index 4e2ec37..1a16255 100644 --- a/agentsession/parse_codex.go +++ b/agentsession/parse_codex.go @@ -138,9 +138,12 @@ func emitCodexItem(item *codexItem, emit func(Event)) { event.ToolName = "Edit" event.ToolInput = firstOf(item.Text, item.Message) emit(event) + // An error item is Codex saying something went sideways without the turn + // ending: a setting it dropped, a budget it trimmed to fit. A turn that + // really failed says so through turn.failed, which fails the run. case "error": if text := firstOf(item.Message, item.Text); text != "" { - emit(textEvent(EventError, text)) + emit(textEvent(EventNotice, text)) } } } diff --git a/imbridge/render.go b/imbridge/render.go index 7d1d200..f2f366a 100644 --- a/imbridge/render.go +++ b/imbridge/render.go @@ -25,6 +25,9 @@ import ( // what an agent did is most of what somebody watching wants to see. type answerBuffer struct { parts []string + // prose says the last part is the agent talking, which is the only part a + // fragment of prose may be joined onto. + prose bool // dirty says something has arrived since the last time this was written out. dirty bool } @@ -37,25 +40,31 @@ func (b *answerBuffer) add(event agentsession.Event) { } // Prose arrives in fragments, so it is joined onto the piece before it // rather than starting a new one. - if len(b.parts) > 0 && !strings.HasPrefix(b.parts[len(b.parts)-1], toolMarker) { + if b.prose { b.parts[len(b.parts)-1] += event.Text } else { b.parts = append(b.parts, event.Text) + b.prose = true } case agentsession.EventToolUse: b.parts = append(b.parts, toolMarker+toolLine(event)) + b.prose = false case agentsession.EventError: - if event.Text != "" { - b.parts = append(b.parts, "\n⚠ "+event.Text) + if event.Text == "" { + return } + b.parts = append(b.parts, "\n⚠ "+event.Text) + b.prose = false + // What the agent mentioned in passing stays out of the chat: the answer is + // what somebody asked for, and a warning about a setting on top of every + // reply is noise. The session page still shows it. default: return } b.dirty = true } -// toolMarker starts the pieces that are a tool rather than prose, so a fragment -// of prose is not appended onto one. +// toolMarker sets a tool line apart from the prose around it. const toolMarker = "· " // toolInputLimit keeps a command in the message without the message becoming the diff --git a/web/src/pages/ChatPage.tsx b/web/src/pages/ChatPage.tsx index 883e190..93933ce 100644 --- a/web/src/pages/ChatPage.tsx +++ b/web/src/pages/ChatPage.tsx @@ -49,6 +49,9 @@ interface Turn { seq: number; prompt: string; blocks: Block[]; + // notices are what the agent mentioned in passing; they are shown beside the + // answer rather than in it, because the turn carried on regardless. + notices: string[]; error: string; usage?: DrivenEvent; done: boolean; @@ -78,7 +81,7 @@ function toTurns(events: DrivenEvent[]): Turn[] { for (const event of events) { switch (event.type) { case "prompt": - turn = {seq: event.seq, prompt: event.text ?? "", blocks: [], error: "", done: false}; + turn = {seq: event.seq, prompt: event.text ?? "", blocks: [], notices: [], error: "", done: false}; turns.push(turn); break; case "text": @@ -112,6 +115,11 @@ function toTurns(events: DrivenEvent[]): Turn[] { turn.error = event.text ?? ""; } break; + case "notice": + if (turn && event.text && !turn.notices.includes(event.text)) { + turn.notices.push(event.text); + } + break; case "done": if (turn) { turn.done = true; @@ -166,6 +174,10 @@ function TurnView({turn}: {turn: Turn}) { ); })} + {turn.notices.map(notice => ( + <MessageAlert key={notice} variant="warning" description={notice} /> + ))} + {turn.error ? <MessageAlert variant="destructive" title={turn.error} /> : null} {turn.usage ? ( diff --git a/web/src/types.ts b/web/src/types.ts index 978122c..adc40e6 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -553,7 +553,7 @@ export interface DrivenSession { /** One piece of what a driven agent said, in the shape every agent is flattened into. */ export interface DrivenEvent { - type: "prompt" | "text" | "thinking" | "toolUse" | "toolResult" | "usage" | "done" | "error"; + type: "prompt" | "text" | "thinking" | "toolUse" | "toolResult" | "usage" | "done" | "error" | "notice"; seq: number; createdTime: string; text?: string; --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
