Skip to content

AxGen: Re-render available_functions in system prompt after ctx.addFunctions() #500

@amitdeshmukh

Description

@amitdeshmukh

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

  1. Create an AxGen program with 5 initial functions + a search_tools meta-function
  2. In afterFunctionExecution hook, call ctx.addFunctions([...10 more tools...]) when search_tools is invoked
  3. Observe that on step N+1, the system prompt's <available_functions> still lists only 5 tools
  4. The LLM calls search_tools again with the same query on every step
  5. 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:

  1. Agent starts with core tools + search_tools meta-function (5 tools instead of 46)
  2. LLM calls search_tools("query") → matched tools are injected via ctx.addFunctions() in afterFunctionExecution hook
  3. On next step, tools work (LLM can call them) but the system prompt still advertises only 5 tools
  4. LLM re-searches on every step, wasting ~50K tokens per redundant cycle

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions