Online edition for BLOGE
0.9.8-RC1· facts verified 2026-09-15 · 中文
Appendix E — Agent Extension
Chapter 17 teaches how to think about agents on top of bloge. This appendix is the lookup table you keep open while writing real ones: every DSL keyword, every record field, every SPI callback, every default value, and every finish-reason string the runtime can emit.
When You Need This Reference
| Question | Section |
|---|---|
What DSL keywords can I use inside agent { … }? | DSL Keyword Reference |
How does a tool { … } block become an LLM tool schema? | Tool Block Reference |
| Which memory strategies exist and what do they cost? | Memory Strategies |
What context variables can I use inside exit_condition? | Exit Condition |
What can AgentOutput.finishReason actually contain? | Finish Reasons and Exceptions |
| Which callbacks fire and in what order? | AgentExecutionListener Cheat Sheet |
| How do I build the agent from Java instead of DSL? | Java Builder API |
| What chat-model SPIs do I have to satisfy? | LLM Provider Surface |
| What Spring auto-configuration kicks in when this module is on the classpath? | Spring Wiring |
| When should I reach for an agent versus a state machine or session? | Decision Table |
This is the reference layer for Chapter 17 — Agent Orchestration. Start there for the conceptual walk-through.
Module Surface
| Property | Value |
|---|---|
| Maven coordinates | com.leanowtech.bloge:bloge-agent-ext:${version} |
| DSL extension kind | "agent" |
| Registration | AgentExtensionProvider discovered via DslExtensionProvider SPI |
| Hard dependencies | bloge-core, bloge-dsl |
| Runtime dependencies | bloge-common-operators (for LlmProvider / llmChat / llmStreamingChat / TokenEstimator); optional JsonCodec for tool argument deserialization |
When the JAR is on the classpath the DSL compiler picks up the agent block
syntax automatically. No manual wiring is required for compilation. At runtime,
the agent loop expects a registered LLM chat operator (sync: "llmChat";
streaming: "llmStreamingChat").
ReAct Turn Cycle
The loop, the listener callback points, and the streaming chunk types in one picture:
DSL Keyword Reference
| Keyword | Scope | Type / value | Default | Purpose |
|---|---|---|---|---|
agent | root, graph body, phase body, state body | block prefix | — | Declares an agent extension block. |
stream agent | same as agent | block prefix | — | Shorthand that sets streaming = true. |
model | agent body | String | required | LLM model identifier passed to llmChat. |
system_prompt | agent body | String | null | Leading system message added to every LLM call. |
max_turns | agent body | int | 10 | Hard cap on think-act-observe rounds. Exceeding it throws MaxTurnsExceededException. |
max_tool_concurrency | agent body | Integer | null (unlimited) | Concurrent tool dispatch cap per turn. |
temperature | agent body | Double | null (provider default) | Sampling temperature passed to llmChat. |
memory | agent body | strategy expression | full() | Conversation memory strategy — see below. |
exit_condition | agent body | DSL expression | null (never early-exit) | Evaluated after each tool round; truthy result exits the loop. |
streaming | agent body | boolean | false | Materializes a StreamingAgentLoopOperator instead of AgentLoopOperator. |
streaming_buffer_capacity | agent body | Integer | 16 | Internal LLM-token buffer size used by the streaming variant. |
tool <name> : <OperatorRef> | agent body, repeatable | block | — | Declares one tool. The operator referenced becomes its implementation. |
description | tool body | String | "" | Tool description sent to the LLM. |
input { … } | tool body | block of assignments | — | Maps tool-call arguments into operator inputs. |
tool_args | inside input { … } | context variable | — | The arguments object provided by the LLM for a tool call. |
Memory Strategies
memory = … accepts these forms (all backed by AgentMemoryStrategy):
| DSL form | Behavior | Notes |
|---|---|---|
full() | Keeps every message ever appended. | Default. Cheapest in CPU, most expensive in tokens. |
sliding_window(n) | Keeps all system messages plus the last n non-system messages. | n must be ≥ 1. |
token_budget(n) | Prunes oldest non-system messages until the TokenEstimator reports ≤ n total tokens. | Needs TokenEstimator (registered as an SPI; a heuristic fallback exists). |
summary(n) | Keeps the last n non-system messages verbatim and replaces older ones with an LLM-generated summary. | Triggers a separate LLM call to summarize — measure cost. |
AgentMemoryStrategy.Custom(factory) | Java only — supply your own ConversationMemory.Factory. | Not expressible in DSL. |
Exit Condition
exit_condition is a DSL expression evaluated against this context after each
tool round:
| Variable | Type | Meaning |
|---|---|---|
finish_reason | String | Finish reason from the most recent LLM response. |
content | String | Assistant content string from the most recent LLM response. |
turn | int | 1-based turn number that just completed. |
tool_calls | List<Map> | Tool calls produced this turn — each has id, name, arguments. |
tool_results | Map<String,Object> | Results from the current tool batch, keyed by tool name. |
messages | List<LlmMessage> | Full conversation state including newly-appended tool messages. |
A truthy result records "exit_condition" in the listener's
LoopExitedPayload.exitReason and stops the loop without another model call.
The returned AgentOutput.finishReason still carries the latest provider reason.
Tool Block Reference
A tool block compiles into an AgentToolRef. Three things flow out of it:
- The LLM-facing
ToolDefinition—name(the DSL label),description(yourdescription = …), and a JSON-schemaparametersobject derived from theinput { … }block. - A subgraph that wraps the referenced operator. The compiler builds it so the agent loop can invoke the operator like any other bloge node.
- The
resultNodeIdthe loop uses to extract the tool's output from that subgraph.
Tool Schema Inference
The compiler walks each assignment in input { … }. Every path of the form
tool_args.<field> becomes a required parameter in the JSON-schema. Other
assignments (constants or references to outer context) do not appear in the
schema — they are filled in at dispatch time.
tool searchKnowledgeBase : KBSearchOperator {
description = "Search the knowledge base"
input {
query = tool_args.query
locale = ctx.user.locale // not part of the LLM-visible schema
}
}
The LLM sees a tool named searchKnowledgeBase with one required string
parameter query. locale is filled from the surrounding graph context. If
your tool inputs include nested objects, configure a JsonCodec on the DSL
compiler so the arguments JSON can be deserialized into structured values.
Worked Example
The shortest end-to-end agent — DSL plus the Java bootstrap needed to run it.
agent customerSupport {
model = "gpt-4o"
system_prompt = "You are a helpful support agent."
max_turns = 15
temperature = 0.7
memory = sliding_window(20)
tool searchKnowledgeBase : KBSearchOperator {
description = "Search the knowledge base"
input { query = tool_args.query }
}
tool escalateToHuman : EscalateOperator {
description = "Escalate to a human agent"
input { reason = tool_args.reason }
}
exit_condition = finish_reason == "stop" || tool_call("escalateToHuman")
}
OperatorRegistry registry = OperatorRegistry.builder()
.register("KBSearchOperator", new KBSearchOperator())
.register("EscalateOperator", new EscalateOperator())
.register("llmChat", new LlmChatOperator(myLlmProvider))
.build();
AgentDef def = new AgentDslCompiler(registry).compile(dslSource);
AgentLoopOperator agent = new AgentLoopOperator("supportAgent", def, registry);
AgentOutput result = agent.execute("My account is locked", OperatorContext.root());
System.out.println(result.content());
System.out.println(result.finishReason()); // provider reason, e.g. "stop" / "tool_calls"
For the streaming variant call toStreamingOperator(...) on AgentBuilder or
set streaming = true in DSL; the runtime emits AgentStreamChunk values
through a NodeChannel.
Java Builder API
AgentBuilder mirrors every DSL keyword:
| Method | Purpose |
|---|---|
AgentBuilder.create(id) | Start a fresh builder. |
AgentBuilder.from(def) | Start from an existing AgentDef. |
.model(String) | Sets the LLM model id. |
.systemPrompt(String) | Sets the system prompt. |
.maxTurns(int) | Sets the turn cap. |
.maxToolConcurrency(int) | Sets the per-turn tool concurrency cap. |
.temperature(Double) | Sets the sampling temperature. |
.memory(AgentMemoryStrategy) | Picks a memory strategy. |
.tool(AgentToolRef) | Adds one tool definition. |
.exitCondition(Expression | String) | Compiles or attaches an exit-condition expression. |
.streaming(boolean) | Toggles the streaming variant. |
.streamingBufferCapacity(int) | Sets the streaming buffer size. |
.build() | Produces an immutable AgentDef. |
.toOperator(ref [, registry]) | Materializes an AgentLoopOperator. |
.toStreamingOperator(ref [, registry]) | Materializes a StreamingAgentLoopOperator. |
Record Schemas
Every record is immutable. Fields are listed in declaration order.
AgentDef
| Field | Type | Notes |
|---|---|---|
id | String | Identifier from the DSL. |
model | String | LLM model id; required. |
systemPrompt | String | May be null. |
maxTurns | int | Default 10. |
maxToolConcurrency | Integer | null => unlimited. |
temperature | Double | null => provider default. |
memoryStrategy | AgentMemoryStrategy | Defaults to Full. |
tools | List<AgentToolRef> | Order preserved from DSL. |
exitCondition | Expression | May be null. |
streaming | boolean | true => use streaming operator. |
streamingBufferCapacity | Integer | Buffer for streaming variant, default 16. |
AgentInput
| Field | Type | Notes |
|---|---|---|
messages | List<LlmMessage> | Normalized form. AgentInput.from(Object) coerces strings, single messages, and lists. |