SkillKit (SkillKit v0.1.0)

Copy Markdown View Source

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

agent()

@type agent() :: SkillKit.AgentRef.t()

Functions

respond(agent, tool_call_id, answer)

@spec respond(agent(), String.t(), any()) :: :ok | {:error, :not_found}

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.

send_event(agent, content, opts)

@spec send_event(agent(), String.t(), keyword()) :: :ok | {:error, :not_found}

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; :forked copies the parent's history (dropping a trailing activate_skill tool use), :empty starts 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-injected send_message tool.
  • :skills_remove_prefix (string) — skill namespace prefix to hide from activate_skill during the sub-loop (e.g. "webhook:")
  • :allow_activate_skill (boolean, default false) — whether the activate_skill meta-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.

send_message(agent, content)

@spec send_message(agent(), String.t()) :: :ok | {:error, :not_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.

send_message_sync(agent, content, timeout \\ 5000)

@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)

start_agent(source)

@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 in activate_skill's enum. When the LLM activates a skill, a child agent is forked with the skill's underlying tool module added to its :tools list, 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

start_agent(source, opts)

@spec start_agent(
  SkillKit.Agent.t() | String.t() | {module(), keyword()} | module(),
  keyword()
) :: {:ok, agent()} | {:error, term()}

stop_agent(agent_ref)

@spec stop_agent(agent()) :: :ok

Stops a running agent and all its child processes.