Problem
In AxGen's multi-step loop, when ctx.addFunctions() is called in a step hook (e.g., afterFunctionExecution), the new tools are correctly added to the API call's tools array — but the <available_functions> section in the system prompt is rendered once at the start of the loop and never updated between steps.
This creates a mismatch the LLM sees on every subsequent step:
- System prompt
<available_functions>: lists only the original N tools
- API tool definitions: include the original N + dynamically added tools
LLMs heavily anchor on the system prompt to understand what's available. Because the system prompt still says only N tools exist, the LLM ignores the dynamically added tool definitions and keeps re-calling the discovery function on every step.
Note: AxAgent does NOT have this issue — when addFunctions() is called, it triggers _buildSplitPrograms() which rebuilds baseActorDefinition, and refreshActorInstruction() picks up the updated function list on the next turn.
Reproduction
- Create an AxGen program with 5 initial functions + a
search_tools meta-function
- In
afterFunctionExecution hook, call ctx.addFunctions([...10 more tools...]) when search_tools is invoked
- Observe that on step N+1, the system prompt's
<available_functions> still lists only 5 tools
- The LLM calls
search_tools again with the same query on every step
- This repeats for the entire
forward() call
With 41 deferred tools and Gemini 2.5 Pro, this caused 660K prompt tokens and 14 steps for a query that should take 4-5 steps. The LLM replays the identical search → list_tables → describe_table → search → ... sequence on every step because it doesn't realize the discovered tools are already available.
Expected behavior
After ctx.addFunctions() is called within AxGen's multi-step loop, the <available_functions> block in the system message should be re-rendered before the next API call to reflect the current function set.
Suggested approach
In AxGen.forwardSendRequest() (or wherever the multi-step loop iterates), after applying pending function mutations from addFunctions() / removeFunctions() and before the next API call, re-generate the system message's <available_functions> section.
Two possible approaches:
- Full re-render: Replace the system message content with an updated render from
AxPromptTemplate using the current function set. Simple, correct, but re-sends the full system prompt.
- Incremental append: Append a short
<newly_available_functions> section to the existing system message listing only the functions added since last step. More token-efficient for progressive discovery (e.g., the LLM searches 3 times for different tool categories).
Why this matters
This affects any use case combining AxGen + step hooks + ctx.addFunctions():
- Deferred/lazy tool loading — only relevant tools activated on demand to reduce initial token cost
- MCP servers with many tools — progressive discovery instead of sending 40+ tool schemas upfront
- Multi-phase workflows — tool access expands as the task progresses
The AxAgent path already handles this correctly via _buildSplitPrograms() + refreshActorInstruction(). The fix would bring AxGen to parity.
Context
This was discovered while implementing client-side deferred tool loading in ax-crew. The pattern:
- Agent starts with core tools +
search_tools meta-function (5 tools instead of 46)
- LLM calls
search_tools("query") → matched tools are injected via ctx.addFunctions() in afterFunctionExecution hook
- On next step, tools work (LLM can call them) but the system prompt still advertises only 5 tools
- LLM re-searches on every step, wasting ~50K tokens per redundant cycle
Problem
In AxGen's multi-step loop, when
ctx.addFunctions()is called in a step hook (e.g.,afterFunctionExecution), the new tools are correctly added to the API call'stoolsarray — but the<available_functions>section in the system prompt is rendered once at the start of the loop and never updated between steps.This creates a mismatch the LLM sees on every subsequent step:
<available_functions>: lists only the original N toolsLLMs heavily anchor on the system prompt to understand what's available. Because the system prompt still says only N tools exist, the LLM ignores the dynamically added tool definitions and keeps re-calling the discovery function on every step.
Note: AxAgent does NOT have this issue — when
addFunctions()is called, it triggers_buildSplitPrograms()which rebuildsbaseActorDefinition, andrefreshActorInstruction()picks up the updated function list on the next turn.Reproduction
search_toolsmeta-functionafterFunctionExecutionhook, callctx.addFunctions([...10 more tools...])whensearch_toolsis invoked<available_functions>still lists only 5 toolssearch_toolsagain with the same query on every stepforward()callWith 41 deferred tools and Gemini 2.5 Pro, this caused 660K prompt tokens and 14 steps for a query that should take 4-5 steps. The LLM replays the identical search → list_tables → describe_table → search → ... sequence on every step because it doesn't realize the discovered tools are already available.
Expected behavior
After
ctx.addFunctions()is called within AxGen's multi-step loop, the<available_functions>block in the system message should be re-rendered before the next API call to reflect the current function set.Suggested approach
In
AxGen.forwardSendRequest()(or wherever the multi-step loop iterates), after applying pending function mutations fromaddFunctions()/removeFunctions()and before the next API call, re-generate the system message's<available_functions>section.Two possible approaches:
AxPromptTemplateusing the current function set. Simple, correct, but re-sends the full system prompt.<newly_available_functions>section to the existing system message listing only the functions added since last step. More token-efficient for progressive discovery (e.g., the LLM searches 3 times for different tool categories).Why this matters
This affects any use case combining AxGen + step hooks +
ctx.addFunctions():The AxAgent path already handles this correctly via
_buildSplitPrograms()+refreshActorInstruction(). The fix would bring AxGen to parity.Context
This was discovered while implementing client-side deferred tool loading in ax-crew. The pattern:
search_toolsmeta-function (5 tools instead of 46)search_tools("query")→ matched tools are injected viactx.addFunctions()inafterFunctionExecutionhook