Turn Completion Notes
Surface why a bot turn ended (max_steps, cancelled, error, provider blocks) as a user-safe note
Surface why a bot turn ended โ so a chat user reads “I stopped at the step limit” instead of a silently truncated reply.
Off by default โ clean completions and existing deployments are byte-for-byte unchanged.
Quick Start
- Turn the note on
Pass
surface_completion_reason=Trueto the session manager. A turn that stops early appends a note to the reply.python from praisonaiagents import Agent from praisonai_bot.bots._session import BotSessionManager agent = Agent( name="assistant", instructions="Help users with their questions.", max_steps=5, # keep it low so max_steps notes are easy to see ) session = BotSessionManager( platform="telegram", surface_completion_reason=True, ) import asyncio reply = asyncio.run(session.chat(agent, "u1", "do a big task")) print(reply) # includes: โณ I stopped after reaching the step limit โ reply "continue" to carry on.A truncated turn now ends with
โณ I stopped after reaching the step limit โ reply "continue" to carry on. - Emit a custom TurnCompletion yourself
Return an
AgentReplywith your ownTurnCompletionwhen the agent already knows why it stopped.python from praisonaiagents import Agent from praisonaiagents.bots import AgentReply, TurnCompletion class MyAgent(Agent): def chat(self, prompt): if self.not_enough_context(prompt): return AgentReply( text="Here's what I have so far.", completion=TurnCompletion( reason="max_steps", detail="I ran out of steps mid-lookup โ send `continue` to keep going.", ), ) return AgentReply(text="Full answer.")detailoverrides the default note verbatim โ it’s already user-safe, no template applied.
How It Works
The gateway reads the turn’s completion after each reply and appends a note only when the turn stopped early and the flag is on.
| Step | What happens |
|---|---|
| Extract | extract_completion(result) prefers an AgentReply.completion; falls back to agent.last_stop_reason |
| Gate | Note is appended only when surface_completion_reason=True |
| Skip clean turns | completed (and "") surface no note |
| Append | Early stops append TurnCompletion.note() to the reply text |
Configuration Options
| Option | Where | Type | Default | Description | |
|---|---|---|---|---|---|
surface_completion_reason | BotSessionManager(...) | bool | False | Opt-in gate. When True, a turn that stops early appends a note to the reply. | |
AgentReply.completion | Agent reply object | `TurnCompletion \ | None` | None | The agent’s own completion; when set, extract_completion prefers it over the agent’s last_stop_reason. |
TurnCompletion.detail | On the TurnCompletion | str | "" | Overrides the default note verbatim. |
Completion reason vocabulary
The reasons mirror Agent.last_stop_reason and the built-in _COMPLETION_NOTES map.
reason | User-facing note (default) | Meaning |
|---|---|---|
completed | (none โ no note surfaced) | Turn finished cleanly |
max_steps | โณ I stopped after reaching the step limit โ reply "continue" to carry on. | Hit ExecutionConfig.max_steps |
cancelled | ๐ This turn was interrupted before it finished โ send it again to retry. | Turn was cancelled |
error | โ ๏ธ This turn ended early due to an error โ please try again. | Turn ended on an exception |
content_filtered | โณ This turn ended early โ please try again. (generic fallback) | Provider’s content filter blocked the response |
refused | โณ This turn ended early โ please try again. (generic fallback) | Model declined to answer (safety refusal) |
length_truncated | โณ This turn ended early โ please try again. (generic fallback) | Response hit the model’s output length limit |
| (any other string) | โณ This turn ended early โ please try again. | Forward-compat fallback for future reasons |
.truncated is True for anything not in {"completed", ""} โ so the three provider-block reasons always surface a note.
The TurnCompletion Contract
TurnCompletion wraps the coarse stop reason into a portable value the reply path can render.
| Field / method | Type | Behaviour |
|---|---|---|
reason | str | Stop reason (completed default); unknown values tolerated |
detail | str | Optional user-safe override of the default note |
truncated | bool (property) | True for any reason outside {"completed", ""} |
note() | str | Concise user note, or "" for a clean completion; returns detail when set |
to_dict() / from_dict() | dict | Round-trip serialisation across a network hop |
from praisonaiagents.bots import TurnCompletion
c = TurnCompletion(reason="max_steps")
c.truncated # True
c.note() # โณ I stopped after reaching the step limit โ reply "continue" to carry on.
TurnCompletion.from_dict(c.to_dict()).reason # "max_steps"Gateway Integration
Operators wiring a custom reply path use the two raw helpers directly.
from praisonaiagents.bots import extract_completion, append_completion_note
completion = extract_completion(result) or extract_completion(agent) # falls back to agent.last_stop_reason
text_to_send = append_completion_note(text, completion, enabled=True)extract_completion checks, in order:
AgentReply.completionif the result is anAgentReply.dict["completion"]if the result is a serialised dict.result.completionattribute on any object.result.last_stop_reasonstring on any object (the plain-text-agent fallback).None.
append_completion_note returns the text untouched when enabled=False, when there is no completion, or when the turn completed cleanly โ so turning it on only affects truncated turns.
Common Patterns
Turn on notes for early stops on public bots
One flag on the session manager, matching the Quick Start.
session = BotSessionManager(platform="telegram", surface_completion_reason=True)Return an AgentReply with a custom detail
For agent code that already knows why it’s stopping.
from praisonaiagents.bots import AgentReply, TurnCompletion
return AgentReply(
text="Partial result.",
completion=TurnCompletion(reason="cancelled", detail="You interrupted me โ resend to retry."),
)Serialise across a network hop
Round-trip via to_dict() / from_dict() on both TurnCompletion and AgentReply.
from praisonaiagents.bots import AgentReply, TurnCompletion
wire = AgentReply(text="hi", completion=TurnCompletion(reason="error")).to_dict()
restored = AgentReply.from_dict(wire)
restored.completion.reason # "error"Best Practices
Keep the flag off unless you want early-stop notes
Clean completions never surface a note, so turning surface_completion_reason on only affects truncated turns. Leave it off for bots where a silent reply is fine.
Prefer TurnCompletion.detail over rewriting the default note
detail is user-safe, agent-authored, and localisable per turn. Set it when the agent knows the specific reason instead of relying on the generic default.
TurnCompletion(reason="max_steps", detail="Ran out of steps mid-search โ send `continue`.")Combine with a low max_steps for debugging
Set max_steps=5 so โณ step limit notes surface fast while you tune step budgets.
Agent(name="debug", instructions="...", max_steps=5)Emit cancelled yourself when a user interrupts
The gateway’s default cancelled note is generic โ set your own detail to replace it.
TurnCompletion(reason="cancelled", detail="Stopped on your request โ resend to retry.")