Packages

Elixir port of Google's Agent Development Kit (ADK) — agent orchestration, sessions, tools, LLM abstraction, plugins, and telemetry. Transport-agnostic.

Current section

Files

Jump to
adk_ex lib adk agent llm_agent.ex
Raw

lib/adk/agent/llm_agent.ex

defmodule ADK.Agent.LlmAgent do
@moduledoc """
An agent backed by a large language model.
Combines a model, tools, instructions, and callbacks into a complete
agent that can have multi-turn conversations and call tools.
"""
@behaviour ADK.Agent
alias ADK.Agent.{CallbackContext, InvocationContext}
alias ADK.Event
alias ADK.Flow
alias ADK.Flow.Processors.{AgentTransfer, Basic, Contents, Instructions, ToolProcessor}
alias ADK.Plugin.Manager, as: PluginManager
alias ADK.Types.Content
@type t :: %__MODULE__{
name: String.t(),
model: struct(),
description: String.t(),
instruction: String.t(),
global_instruction: String.t(),
instruction_provider: (InvocationContext.t() -> String.t()) | nil,
global_instruction_provider: (InvocationContext.t() -> String.t()) | nil,
output_key: String.t() | nil,
output_schema: map() | nil,
generate_content_config: map(),
tools: [struct()],
toolsets: [struct()],
sub_agents: [struct()],
include_contents: :default | :none,
disallow_transfer_to_parent: boolean(),
disallow_transfer_to_peers: boolean(),
before_agent_callbacks: [term()],
after_agent_callbacks: [term()],
before_model_callbacks: [Flow.before_model_callback()],
after_model_callbacks: [Flow.after_model_callback()],
on_model_error_callbacks: [term()],
before_tool_callbacks: [Flow.before_tool_callback()],
after_tool_callbacks: [Flow.after_tool_callback()],
on_tool_error_callbacks: [term()]
}
@enforce_keys [:name, :model]
defstruct [
:name,
:model,
:instruction_provider,
:global_instruction_provider,
:output_key,
:output_schema,
description: "",
instruction: "",
global_instruction: "",
generate_content_config: %{},
tools: [],
toolsets: [],
sub_agents: [],
include_contents: :default,
disallow_transfer_to_parent: false,
disallow_transfer_to_peers: false,
before_agent_callbacks: [],
after_agent_callbacks: [],
before_model_callbacks: [],
after_model_callbacks: [],
on_model_error_callbacks: [],
before_tool_callbacks: [],
after_tool_callbacks: [],
on_tool_error_callbacks: []
]
@impl ADK.Agent
def name(%__MODULE__{name: name}), do: name
@impl ADK.Agent
def description(%__MODULE__{description: desc}), do: desc
@impl ADK.Agent
def sub_agents(%__MODULE__{sub_agents: agents}), do: agents
@impl ADK.Agent
def run(%__MODULE__{} = agent, %InvocationContext{} = ctx) do
Stream.resource(
fn -> {:before, agent, ctx} end,
&next/1,
fn _ -> :ok end
)
end
defp next(:done), do: {:halt, :done}
defp next({:before, agent, ctx}) do
cb_ctx = CallbackContext.new(ctx)
pm = ctx.plugin_manager
# Plugin before_agent runs first
case PluginManager.run_before_agent(pm, cb_ctx) do
{%Content{} = content, updated_cb_ctx} ->
event = make_callback_event(ctx, agent, content, updated_cb_ctx)
{[event], :done}
{nil, plugin_cb_ctx} ->
# Then agent before_agent callbacks
case run_callbacks(agent.before_agent_callbacks, plugin_cb_ctx) do
{:short_circuit, content, updated_cb_ctx} ->
event = make_callback_event(ctx, agent, content, updated_cb_ctx)
{[event], :done}
{:continue, _cb_ctx} ->
flow = build_flow(agent)
events = flow |> Flow.run(ctx) |> Enum.to_list()
events = maybe_save_output(events, agent)
{events, {:after, agent, ctx}}
end
end
end
defp next({:after, agent, ctx}) do
cb_ctx = CallbackContext.new(ctx)
pm = ctx.plugin_manager
# Plugin after_agent runs first
case PluginManager.run_after_agent(pm, cb_ctx) do
{%Content{} = content, updated_cb_ctx} ->
event = make_callback_event(ctx, agent, content, updated_cb_ctx)
{[event], :done}
{nil, plugin_cb_ctx} ->
# Then agent after_agent callbacks
case run_callbacks(agent.after_agent_callbacks, plugin_cb_ctx) do
{:short_circuit, content, updated_cb_ctx} ->
event = make_callback_event(ctx, agent, content, updated_cb_ctx)
{[event], :done}
{:continue, _cb_ctx} ->
{[], :done}
end
end
end
defp build_flow(agent) do
%Flow{
model: agent.model,
tools: agent.tools,
toolsets: agent.toolsets,
request_processors: [
&Basic.process/3,
&ToolProcessor.process/3,
&Instructions.process/3,
&AgentTransfer.process/3,
&Contents.process/3
],
before_model_callbacks: agent.before_model_callbacks,
after_model_callbacks: agent.after_model_callbacks,
on_model_error_callbacks: agent.on_model_error_callbacks,
before_tool_callbacks: agent.before_tool_callbacks,
after_tool_callbacks: agent.after_tool_callbacks,
on_tool_error_callbacks: agent.on_tool_error_callbacks
}
end
defp run_callbacks([], cb_ctx), do: {:continue, cb_ctx}
defp run_callbacks([callback | rest], cb_ctx) do
case callback.(cb_ctx) do
{%Content{} = content, updated_ctx} -> {:short_circuit, content, updated_ctx}
{nil, updated_ctx} -> run_callbacks(rest, updated_ctx)
end
end
defp make_callback_event(ctx, agent, content, cb_ctx) do
Event.new(
invocation_id: ctx.invocation_id,
branch: ctx.branch,
author: agent.name,
content: content,
actions: cb_ctx.actions
)
end
defp maybe_save_output(events, %__MODULE__{output_key: nil}), do: events
defp maybe_save_output(events, %__MODULE__{output_key: key}) do
case find_last_model_text(events) do
nil ->
events
text ->
# Update the last event's state_delta with the output key
{init, [last]} = Enum.split(events, -1)
actions = %{last.actions | state_delta: Map.put(last.actions.state_delta, key, text)}
init ++ [%{last | actions: actions}]
end
end
defp find_last_model_text(events) do
events
|> Enum.reverse()
|> Enum.find_value(fn event ->
if event.content && event.content.role == "model" do
extract_text(event.content)
end
end)
end
defp extract_text(%Content{parts: parts}) do
text =
parts
|> Enum.map_join("", fn part ->
if part.text, do: part.text, else: ""
end)
if text == "", do: nil, else: text
end
end