Skip to content

Agents

The agentloop statement executes and manages a session-long AI agent inside your scripted bot, effectively handing off control flow from Bubblescript to the LLM.

Unlike one-off LLM.complete calls, the agent keeps running for the rest of the conversation: user messages are forwarded into the loop, and the model can call declared Bubblescript tasks and dialogs as tools without a hand-written prompt dialog.

Minimal example

dialog main do
  _tools = [
    %{type: "task", task: "get_weather"},
    %{type: "dialog", dialog: "completed"}
  ]
  agentloop(
    LLM.prompt(
      id: "weather_agent",
      text: "system: You are an assistant specializing in the weather. Use [Get weather](urn:tool_call:get_weather) to fetch a forecast, and [Completed](urn:tool_call:completed) when the conversation is over.\nassistant: Greet the user to start.",
      tools: _tools
    )
  )

  say "Very well, glad to be of service."
  close
end

declare task get_weather,
  description: "Get weather forecast information for a city",
  in: %{type: "object", properties: %{city: %{type: "string", description: "The city for the weather report"}}},
  out: %{type: "string", description: "The forecast"}

task get_weather do
  _city = _args.city
  return random(["sun", "rain", "cloud", "tornado"])
end

declare dialog completed,
  description: "When the conversation is over"
dialog completed do
  # continues the conversation after the `agentloop` statement
  continue
end

For background on prompts and tool declarations, see LLM / ChatGPT support and LLM Tool calling.

Starting an agent

Use agentloop(prompt) to start the platform agent loop.

The argument must be a real LLM prompt — either LLM.prompt(...) or a prompt constant such as @prompts.weather_agent from prompts.yaml or an individual prompt file. A plain string is not accepted; the runtime stops with agentloop requires a prompt.

The conversation so far is included automatically. Before the prompt is prepared the runtime appends [[ full_transcript ]], so the turns that came before the agentloop statement reach the agent as real messages rather than as text — the model picks up the conversation instead of starting cold. A prompt that already places [[ transcript ]] or [[ full_transcript ]] itself is left alone, so you keep control over how much history is sent and where it goes. Use LLM.compact() to shorten a long history first.

Tools are passed on the prompt as a list of maps, for example %{type: "task", task: "get_weather"}. This is the same shape as the tools: key in a prompts YAML file.

The declared task follows the same rules as LLM tool calling: a clear description, an input schema (in:), an output schema (out:), and a task implementation. When the model calls the tool, the task runs and its return value is fed back into the agent.

How the loop behaves

After agentloop, the current dialog waits while the agent runs. User messages are forwarded into the loop until a dialog tool stops it (see Stopping the loop).

  • Assistant text is sent to the user as normal bot messages on chat and phone channels.
  • Text user messages are forwarded into the running agent. Attachments, locations, and other non-text input are not supported yet.
  • The agent is not supported on process frontends (master / group).

Dialog tools and scripted handoff

When the model calls a dialog tool (not a task), the runtime pauses the agent loop and runs that Bubblescript dialog on the main interpreter. While the dialog is active:

  • User messages are handled by the dialog (ask, etc.) and are not forwarded to the agent.
  • Bot say text during the dialog is recorded for context but not sent to the agent as separate turns.

When the dialog returns (falls off the end or returns), the agent resumes. The transcript of user and bot lines from that handoff is returned to the model as the dialog tool result. Nested invokes inside the dialog keep the agent paused until the whole tool dialog has popped back to the agentloop await dialog.

The agent waits for the dialog to complete; there is no fixed dialog-tool timeout. Stopping the session or agent loop cancels the wait through the normal process lifecycle.

If the session is stopped while a dialog handoff is in progress, both the interpreter stack and agent state (including the in-progress handoff) are persisted and restored on the next session start.

continue inside a dialog tool still ends the entire agent loop — it is not a resume from handoff.

Stopping the loop

A dialog tool that runs continue exits the agent loop. The agent process is torn down, and execution resumes at the next statement after agentloop. Any dialog tool that continues has this same effect. Task tools cannot stop the loop this way: continue only applies to dialogs.

Errors and unsupported nesting

Agent-loop failures use normal Bubblescript error handling. Define an __error__ dialog when the bot needs to present a custom message or recovery path:

dialog main do
  agentloop(@prompts.support_agent)

  dialog __error__ do
    log("Agent loop failed: #{error.message}")
    say("Sorry, I cannot continue this conversation right now.")
  end
end

The following conditions raise a runtime error:

  • the value passed to agentloop is not an LLM prompt;
  • rendering or preparing the prompt fails;
  • the frontend or selected engine is unsupported;
  • the running agent terminates because of a provider or process failure; and
  • another agentloop is started while one is already active.

Dialog tools may invoke nested Bubblescript dialogs, but they cannot start a nested agent loop. Attempting that raises nested agentloop is not supported.

Legacy engine

agentloop(:legacy, prompt) selects the legacy engine, available for migration only. It requires include: [legacy_agentloop] in app.dev and replaces the provider and model on the supplied prompt with @llm_smart_model. It also exposes the overridable pre_perform_tool_calls dialog and the on_agent_llm_error task hook, which the default engine does not.