Online edition for BLOGE
0.9.8-RC1· facts verified 2026-09-15 · 中文
Chapter 17 — Putting a Nondeterministic Agent Inside a Deterministic Lifecycle
Promise: You will let a support Agent use only allowlisted read tools, make turns, tool concurrency, and exit reasons observable, and prevent an incomplete answer from marking a ticket resolved through a deterministic gate.
Learning goals
By the end of this chapter, you can:
- Separate Session rounds, ticket-state transitions, and Agent model turns.
- Treat the tool list as a capability allowlist rather than prompt advice.
- Separate
finishReason, loop exit reason, and business resolution. - Handle normal stop, tool failure,
max_turns, and recoverable checkpoints correctly. - Place deterministic validation and human handoff after an Agent.
This chapter adds one variable
The support story already has a stable ticketId, a multi-round Session, and a ticket state machine. This chapter does not introduce a fourth lifecycle; it adds one nondeterministic model/tool loop inside triaging.
Session round: customer and system exchanges
Ticket state: open → triaging → assigned / escalated → resolved
Agent turn: model response → tool call → observation → next response
All three counts can change, but the word “turn” must not collapse them.
Opening problem: can “resolved” close the ticket
A customer says, “My last refund has not arrived.” After searching the knowledge base, the Agent replies, “Refunds usually take three to five business days; this is resolved.”
Should ticket.status become resolved?
No. Model text is a candidate explanation. The system still does not know whether this refund exists, exceeded SLA, or needs account review. No refund effect occurred.
Minimum support Agent: two read-only tools
agent supportTriage {
model = "replaceable-support-model"
system_prompt = "Clarify the issue. Never claim a refund was issued."
max_turns = 4
max_tool_concurrency = 2
memory = sliding_window(6)
tool searchKnowledgeBase : KBSearchOperator {
description = "Search approved support articles"
input { query = tool_args.query }
}
tool readTicket : ReadTicketOperator {
description = "Read the current ticket snapshot"
input { ticketId = ctx.ticketId }
}
exit_condition = finish_reason == "stop"
}
The two parameters have different authority. The model supplies query; the host injects ticketId, so the model cannot switch to someone else's ticket.
There is no refund, closeTicket, or changeSla tool. “Never refund” in a prompt is guidance. The absence of a refund capability from the tool list is the execution boundary.
Observe the actual RC1 loop semantics
Run compiler and loop tests:
cd submodule/bloge
mvn -pl bloge-agent-ext \
-Dtest=AgentLoopOperatorTest,AgentDslCompileTest test
Observed on 2026-09-14 at commit cc38fbe5:
AgentLoopOperatorTest Tests run: 6, Failures: 0
AgentDslCompileTest Tests run: 5, Failures: 0
Tests run: 11, Failures: 0, Errors: 0, Skipped: 0
The 11 tests cover direct stop, continue after a tool call, multi-tool concurrency, tool-failure feedback, max_turns, DSL schema inference, and embedded operators.
Mechanism: what happens in one turn
The synchronous AgentLoopOperator path is:
assemble memory
→ call llmChat
→ no tool call or finish_reason=stop: return AgentOutput
→ tool call: dispatch only through compiled toolBindings
→ append tool result to memory
→ evaluate exit_condition
→ next turn or exit
ToolDispatcher resolves a name to AgentToolRef, which points to a compiled subgraph and operator. An undeclared name has no dispatch binding. The host must still register the correct implementation and authority; an allowlist cannot repair an operator that is itself overprivileged.
Read four outcomes separately
| Runtime outcome | RC1 behavior | Downstream handling |
|---|---|---|
model stop, no tool call | returns AgentOutput | still pass the business gate |
exit_condition true | returns AgentOutput; listener records exit | still pass the business gate |
| tool failure | error becomes a tool message; loop may continue | inspect degradation and handoff |
max_turns reached | throws MaxTurnsExceededException with AgentCheckpoint | node fails; retain/handoff, never treat as final |
This corrects a common misconception: in RC1, exhausting max_turns does not send partial text downstream as a normal AgentOutput. Partial progress is in the exception's checkpoint: conversation, turn count, and recent tool results.
On a normal return, AgentOutput.finishReason is the model's last reason. exit_condition is the listener's loop-exit reason. Neither is business state resolved.
Budget: what is controlled and what is not
max_turns bounds model calls; max_tool_concurrency bounds tools per turn. sliding_window(6) limits history sent on each call, not total tokens or cost.
The RC1 loop reports accumulated tokens to listeners, but this source path has no hard “stop after total tokens” gate. If the business requires a spend limit, the host must enforce rejection in the provider, operator/interceptor, or outer control plane and map that rejection to an explicit state.
Deterministic downstream gate
After a normal Agent return, ValidateResolutionOperator reads structured facts:
ResolutionDecision validate(AgentOutput out, TicketSnapshot ticket) {
if (!"stop".equalsIgnoreCase(out.finishReason())) return HANDOFF;
if (out.content() == null || out.content().isBlank()) return HANDOFF;
if (!ticket.requiredFactsComplete()) return ASK_CUSTOMER;
if (ticket.refundOrSlaDispute()) return HUMAN_REVIEW;
return PROPOSE_RESOLUTION;
}
The gate does not grade whether prose sounds human. It checks facts required before closure. PROPOSE_RESOLUTION remains a proposal; the state machine transitions to resolved only after an explicit event.
The stable responsibility chain is:
Agent produces a candidate answer
→ deterministic gate checks structured facts
→ state machine receives an explicit event
→ human or system owner owns the final state
Single-factor failure: change max_turns from 4 to 2
Keep model responses and tools fixed. Turn 1 asks for a knowledge lookup; turn 2 requests another tool and never returns stop. AgentLoopOperatorTest.throwsWhenMaxTurnsAreExhausted asserts:
exception = MaxTurnsExceededException
maxTurns = 2
checkpoint.turnCount = 2
Recovery is not showing the checkpoint's last sentence as an answer. Retain checkpoint and exit reason, leave the ticket in triaging, then let policy choose retry, customer clarification, or human handoff.
Another failure: a tool errors, but the loop sounds fluent
RC1 serializes a tool exception into a tool message so the model can explain or degrade. The test's tool throws boom; the next turn returns Handled fallback.
The loop returned successfully, but the business fact was not obtained. The deterministic gate must know whether a required fact came from a successful tool result. Fluent fallback cannot turn “refund record unavailable” into “refund is normal.”
Real-world transfer: an emergency triage assistant
An emergency assistant may read registration data and triage guidance. It cannot discharge a patient because a model says “safe to go home.” A rule gate checks vital signs and required exams; a clinician owns the final transition.
| Support system | Emergency system |
|---|---|
| read-only ticket/KB tools | patient/guideline lookup |
max_turns | interview-step limit |
| partial checkpoint | incomplete interview record |
| resolution gate | required-exam and risk gate |
| human handoff | clinician decision |
The mapping is not simply “both use AI.” Nondeterministic advice is enclosed by a deterministic responsibility boundary.
Your turn: draw an authority and exit card
Choose one candidate Agent in your system:
- List at most three read-only tools and no default write tools.
- Mark each parameter as model-supplied or host-injected.
- Define
max_turns, per-turn concurrency, and the owner of a true total budget. - Draw
stop, tool failure, and max-turns paths. - Design a downstream gate that reads structured facts only.
Deliver an authority-boundary diagram and an exit matrix. Stop when no natural-language output can bypass the gate to trigger a high-risk effect or terminal state.
What this chapter proved—and did not prove
The RC1 tests prove that declared Agent DSL compiles into an embedded operator, allowlisted tools execute, concurrency limits hold, tool failures can return as observations, and exhausted turns throw an exception with checkpoint. They do not prove answer correctness, prompt-based authorization, a total token budget, or automatic ticket closure.
Field, Builder, memory, streaming, and listener catalogs remain in Appendix E. Chapter 18 — Testing Your Graphs next establishes the test layers needed before Arc 6 separates business verdict, trust, and capability.
Experiment acceptance card
- Expected and observed: A scoped Agent returns a finish reason, turn budget, and deterministic gate.
- Failure and recovery: Set max_turns to 2 or fail a tool; recover with an explicit partial result.
- Proof boundary: Proves controlled loop and permissions, not a correct or approved model answer.
- Exercise contract: One ticket; change only allowlist or budget; deliver an exit card; stop when every exit has a downstream action.
Exact fact entry points
AgentLoopOperator.javaAgentLoopOperatorTest.javaAgentDslCompileTest.javaAgentOutput.javaMaxTurnsExceededException.java- Customer-support Story Bible
Coding Agent: Open the versioned task guide.