Skip to content

fix(agent): propagate LLM stop reason through lifecycle events#24867

Merged
visionik merged 1 commit intoopenclaw:mainfrom
visionik:fix/propagate-stop-reason
Mar 3, 2026
Merged

fix(agent): propagate LLM stop reason through lifecycle events#24867
visionik merged 1 commit intoopenclaw:mainfrom
visionik:fix/propagate-stop-reason

Conversation

@visionik
Copy link
Copy Markdown
Contributor

@visionik visionik commented Feb 23, 2026

Problem

The LLM stop reason (end_turn, max_tokens, etc.) is lost between the agent runner and the gateway event stream. Downstream consumers like the ACP bridge, TUI, and WebSocket clients cannot distinguish a complete response from one truncated by max_tokens.

The stop reason is dropped at two points:

  1. pi-embedded-runner/run.ts only sets meta.stopReason for tool_calls, discarding the actual LLM stop reason from lastAssistant.stopReason
  2. commands/agent.ts emits the lifecycle phase: "end" event without including stopReason

Fix

  • run.ts: Propagate lastAssistant.stopReason into result.meta.stopReason when there are no client tool calls
  • agent.ts: Include stopReason in the lifecycle end event data and log non-end_turn stop reasons to stderr

Context

Related to #24856 (flush throttled delta before final). Together these two PRs improve streaming reliability for gateway clients:

Changes

2 files, 10 lines added, 1 removed.

Greptile Summary

Propagates LLM stop reason (end_turn, max_tokens, etc.) from the agent runner to lifecycle events and downstream consumers.

Changes:

  • run.ts:1138-1140 - propagates lastAssistant.stopReason when no client tool calls present
  • agent.ts:600-612 - includes stopReason in lifecycle end event and logs non-end_turn reasons to stderr

This allows downstream consumers (ACP bridge, TUI, WebSocket clients) to distinguish complete responses from ones truncated by token limits.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The changes are minimal, well-scoped, and backward compatible. The new stopReason field is optional and doesn't break existing consumers. The implementation correctly propagates the stop reason from the LLM through the execution flow. Type safety is maintained with appropriate casts, and the change aligns with existing patterns in the codebase.
  • No files require special attention

Last reviewed commit: e8f66e8

(4/5) You can add custom instructions or style guidelines for the agent here!

@openclaw-barnacle openclaw-barnacle bot added gateway Gateway runtime commands Command implementations agents Agent runtime and tooling size: XS maintainer Maintainer-authored PR labels Feb 23, 2026
Copy link
Copy Markdown
Contributor

@kevinWangSheng kevinWangSheng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Good change in concept - propagating stopReason helps downstream consumers detect truncated responses.

Issue found

⚠️ CI check failed - check job failed: https://github.com/openclaw/openclaw/actions/runs/22329117075/job/64607637997

Please fix the failing check and I'll be happy to approve.

Code review notes

  • The implementation is clean and follows existing patterns
  • Adding stopReason to lifecycle events is backward compatible
  • Good logging for non-end_turn cases

Status: Request changes until CI passes.

Complete the stop reason propagation chain so ACP clients can
distinguish end_turn from max_tokens:

- server-chat.ts: emitChatFinal accepts optional stopReason param,
  includes it in the final payload, reads it from lifecycle event data
- translator.ts: read stopReason from the final payload instead of
  hardcoding end_turn

Chain: LLM API → run.ts (meta.stopReason) → agent.ts (lifecycle event)
→ server-chat.ts (final payload) → ACP translator (PromptResponse)
@visionik visionik force-pushed the fix/propagate-stop-reason branch from 83b30ee to 333c160 Compare March 3, 2026 05:16
@openclaw-barnacle openclaw-barnacle bot removed commands Command implementations agents Agent runtime and tooling labels Mar 3, 2026
@visionik visionik merged commit 0b3bbfe into openclaw:main Mar 3, 2026
29 checks passed
@visionik
Copy link
Copy Markdown
Contributor Author

visionik commented Mar 3, 2026

@kevinWangSheng thank you for the comment. Passed all tests and I just merged. Happy to have your input on any of my future PR's!

kevinWangSheng pushed a commit to kevinWangSheng/openclaw that referenced this pull request Mar 3, 2026
…openclaw#24867)

Complete the stop reason propagation chain so ACP clients can
distinguish end_turn from max_tokens:

- server-chat.ts: emitChatFinal accepts optional stopReason param,
  includes it in the final payload, reads it from lifecycle event data
- translator.ts: read stopReason from the final payload instead of
  hardcoding end_turn

Chain: LLM API → run.ts (meta.stopReason) → agent.ts (lifecycle event)
→ server-chat.ts (final payload) → ACP translator (PromptResponse)
yumesha pushed a commit to yumesha/openclaw that referenced this pull request Mar 3, 2026
…openclaw#24867)

Complete the stop reason propagation chain so ACP clients can
distinguish end_turn from max_tokens:

- server-chat.ts: emitChatFinal accepts optional stopReason param,
  includes it in the final payload, reads it from lifecycle event data
- translator.ts: read stopReason from the final payload instead of
  hardcoding end_turn

Chain: LLM API → run.ts (meta.stopReason) → agent.ts (lifecycle event)
→ server-chat.ts (final payload) → ACP translator (PromptResponse)
mrosmarin added a commit to mrosmarin/openclaw that referenced this pull request Mar 3, 2026
* main: (134 commits)
  fix(telegram): warn when accounts.default is missing in multi-account setup (openclaw#32544)
  agents: propagate config for embedded skill loading
  Gateway: fix stale self version in status output (openclaw#32655)
  feat(mattermost): add native slash command support (refresh) (openclaw#32467)
  Diffs: Migrate tool usage guidance from before_prompt_build to a plugin skill (openclaw#32630)
  bug: Workaround for QMD upstream bug (openclaw#27028)
  fix: improve compaction summary instructions to preserve active work (openclaw#8903)
  chore: Updated Brave documentation (openclaw#26860)
  security(line): synthesize strict LINE auth boundary hardening
  test(e2e): isolate module mocks across harnesses
  fix(telegram): debounce forwarded media-only bursts
  test(live): harden gateway model profile probes
  fix(ci): handle disabled systemd units in docker doctor flow
  fix(test): stabilize appcast version assertion
  fix(line): synthesize media/auth/routing webhook regressions (openclaw#32546) thanks @Takhoffman
  fix(gateway+acp): thread stopReason through final event to ACP bridge (openclaw#24867)
  docs(changelog): reattribute duplicated PR credits
  fix: scope extension runtime deps to plugin manifests
  ci: enable stale workflow
  chore(release): bump to 2026.3.3 and seed changelog
  ...
dawi369 pushed a commit to dawi369/davis that referenced this pull request Mar 3, 2026
…openclaw#24867)

Complete the stop reason propagation chain so ACP clients can
distinguish end_turn from max_tokens:

- server-chat.ts: emitChatFinal accepts optional stopReason param,
  includes it in the final payload, reads it from lifecycle event data
- translator.ts: read stopReason from the final payload instead of
  hardcoding end_turn

Chain: LLM API → run.ts (meta.stopReason) → agent.ts (lifecycle event)
→ server-chat.ts (final payload) → ACP translator (PromptResponse)