SkillKit — an Elixir framework for building LLM agent systems.
This module is the public API for starting agents, sending messages, and receiving streamed responses.
Quick Start
{:ok, agent} = SkillKit.start_agent("agents/my-agent",
skills: ["skills"],
caller: self()
)
:ok = SkillKit.send_message(agent, "Hello")
receive do
%SkillKit.Event.Delta{text: text} -> IO.write(text)
%SkillKit.Types.AssistantMessage{content: text} -> IO.puts("Done.")
%SkillKit.Event.Error{reason: reason} -> IO.puts("Error")
end
SkillKit.stop_agent(agent)Events
The caller process receives structs directly:
%SkillKit.Event.Delta{agent: name, text: text}— real-time text fragment%SkillKit.Event.ToolCallStart{agent: name, id: id, name: name}— tool call began%SkillKit.Event.ToolCallComplete{agent: name, id: id, name: name, input: input}— tool call parsed%SkillKit.Types.AssistantMessage{agent: name, content: text}— complete response at turn end%SkillKit.Types.ToolResult{agent: name, content: content}— tool result%SkillKit.Event.InputRequested{agent: name, tool_call_id: id}— tool suspended, needs input%SkillKit.Event.Error{agent: name, reason: reason}— LLM or execution error
Configuration
config :skill_kit, SkillKit.LLM,
providers: [
anthropic: SkillKit.LLM.Anthropic
],
default_provider: :anthropic
Summary
Functions
Responds to a suspended tool call with input.
Dispatches a discrete event to the agent for processing as a bounded task.
Sends a user message to the agent referenced by agent.
Sends a message and blocks until the agent responds.
Starts a new agent.
Stops a running agent and all its child processes.
Types
@type agent() :: SkillKit.AgentRef.t()
Functions
Responds to a suspended tool call with input.
When a tool returns {:pending, state}, the caller receives an
%Event.InputRequested{} event. Call respond/3 with the
tool_call_id from the event and the answer to resume execution.
Dispatches a discrete event to the agent for processing as a bounded task.
Events are processed in an isolated sub-loop: the content is treated as
a user message, the sub-loop runs with a scoped tool set and a
configurable initial message history, and intermediate tool calls,
reasoning, and sub-agent events stay inside the sub-loop. The parent
agent's state.messages is NOT mutated by the cast itself.
Surfacing back to the main conversation is opt-in via the send_message
tool. Every event sub-loop is injected with SkillKit.Tools.SendMessage
bound to the parent agent; if the sub-loop's LLM calls it, that
delivers a UserMessage to the parent's mailbox, which then runs a
normal turn and produces an assistant response visible in the main
conversation. If the sub-loop finishes without calling send_message,
the event is handled silently and nothing reaches the main conversation.
This contrasts with send_message/2, which adds to the ongoing
conversation and produces regular assistant turns with all intermediate
steps visible.
Primary caller today is SkillKit.Webhook.Inbox.Memory.put/2, which
emits webhook deliveries to their bound agent with a scoped tool set
(webhook config tool stripped, webhook_inbox injected). Future callers
include cron-like schedulers, admin-initiated runs, and any other
event-driven trigger that wants isolated processing.
Options
:system_append(string, required) — appended to the parent agent's system prompt for the duration of the sub-loop:initial_messages(:empty | :forked | [msg], default:empty) — the message history the sub-loop starts with;:forkedcopies the parent's history (dropping a trailingactivate_skilltool use),:emptystarts fresh:tools_add(list of{module, context}) — extra tools injected into the sub-loop:tools_remove(list of modules) — parent tools stripped from the sub-loop. Does not apply to the auto-injectedsend_messagetool.:skills_remove_prefix(string) — skill namespace prefix to hide fromactivate_skillduring the sub-loop (e.g."webhook:"):allow_activate_skill(boolean, defaultfalse) — whether theactivate_skillmeta-tool is exposed in the sub-loop:sub_agent_name(string, required) — tag applied to events forwarded to the parent's caller so chat printers can attribute them
Returns :ok if the cast was delivered, or {:error, :not_found} if
the agent's server process cannot be found.
Sends a user message to the agent referenced by agent.
Returns :ok if the message was delivered, or {:error, :not_found}
if the agent's mailbox process cannot be found.
@spec send_message_sync(agent(), String.t(), timeout()) :: {:ok, SkillKit.Types.AssistantMessage.t()} | {:error, term()}
Sends a message and blocks until the agent responds.
Returns {:ok, text} on success, {:error, reason} on LLM error,
or {:error, :timeout} if the turn doesn't complete within timeout ms.
Must be called from the process registered as :caller in start_agent/2.
Intermediate events (:delta, :tool_call, :tool_result) remain in
the caller's mailbox and are not consumed.
Examples
{:ok, "Hello!"} = SkillKit.send_message_sync(agent, "Hi")
{:error, :timeout} = SkillKit.send_message_sync(agent, "Hi", 100)
@spec start_agent(SkillKit.Agent.t() | String.t() | {module(), keyword()} | module()) :: {:ok, agent()} | {:error, term()}
Starts a new agent.
The first argument identifies the agent. It accepts:
"path"— string path, resolved as{Kit.Local, dir: "path"}MyModule— bare module, resolved as{MyModule, []}{module, opts}— a kit provider tuple
When the agent is loaded from a kit provider, the kit's skills and sub-agents are automatically included in the tool pool.
Returns {:ok, agent_ref} where agent_ref is an opaque reference
used with send_message/2 and stop_agent/1.
Options
:tools— list of tool providers (default:[]). Tools are always available to the LLM — called directly, no activation needed. Examples:[{SkillKit.Tools.Shell, cwd: "."}].:skills— list of skill providers (default:[]). Skills appear only inactivate_skill's enum. When the LLM activates a skill, a child agent is forked with the skill's underlying tool module added to its:toolslist, so the child can execute it directly.:runtime—{module, config}for agent spawning (default:{Runtime.Local, []}):scope— authorization scope (default:nil):conversation_store—{module, config}for persistence (default:nil):caller— pid for events (default:self()):name— override agent name
@spec stop_agent(agent()) :: :ok
Stops a running agent and all its child processes.