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.

Diagram

Turn Completion Notes

completed

max_steps / cancelled / error
content_filtered / refused / length_truncated

๐Ÿง  Agent turn

last_stop_reason?

โœ… Reply as-is
no note surfaced

๐Ÿ“ append user-safe note

๐Ÿ’ฌ Reply + note

Off by default โ€” clean completions and existing deployments are byte-for-byte unchanged.

Quick Start

  1. Turn the note on

    Pass surface_completion_reason=True to 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.

  2. Emit a custom TurnCompletion yourself

    Return an AgentReply with your own TurnCompletion when 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.")

    detail overrides 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.

Diagram
๐Ÿง  Agent๐Ÿงบ BotSessionManager๐Ÿค– Bot๐Ÿ‘ค User๐Ÿง  Agent๐Ÿงบ BotSessionManager๐Ÿค– Bot๐Ÿ‘ค Userextract_completion(result)then extract_completion(agent)alt[reason is completed][max_steps / cancelled /error / content_filtered /refused /length_truncated]messagechat(agent, user, text)agent.chat(text)reply + last_stop_reasonreply unchangedreply + user-safe note๐Ÿ’ฌ final reply
StepWhat happens
Extractextract_completion(result) prefers an AgentReply.completion; falls back to agent.last_stop_reason
GateNote is appended only when surface_completion_reason=True
Skip clean turnscompleted (and "") surface no note
AppendEarly stops append TurnCompletion.note() to the reply text

Configuration Options

OptionWhereTypeDefaultDescription
surface_completion_reasonBotSessionManager(...)boolFalseOpt-in gate. When True, a turn that stops early appends a note to the reply.
AgentReply.completionAgent reply object`TurnCompletion \None`NoneThe agent’s own completion; when set, extract_completion prefers it over the agent’s last_stop_reason.
TurnCompletion.detailOn the TurnCompletionstr""Overrides the default note verbatim.

Completion reason vocabulary

The reasons mirror Agent.last_stop_reason and the built-in _COMPLETION_NOTES map.

reasonUser-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 / methodTypeBehaviour
reasonstrStop reason (completed default); unknown values tolerated
detailstrOptional user-safe override of the default note
truncatedbool (property)True for any reason outside {"completed", ""}
note()strConcise user note, or "" for a clean completion; returns detail when set
to_dict() / from_dict()dictRound-trip serialisation across a network hop
python
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.

python
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:

  1. AgentReply.completion if the result is an AgentReply.
  2. dict["completion"] if the result is a serialised dict.
  3. result.completion attribute on any object.
  4. result.last_stop_reason string on any object (the plain-text-agent fallback).
  5. 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.

python
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.

python
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.

python
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.

python
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.

python
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.

python
TurnCompletion(reason="cancelled", detail="Stopped on your request โ€” resend to retry.")

Related