renderDocx requires the markdown body to flow through the LLM as a
tool argument. For an 80 KB project proposal that's ≈ 20 K tokens of
streaming output spent just to repeat back content the model already
wrote to disk a turn earlier — multi-minute generation, real money.
renderDocxFromFile takes a file path instead. The agent uses
write_file / edit_file to assemble the markdown locally, then calls
this tool with just the path. JVM reads the file in one IO syscall
and feeds it to the existing MarkdownDocxRenderer. Token cost drops
from ≈ 20 K to ≈ 50 (the path string).
Behavior:
- Path resolution honors WorkspacePathGuard, same boundary as
read_file / write_file. No path traversal.
- UTF-8 read; rejects empty / missing / non-regular paths with
typed error messages so the agent can recover.
- Output cached in GeneratedFileCache and returned as a relative
/api/v1/files/generated/{id} link, with the same anti-host-
hallucination instruction renderDocx already carries.
- Same supported markdown subset (headings, bold, lists, tables).
Image references () still render as raw text — full
image embedding (P1) and SVG → PNG conversion (also P1) need
Apache Batik plus image-rendering plumbing in MarkdownDocxRenderer
and is tracked separately. Chapter-mode merge (P2) likewise needs
its own plumbing.
The @Tool description tells the agent to prefer this path when
markdown exceeds ~5 KB and shows the full write_file →
renderDocxFromFile workflow inline.
Three small but high-impact fixes that all surfaced together while
verifying the long-form generation flow.
1. ChatConsole onBeforeUnmount no longer kills the backend turn.
Previously, switching tabs / route navigation / any cause that
unmounted the chat view called stopChatGeneration(), which POSTs
/chat/{cid}/stop and aborts the in-flight LLM call. The user
reported a turn dying mid-generation just from switching pages.
Replaced with resetForNewConversation() — front-end SSE disconnect
only, no /stop. Backend keeps running; pollActivity / status probe
reconnects on return. Aligns with the existing comment in
selectConversation: "let A's backend agent run continue running."
2. Agent max_iterations raised 25 → 100 with a hard ceiling.
The previous 25-step ceiling caused LimitExceededNode to fire on
substantive multi-tool tasks (document generation + image conversion
+ retry loops). 100 matches QwenPaw's _MAX_MAX_ITERATIONS upper
bound. New plumbing:
- BaseAgent.MAX_ITERATIONS_HARD_CEILING = 100 public constant
- BaseAgent default field 25 → 100 (Java-side fallback)
- AgentGraphBuilder clamps any per-agent DB override to the
ceiling at runtime; if the row holds 200, runtime sees 100 and
a WARN is logged with the original value.
- V47 migration (h2 + mysql) idempotently bumps the three default
seeded agents (1000000001, 1000000002, 1000000003) only if they
still hold the old defaults (25 / 20). User-customized values
are not touched.
- data-en/zh/-mysql-en/-mysql-zh seed files updated to 100 for
fresh installs.
3. DocxRenderTool tells the LLM not to prepend a host to the URL.
DeepSeek and Claude have both been observed wrapping the
/api/v1/files/generated/{id} relative path returned by renderDocx
into an absolute URL with a hallucinated domain (e.g.
https://ai-tools-system.com/...), breaking the download link in
the rendered chat bubble. The tool's return string now appends an
explicit "must use the relative path verbatim, do not add any
https:// or http:// prefix" instruction, which Claude and
DeepSeek both honor.
Full-stack AI assistant built on Spring AI Alibaba.
Features: ReAct Agent, Plan-and-Execute, MCP Protocol, Multi-Model, Multi-Channel.
Apache-2.0 License