{"id":48473985,"url":"https://github.com/beardedeagle/beam-agent","last_synced_at":"2026-04-07T07:49:05.692Z","repository":{"id":343205100,"uuid":"1175588387","full_name":"beardedeagle/beam-agent","owner":"beardedeagle","description":"Canonical BEAM SDK for agentic coding runtimes in Erlang and Elixir. One unified API over Claude Code, Codex, Gemini, OpenCode, and Copilot, with shared sessions, threads, MCP, hooks, telemetry, and backend-native escape hatches.","archived":false,"fork":false,"pushed_at":"2026-03-30T17:30:06.000Z","size":2354,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-07T07:49:01.897Z","etag":null,"topics":["ai-agents","ai-assistant","ai-tools","beam","claude","codex","copilot","elixir","erlang","gemini-cli","mcp","opencode","otp","sdk"],"latest_commit_sha":null,"homepage":"https://beardedeagle.github.io/beam-agent/","language":"Erlang","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/beardedeagle.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-07T22:56:00.000Z","updated_at":"2026-03-30T12:46:16.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/beardedeagle/beam-agent","commit_stats":null,"previous_names":["beardedeagle/beam-agent"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/beardedeagle/beam-agent","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beardedeagle%2Fbeam-agent","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beardedeagle%2Fbeam-agent/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beardedeagle%2Fbeam-agent/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beardedeagle%2Fbeam-agent/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/beardedeagle","download_url":"https://codeload.github.com/beardedeagle/beam-agent/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beardedeagle%2Fbeam-agent/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31504897,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T03:10:19.677Z","status":"ssl_error","status_checked_at":"2026-04-07T03:10:13.982Z","response_time":105,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agents","ai-assistant","ai-tools","beam","claude","codex","copilot","elixir","erlang","gemini-cli","mcp","opencode","otp","sdk"],"created_at":"2026-04-07T07:49:05.082Z","updated_at":"2026-04-07T07:49:05.682Z","avatar_url":"https://github.com/beardedeagle.png","language":"Erlang","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\".github/social-preview.png\" alt=\"BEAM Agent\" width=\"100%\"\u003e\n\u003c/p\u003e\n\n# BEAM Agent\n\nCanonical BEAM SDKs for integrating subscription-backed coding agents into\nErlang/OTP and Elixir applications.\n\nThe canonical public SDK surfaces are now:\n\n- `beam_agent` for Erlang\n- `BeamAgent` for Elixir\n\nThey let callers choose **Claude Code**, **Codex CLI**, **Gemini CLI**,\n**OpenCode**, or **GitHub Copilot** at runtime while working against one\ncapability-oriented API surface.\n\n## Architecture\n\nAll five backends share a three-layer architecture:\n\n```\nConsumer → beam_agent / BeamAgent (canonical public API)\n         → beam_agent_session_engine (gen_statem — lifecycle, queue, telemetry)\n         → beam_agent_session_handler callbacks (per-backend protocol logic)\n         → beam_agent_transport (byte I/O — port, HTTP, WebSocket)\n```\n\nEach backend facade module implements the `beam_agent_adapter` behaviour\n(identity and capabilities) and the `beam_agent_adapter_session` sub-behaviour\n(session lifecycle). Under the hood, a `beam_agent_session_handler` callback\nmodule handles the wire protocol for the engine. The engine provides all shared\norchestration (state machine, consumer/queue, telemetry, error recovery) so\nhandlers focus only on what is unique to their backend's wire protocol. Zero\nadditional processes — the engine gen_statem IS the session process.\n\n```\n                     +----------------------+\n                     |      beam_agent      |\n                     | canonical Erlang SDK |\n                     +----------+-----------+\n                                |\n               +----------------+----------------+\n               |  beam_agent_session_engine      |\n               |  (gen_statem: lifecycle/queue/   |\n               |   telemetry/error recovery)     |\n               +----------------+----------------+\n                                |\n       +-------------+----------+----------+-------------+-------------+\n       |             |                     |             |             |\n +-----+-----+ +-----+-----+         +-----+-----+ +-----+-----+ +-----+-----+\n | Claude    | | Codex     |         | Gemini    | | OpenCode  | | Copilot   |\n | handler   | | handler   |         | handler   | | handler   | | handler   |\n | port/jsonl| | port/rpc  |         | port/rpc  | | http/sse  | | port/jsonrpc|\n +-----------+ +-----------+         +-----------+ +-----------+ +-----------+\n```\n\nThose backend handlers are internal implementation modules inside the single\n`beam_agent` project, not separate SDK packages.\n\nAll five handlers normalize messages into `beam_agent:message()` — a common map\ntype you can pattern-match on regardless of which agent you're talking to.\n\n`beam_agent` is not only shared plumbing. It is the union-capability layer the\nrepo is building and verifying toward: when a backend supports a feature\nnatively, the handler can route to that implementation; when it does not,\n`beam_agent` provides the universal fallback recorded in the architecture\nmatrices.\n\nTo add a new agentic backend, implement three modules: a facade implementing\n`beam_agent_adapter`, a session module implementing `beam_agent_adapter_session`,\nand a handler implementing `beam_agent_session_handler`. Stateless API backends\nimplement `beam_agent_adapter` plus `beam_agent_adapter_api` instead. See the\n[Backend Integration Guide](docs/guides/backend_integration_guide.md) for a\ncomplete walkthrough.\n\n## Quick Start\n\n### Erlang\n\nAdd the canonical SDK to your `rebar.config` deps:\n\n```erlang\n{deps, [\n    {beam_agent, {path, \".\"}}\n]}.\n```\n\n```erlang\n%% Start a routed session through the canonical SDK\n{ok, Session} = beam_agent:start_session(#{\n    backend =\u003e auto,\n    routing =\u003e #{\n        policy =\u003e preferred_then_fallback,\n        preferred_backends =\u003e [claude, codex]\n    },\n    cli_path =\u003e \"/usr/local/bin/claude\",\n    permission_mode =\u003e \u003c\u003c\"bypassPermissions\"\u003e\u003e\n}),\n\n%% Blocking query — returns all messages\n{ok, Messages} = beam_agent:query(Session, \u003c\u003c\"Explain OTP supervisors\"\u003e\u003e),\n\n%% Find the result\n[Result | _] = [M || #{type := result} = M \u003c- Messages],\nio:format(\"~s~n\", [maps:get(content, Result, \u003c\u003c\u003e\u003e)]),\n\nbeam_agent:stop(Session).\n```\n\n### Elixir\n\n```elixir\n# In mix.exs\ndefp deps do\n  [{:beam_agent_ex, path: \"beam_agent_ex\"}]\nend\n```\n\n```elixir\n{:ok, session} =\n  BeamAgent.start_session(\n    backend: :auto,\n    routing: %{policy: :preferred_then_fallback, preferred_backends: [:claude, :codex]},\n    cli_path: \"claude\"\n  )\n\n# Streaming query — lazy enumerable\nsession\n|\u003e BeamAgent.stream!(\"Explain GenServer\")\n|\u003e Enum.each(fn msg -\u003e\n  case msg.type do\n    :text -\u003e IO.write(msg.content)\n    :result -\u003e IO.puts(\"\\n--- Done ---\")\n    _ -\u003e :ok\n  end\nend)\n\nBeamAgent.stop(session)\n```\n\nBackend-specific wrappers such as `ClaudeEx`, `CodexEx`, `GeminiEx`,\n`OpencodeEx`, and `CopilotEx` still exist. Use them when you want a preset\nbackend boundary or direct access to backend-native APIs from within the single\n`beam_agent_ex` package.\n\n## Adapters at a Glance\n\n| Adapter | CLI | Transport | Protocol | Bidirectional |\n|---------|-----|-----------|----------|---------------|\n| `claude_agent_sdk` | `claude` | Port | JSONL | Yes (control protocol) |\n| `codex_app_server` | `codex` | Port / WebSocket | JSON-RPC / JSONL / Realtime WS | Yes (app-server or direct realtime) or No (exec) |\n| `gemini_cli_client` | `gemini --experimental-acp` | Port | JSON-RPC over NDJSON | Yes (persistent ACP session) |\n| `opencode_client` | `opencode serve` | HTTP + SSE | REST + SSE | Yes |\n| `copilot_client` | `copilot` | Port | JSON-RPC / Content-Length | Yes (bidirectional) |\n\n## Canonical API Surface\n\n`beam_agent` / `BeamAgent` expose the shared lifecycle/query surface directly:\n\n```erlang\nstart_session(Opts)    -\u003e {ok, Pid} | {error, Reason}\nstop(Pid)              -\u003e ok\nquery(Pid, Prompt)     -\u003e {ok, [Message]} | {error, Reason}\nquery(Pid, Prompt, Params) -\u003e {ok, [Message]} | {error, Reason}\nhealth(Pid)            -\u003e ready | connecting | initializing | active_query | error\nsession_info(Pid)      -\u003e {ok, Map} | {error, Reason}\nset_model(Pid, Model)  -\u003e {ok, term()} | {error, term()}\nset_permission_mode(Pid, Mode) -\u003e {ok, term()} | {error, term()}\nsession_capabilities(Pid) -\u003e {ok, [Capability]} | {error, term()}\nchild_spec(Opts)       -\u003e supervisor:child_spec()\n```\n\n`start_session/1` accepts either an explicit backend or `backend =\u003e auto`\nplus a `routing` request map. The canonical routing domain is exposed through\n`beam_agent_routing` / `BeamAgent.Routing`.\n\nScheduled execution is exposed through `beam_agent_routines` /\n`BeamAgent.Routines`. The routines layer stores durable job records and\nprovides explicit `run_due/1` entrypoints for caller-owned schedulers; it\ndoes not start a hidden BeamAgent scheduler process.\n\nParent-child orchestration is exposed through `beam_agent_orchestrator` /\n`BeamAgent.Orchestrator`. The orchestrator layer records delegation lineage,\ncross-session child relationships, and collection/cancellation status without\nstarting a worker pool or resident BeamAgent process.\n\nElixir adds `stream!/3` and `stream/3` (lazy `Stream.resource/3`-based\nenumerables) on top of the same canonical surface.\n\nBeyond the lifecycle/query surface, the canonical SDK exposes the following\ncapability families through domain modules (`beam_agent_session_store`,\n`beam_agent_threads`, `beam_agent_runtime`, `beam_agent_config`,\n`beam_agent_provider`, `beam_agent_catalog`, `beam_agent_capabilities`,\n`beam_agent_command`, `beam_agent_command_validator`, `beam_agent_control`, `beam_agent_mcp`,\n`beam_agent_skills`, `beam_agent_artifacts`,\n`beam_agent_context`, `beam_agent_journal`, `beam_agent_memory`,\n`beam_agent_orchestrator`, `beam_agent_routing`, `beam_agent_routines`,\n`beam_agent_checkpoint`, `beam_agent_hooks`, `beam_agent_policy`, `beam_agent_runs`,\n`beam_agent_telemetry`).\nTheir status and route shape for each\nbackend/capability pair are tracked via\n`support_level`, `implementation`, and `fidelity` in the capability registry.\nAll families have universal fallback coverage:\n\n- shared session history/state\n- shared/native thread management\n- canonical run and step lifecycle\n- typed artifact and context storage\n- context pressure estimation and policy-driven compaction\n- durable canonical domain-event journal\n- durable audit records layered on the journal\n- long-term memory with lexical recall and expiry\n- internal store abstraction with ETS as the default canonical adapter\n- reusable policy profiles for approvals, commands, backends, routines,\n  memory writes, compaction, and orchestration\n- policy-driven backend routing with explicit, sticky, round-robin, failover,\n  capability-first, and preferred-then-fallback selection\n- durable routines and caller-driven scheduled execution\n- parent-child orchestration, delegation lineage, and run collection\n- runtime provider and agent defaults\n- universal config/provider fallbacks for backends without native admin APIs\n- universal review/realtime participation for backends without native review APIs\n- attachment materialization with size gating (512 KB default, configurable): native content blocks for Claude, canonical blocks for Gemini, text fallback for unknown backends\n- catalog accessors for tools/skills/plugins/agents\n- capability introspection (`support_level`, `implementation`, `fidelity`)\n- raw native escape hatches for backend-specific APIs\n- backend event streaming through native transports or the universal event bus\n\nShared session history/state and thread management:\n\n```erlang\n%% Universal session history/state — beam_agent_session_store\nbeam_agent_session_store:list_sessions(Opts)                    -\u003e {ok, [SessionMeta]}\nbeam_agent_session_store:get_session(SessionId)                 -\u003e {ok, SessionMeta} | {error, not_found}\nbeam_agent_session_store:get_session_messages(SessionId, Opts)  -\u003e {ok, [Message]} | {error, not_found}\nbeam_agent_session_store:delete_session(SessionId)              -\u003e ok\nbeam_agent_session_store:fork_session(SessionId, Opts)         -\u003e {ok, SessionMeta} | {error, not_found}\nbeam_agent_session_store:revert_session(SessionId, Selector)   -\u003e {ok, SessionMeta} | {error, not_found | invalid_selector}\nbeam_agent_session_store:unrevert_session(SessionId)           -\u003e {ok, SessionMeta} | {error, not_found}\nbeam_agent_session_store:share_session(SessionId, Opts)        -\u003e {ok, session_share()} | {error, not_found}\nbeam_agent_session_store:unshare_session(SessionId)            -\u003e ok | {error, not_found}\nbeam_agent_session_store:summarize_session(SessionId, Opts)    -\u003e {ok, session_summary()} | {error, not_found}\nbeam_agent_session_store:export_session(SessionId)             -\u003e {ok, exported_session()} | {error, not_found}\nbeam_agent_session_store:import_session(Exported)              -\u003e {ok, session_meta()} | {error, Reason}\nbeam_agent_session_store:import_session(Exported, Opts)        -\u003e {ok, session_meta()} | {error, Reason}\n\n%% Universal/native thread state — beam_agent_threads\nbeam_agent_threads:thread_start(Session, Opts)         -\u003e {ok, ThreadMeta} | {error, Reason}\nbeam_agent_threads:thread_resume(Session, ThreadId)    -\u003e {ok, ThreadMeta} | {error, not_found}\nbeam_agent_threads:thread_list(Session)                -\u003e {ok, [ThreadMeta]} | {error, Reason}\nbeam_agent_threads:thread_fork(Session, ThreadId, Opts)-\u003e {ok, ThreadMeta} | {error, not_found | message_limit_reached}\nbeam_agent_threads:thread_read(Session, ThreadId, Opts)-\u003e {ok, map()} | {error, not_found}\nbeam_agent_threads:thread_archive(Session, ThreadId)   -\u003e {ok, map()} | {error, not_found}\nbeam_agent_threads:thread_unarchive(Session, ThreadId) -\u003e {ok, map()} | {error, not_found}\nbeam_agent_threads:thread_rollback(Session, ThreadId, Selector) -\u003e\n    {ok, map()} | {error, Reason}\n\n%% Canonical runs/steps -- beam_agent_runs\nbeam_agent_runs:start_run(Scope, Opts)                   -\u003e {ok, Run} | {error, Reason}\nbeam_agent_runs:get_run(RunId)                           -\u003e {ok, Run} | {error, not_found}\nbeam_agent_runs:list_runs(Filter)                        -\u003e {ok, [Run]} | {error, Reason}\nbeam_agent_runs:complete_run(RunId, Result)              -\u003e {ok, Run} | {error, Reason}\nbeam_agent_runs:fail_run(RunId, ErrorTerm)               -\u003e {ok, Run} | {error, Reason}\nbeam_agent_runs:cancel_run(RunId, Reason)                -\u003e {ok, Run} | {error, Reason}\nbeam_agent_runs:start_step(RunId, Opts)                  -\u003e {ok, Step} | {error, Reason}\nbeam_agent_runs:get_step(RunId, StepId)                  -\u003e {ok, Step} | {error, not_found}\nbeam_agent_runs:list_steps(RunId)                        -\u003e {ok, [Step]} | {error, not_found}\nbeam_agent_runs:complete_step(RunId, StepId, Result)     -\u003e {ok, Step} | {error, Reason}\nbeam_agent_runs:fail_step(RunId, StepId, ErrorTerm)      -\u003e {ok, Step} | {error, Reason}\nbeam_agent_runs:cancel_step(RunId, StepId, Reason)       -\u003e {ok, Step} | {error, Reason}\n\n%% Canonical artifacts -- beam_agent_artifacts\nbeam_agent_artifacts:put(Artifact)                       -\u003e {ok, ArtifactRecord} | {error, Reason}\nbeam_agent_artifacts:get(ArtifactId)                     -\u003e {ok, ArtifactRecord} | {error, not_found}\nbeam_agent_artifacts:list(Filter)                        -\u003e {ok, [ArtifactRecord]} | {error, Reason}\nbeam_agent_artifacts:search(Query)                       -\u003e {ok, [ArtifactRecord]}\nbeam_agent_artifacts:search(Query, Filter)               -\u003e {ok, [ArtifactRecord]} | {error, Reason}\nbeam_agent_artifacts:attach(ArtifactId, RefType, RefId)  -\u003e ok | {error, Reason}\nbeam_agent_artifacts:delete(ArtifactId)                  -\u003e ok | {error, not_found}\n\n%% Durable canonical journal -- beam_agent_journal\nbeam_agent_journal:append(EventType, Event)              -\u003e {ok, Entry} | {error, Reason}\nbeam_agent_journal:list(Filter)                          -\u003e {ok, [Entry]} | {error, Reason}\nbeam_agent_journal:stream_from(Cursor, Filter)           -\u003e {ok, [Entry]} | {error, Reason}\nbeam_agent_journal:get(EventId)                          -\u003e {ok, Entry} | {error, not_found}\nbeam_agent_journal:ack(ConsumerId, EventId)              -\u003e ok | {error, not_found}\n\n%% Canonical audit (layered on journal) -- beam_agent_journal\nbeam_agent_journal:list_events(Filter)                   -\u003e {ok, [Entry]} | {error, Reason}\nbeam_agent_journal:get_event(EventId)                    -\u003e {ok, Entry} | {error, not_found}\n\n%% Long-term memory -- beam_agent_memory\nbeam_agent_memory:remember(Scope, MemoryInput)           -\u003e {ok, Memory} | {error, Reason}\nbeam_agent_memory:remember(Scope, Kind, MemoryInput)     -\u003e {ok, Memory} | {error, Reason}\nbeam_agent_memory:get(MemoryId)                          -\u003e {ok, Memory} | {error, not_found}\nbeam_agent_memory:list(Filter)                           -\u003e {ok, [Memory]} | {error, Reason}\nbeam_agent_memory:recall(Scope, Query)                   -\u003e {ok, [Memory]} | {error, Reason}\nbeam_agent_memory:search(Query, Filter)                  -\u003e {ok, [Memory]} | {error, Reason}\nbeam_agent_memory:forget(MemoryId)                       -\u003e ok | {error, not_found}\nbeam_agent_memory:update(MemoryId, Changes)              -\u003e {ok, Memory} | {error, Reason}\nbeam_agent_memory:pin(MemoryId)                          -\u003e ok | {error, not_found}\nbeam_agent_memory:unpin(MemoryId)                        -\u003e ok | {error, not_found}\nbeam_agent_memory:expire(Filter)                         -\u003e {ok, Count} | {error, Reason}\nbeam_agent_memory:configure_persistence(StoreConfig)     -\u003e ok | {error, Reason}\n\n%% Canonical backend routing -- beam_agent_routing\nbeam_agent_routing:select_backend(RouteRequest)          -\u003e {ok, Decision} | {error, Reason}\nbeam_agent_routing:select_backend(SessionOrOpts, RouteRequest) -\u003e\n    {ok, Decision} | {error, Reason}\n\n%% Canonical policy profiles -- beam_agent_policy\nbeam_agent_policy:put_profile(ProfileId, Profile)        -\u003e ok | {error, Reason}\nbeam_agent_policy:get_profile(ProfileId)                 -\u003e {ok, Profile} | {error, not_found}\nbeam_agent_policy:list_profiles()                        -\u003e {ok, [Profile]}\nbeam_agent_policy:evaluate(ProfileId, Action, Context)   -\u003e allow | {deny, Reason}\n\n%% Canonical routines -- beam_agent_routines\nbeam_agent_routines:create(Job)                          -\u003e {ok, JobRecord} | {error, Reason}\nbeam_agent_routines:update(JobId, Patch)                -\u003e {ok, JobRecord} | {error, Reason}\nbeam_agent_routines:list_due(Filter)                     -\u003e {ok, [JobRecord]} | {error, Reason}\nbeam_agent_routines:run_due(Opts)                       -\u003e {ok, [map()]} | {error, Reason}\nbeam_agent_routines:run_now(JobId)                      -\u003e {ok, Run} | {error, Reason}\nbeam_agent_routines:next_due_at()                       -\u003e {ok, DueAt} | {error, none}\n\n%% Canonical orchestration -- beam_agent_orchestrator\nbeam_agent_orchestrator:spawn(Parent, Opts)             -\u003e {ok, Child} | {error, Reason}\nbeam_agent_orchestrator:delegate(Parent, Task, Opts)    -\u003e {ok, Run} | {error, Reason}\nbeam_agent_orchestrator:await(RunId, Timeout)           -\u003e {ok, Result} | {error, Reason}\nbeam_agent_orchestrator:collect(RunId, Opts)            -\u003e {ok, map()} | {error, Reason}\nbeam_agent_orchestrator:cancel(RunId, Reason)           -\u003e ok | {error, Reason}\nbeam_agent_orchestrator:status(RunId)                   -\u003e {ok, map()} | {error, not_found}\nbeam_agent_orchestrator:list_children(Parent)           -\u003e {ok, [map()]} | {error, Reason}\n\n%% Canonical context management -- beam_agent_context\nbeam_agent_context:context_status(SessionOrThread)       -\u003e {ok, Status} | {error, Reason}\nbeam_agent_context:budget_estimate(SessionOrThread)      -\u003e {ok, Budget} | {error, Reason}\nbeam_agent_context:compact_now(SessionOrThread, Opts)    -\u003e {ok, Result} | {error, Reason}\nbeam_agent_context:maybe_compact(SessionOrThread, Opts)  -\u003e {ok, Result} | {error, Reason}\n```\n\nThe Elixir `BeamAgent` wrapper exposes those stores directly through\n`BeamAgent.SessionStore`, `BeamAgent.Threads`, `BeamAgent.Runs`, and\n`BeamAgent.Artifacts`, `BeamAgent.Context`,\n`BeamAgent.Memory`, `BeamAgent.Orchestrator`, `BeamAgent.Policy`,\n`BeamAgent.Routing`, `BeamAgent.Routines`, and\nthe runtime/catalog layers through `BeamAgent.Runtime`,\n`BeamAgent.Catalog`, `BeamAgent.Capabilities`, and `BeamAgent.Raw`.\nInternally, the newer canonical stores route through `beam_agent_store` with\n`beam_agent_store_ets` as the default adapter and `beam_agent_store_dets` as\na durable disk-backed alternative. Both adapters preserve the existing\nprocess-free reads plus hardened table-owner write sharding behavior. The DETS\nadapter supports an `atomic_counters` option that uses OTP `atomics` for\nlock-free concurrent counter increments. Elixir callers use `BeamAgent.Store`\nfor domain configuration and DETS helpers.\n\nFor a domain-by-domain explanation of ownership, storage, and process\nboundaries, see [docs/guides/canonical_domain_guide.md](docs/guides/canonical_domain_guide.md).\n\n## Unified Message Format\n\nAll adapters normalize messages to `beam_agent:message()`:\n\n```erlang\n#{type := text, content := \u003c\u003c\"Hello!\"\u003e\u003e}\n#{type := tool_use, tool_name := \u003c\u003c\"Bash\"\u003e\u003e, tool_input := #{...}}\n#{type := tool_result, tool_name := \u003c\u003c\"Bash\"\u003e\u003e, content := \u003c\u003c\"output...\"\u003e\u003e}\n#{type := result, content := \u003c\u003c\"Final answer\"\u003e\u003e, duration_ms := 5432}\n#{type := error, content := \u003c\u003c\"Something went wrong\"\u003e\u003e, category := unknown}\n#{type := error, content := \u003c\u003c\"Rate limit exceeded\"\u003e\u003e, category := rate_limit, retry_after := 30}\n#{type := thinking, content := \u003c\u003c\"Let me consider...\"\u003e\u003e}\n#{type := system, subtype := \u003c\u003c\"init\"\u003e\u003e, system_info := #{...}}\n```\n\nPattern match on `type` for dispatch:\n\n```erlang\nhandle_message(#{type := text, content := Content}) -\u003e\n    io:format(\"~s\", [Content]);\nhandle_message(#{type := tool_use, tool_name := Name}) -\u003e\n    io:format(\"Using tool: ~s~n\", [Name]);\nhandle_message(#{type := error, category := rate_limit} = Msg) -\u003e\n    Retry = maps:get(retry_after, Msg, 60),\n    io:format(\"Rate limited — retry in ~B seconds~n\", [Retry]);\nhandle_message(#{type := error, category := Cat, content := Content}) -\u003e\n    io:format(\"Error [~p]: ~s~n\", [Cat, Content]);\nhandle_message(#{type := result} = Msg) -\u003e\n    io:format(\"Done! Cost: $~.4f~n\", [maps:get(total_cost_usd, Msg, 0.0)]);\nhandle_message(_Other) -\u003e\n    ok.\n```\n\n## SDK Features\n\n### Structured Error Categorization\n\nEvery error message carries a `category` atom for structured error handling\nwithout content-text parsing. Categories are inferred automatically from\nwire-format data (when the backend provides structured error info) or from\ncontent text pattern matching as a universal fallback.\n\n| Category | Meaning |\n|----------|---------|\n| `rate_limit` | Too many requests / 429 / throttled |\n| `subscription_exhausted` | Quota, billing, or credit limit reached |\n| `context_exceeded` | Context window or token limit exceeded |\n| `auth_expired` | Authentication or authorization failure |\n| `server_error` | Backend 5xx / overloaded / unavailable |\n| `unknown` | Unrecognized error (fallback) |\n\nWhen available, `retry_after` (integer seconds) is also attached.\n\n```erlang\nhandle_error(#{category := rate_limit, retry_after := Secs}) -\u003e\n    timer:sleep(Secs * 1000),\n    retry;\nhandle_error(#{category := context_exceeded}) -\u003e\n    compact_and_retry;\nhandle_error(#{category := auth_expired}) -\u003e\n    {stop, reauthenticate};\nhandle_error(#{category := Cat, content := Content}) -\u003e\n    logger:warning(\"~p error: ~s\", [Cat, Content]),\n    {stop, Cat}.\n```\n\n### Backend Event Streams\n\nThe public API exposes a canonical event-stream interface for every backend.\n\nErlang:\n\n```erlang\nok = beam_agent:event_subscribe(Session),\nreceive\n    {beam_agent_event, Session, Event} -\u003e\n        io:format(\"Event: ~p~n\", [Event])\nafter 30_000 -\u003e\n    timeout\nend,\nok = beam_agent:event_unsubscribe(Session, self()).\n```\n\n`receive_event/2,3` provides a convenience wrapper with an optional timeout:\n\n```erlang\n{ok, Event} = beam_agent:receive_event(Session, 30_000).\n```\n\nElixir:\n\n```elixir\nsession\n|\u003e BeamAgent.event_stream!(timeout: 30_000)\n|\u003e Enum.each(\u0026IO.inspect/1)\n```\n\nBackends with richer native event feeds keep them. For the rest, BeamAgent\nprovides the shared event-bus fallback documented in\n`docs/architecture/backend_conformance_matrix.md`.\n\n### Codex Direct Realtime Voice\n\nCodex can also run a direct realtime session instead of the app-server path:\n\n```erlang\n{ok, Session} = beam_agent:start_session(#{\n    backend =\u003e codex,\n    transport =\u003e realtime,\n    api_key =\u003e \u003c\u003c\"sk-live-key\"\u003e\u003e,\n    voice =\u003e \u003c\u003c\"alloy\"\u003e\u003e\n}),\n{ok, #{thread_id := ThreadId}} =\n    beam_agent_control:thread_realtime_start(Session, #{mode =\u003e \u003c\u003c\"voice\"\u003e\u003e}),\nok = beam_agent_control:thread_realtime_append_text(Session, ThreadId, #{text =\u003e \u003c\u003c\"Hello\"\u003e\u003e}),\nok = beam_agent:stop(Session).\n```\n\nThat path uses the direct realtime websocket transport for Codex-native\naudio/text sessions while the app-server transport remains available for the\nbroader CLI control-plane surface.\n\nRealtime sessions support the full SDK hook lifecycle — `session_start`,\n`session_end`, `user_prompt_submit` (blocking), `stop`, and\n`post_tool_use_failure` — so the same hooks registered for app-server\nsessions fire in realtime mode as well:\n\n```erlang\nHook = beam_agent_hooks:hook(user_prompt_submit, fun(Ctx) -\u003e\n    %% Inspect or modify the prompt before it's sent over WebSocket\n    {ok, Ctx}\nend),\n{ok, Session} = beam_agent:start_session(#{\n    backend =\u003e codex,\n    transport =\u003e realtime,\n    api_key =\u003e \u003c\u003c\"sk-live-key\"\u003e\u003e,\n    sdk_hooks =\u003e [Hook]\n}).\n```\n\n### MCP (Model Context Protocol)\n\nThe SDK includes a full MCP 2025-06-18 implementation with four layers:\n\n| Layer | Module | Purpose |\n|-------|--------|---------|\n| Protocol | `beam_agent_mcp_protocol` | JSON-RPC 2.0 message constructors, validators, and encoders for the MCP spec |\n| Server dispatch | `beam_agent_mcp_dispatch` | Server-side state machine — lifecycle, capability negotiation, request routing |\n| Client dispatch | `beam_agent_mcp_client_dispatch` | Client-side state machine — request/response tracking, timeouts, server capability discovery |\n| Tool registry | `beam_agent_tool_registry` | In-process tool registration, dispatch, and session-scoped registry management |\n| Transports | `beam_agent_mcp_transport_stdio`, `beam_agent_mcp_transport_http` | Stdio (line-delimited JSON) and Streamable HTTP transports |\n\nThe public API is `beam_agent_mcp` (Erlang) / `BeamAgent.MCP` (Elixir).\n\n#### In-Process MCP Tool Servers\n\nDefine custom tools as Erlang functions that Claude can call:\n\n```erlang\nTool = beam_agent_mcp:tool(\n    \u003c\u003c\"lookup_user\"\u003e\u003e,\n    \u003c\u003c\"Look up a user by ID\"\u003e\u003e,\n    #{\u003c\u003c\"type\"\u003e\u003e =\u003e \u003c\u003c\"object\"\u003e\u003e,\n      \u003c\u003c\"properties\"\u003e\u003e =\u003e #{\u003c\u003c\"id\"\u003e\u003e =\u003e #{\u003c\u003c\"type\"\u003e\u003e =\u003e \u003c\u003c\"string\"\u003e\u003e}}},\n    fun(Input) -\u003e\n        Id = maps:get(\u003c\u003c\"id\"\u003e\u003e, Input, \u003c\u003c\u003e\u003e),\n        {ok, [#{type =\u003e text, text =\u003e \u003c\u003c\"User: \", Id/binary\u003e\u003e}]}\n    end\n),\nServer = beam_agent_mcp:server(\u003c\u003c\"my-tools\"\u003e\u003e, [Tool]),\n{ok, Session} = beam_agent:start_session(#{\n    backend =\u003e claude,\n    sdk_mcp_servers =\u003e [Server]\n}).\n```\n\n#### MCP Server Dispatch\n\nBuild a full MCP server that handles the protocol lifecycle:\n\n```erlang\n%% Create a server dispatch state machine\nState = beam_agent_mcp:new_dispatch(\n    #{name =\u003e \u003c\u003c\"my-server\"\u003e\u003e, version =\u003e \u003c\u003c\"1.0.0\"\u003e\u003e},\n    #{tools =\u003e true, resources =\u003e true},\n    #{tool_registry =\u003e Registry, provider =\u003e MyProviderModule}\n),\n\n%% Feed incoming JSON-RPC messages through the state machine\n{Responses, NewState} = beam_agent_mcp:dispatch_message(IncomingMsg, State).\n```\n\n#### MCP Client Dispatch\n\nConnect to an MCP server as a client:\n\n```erlang\n%% Create a client dispatch state machine\nClient = beam_agent_mcp:new_client(\n    #{name =\u003e \u003c\u003c\"my-client\"\u003e\u003e, version =\u003e \u003c\u003c\"1.0.0\"\u003e\u003e},\n    #{roots =\u003e true, sampling =\u003e true},\n    #{handler =\u003e MyHandlerModule}\n),\n\n%% Build and send the initialize request\n{InitMsg, Client1} = beam_agent_mcp:client_send_initialize(Client),\n%% ... send InitMsg over transport, receive response ...\n{Events, Client2} = beam_agent_mcp:client_handle_message(Response, Client1),\n\n%% Complete the handshake\n{InitializedMsg, Client3} = beam_agent_mcp:client_send_initialized(Client2),\n\n%% List available tools\n{ToolsReq, Client4} = beam_agent_mcp:client_send_tools_list(Client3).\n```\n\n### SDK Lifecycle Hooks\n\nRegister callbacks at key session lifecycle points. Hooks receive a context\nmap and return a three-way result: `{ok, Ctx}` (allow, continue chain with\npossibly modified context), `{deny, Reason}` (block the action), or\n`{ask, Reason}` (escalate to caller for decision).\n\n```erlang\n%% Block dangerous tool calls\nHook = beam_agent_hooks:hook(pre_tool_use, fun(Ctx) -\u003e\n    case maps:get(tool_name, Ctx, \u003c\u003c\u003e\u003e) of\n        \u003c\u003c\"Bash\"\u003e\u003e -\u003e {deny, \u003c\u003c\"Shell access denied\"\u003e\u003e};\n        _ -\u003e {ok, Ctx}\n    end\nend),\n{ok, Session} = beam_agent:start_session(#{\n    backend =\u003e claude,\n    sdk_hooks =\u003e [Hook]\n}).\n```\n\nBlocking events (may return `{deny, _}` or `{ask, _}` to prevent the action):\n`pre_tool_use`, `user_prompt_submit`, `permission_request`, `subagent_start`,\n`pre_compact`, `config_change`.\n\nNotification-only events (always proceed regardless of return value):\n`post_tool_use`, `post_tool_use_failure`, `stop`, `session_start`,\n`session_end`, `subagent_stop`, `notification`, `task_completed`,\n`teammate_idle`.\n\nCrash protection: each callback is wrapped in try/catch. Blocking hook\ncrashes return `{deny, \u003c\u003c\"hook crashed (fail-safe deny)\"\u003e\u003e}` (fail-closed) —\na security hook that crashes must not allow the action through unchecked.\nNotification hook crashes are logged and the context passes through\nunmodified (fail-open).\n\n### Telemetry\n\nBeamAgent emits telemetry at key points across both backend session handling and\nthe canonical runtime domains added in this repo: query lifecycle, command\nexecution, run and step lifecycle, routing decisions, artifact and memory\noperations, journal replay, routines, orchestration, policy evaluation, audit,\ncontext compaction, and buffer overflow. The `telemetry` library is an\n**optional** dependency — when present, events are emitted via\n`telemetry:execute/3`; when absent, emission is a silent no-op with zero\noverhead.\n\nTo opt in, add `{telemetry, \"~\u003e 1.3\"}` to your application's `deps` and\n`applications` list, then attach handlers:\n\n```erlang\ntelemetry:attach(my_handler, [beam_agent, command, run, stop], fun handle/4, #{}).\n```\n\nRepresentative events:\n- `[beam_agent, claude, query, start|stop|exception]`\n- `[beam_agent, command, run, start|stop|exception]`\n- `[beam_agent, run, state_change]`\n- `[beam_agent, artifact, put|search, start|stop|exception]`\n- `[beam_agent, journal, append|stream_from, start|stop|exception]`\n- `[beam_agent, memory, remember|update|search, start|stop|exception]`\n- `[beam_agent, routing, select_backend, start|stop|exception]`\n- `[beam_agent, context, maybe_compact, start|stop|exception]`\n- `[beam_agent, context, state_change]`\n- `[beam_agent, routine, create|run_due, start|stop|exception]`\n- `[beam_agent, orchestrator, delegate|collect, start|stop|exception]`\n- `[beam_agent, policy, evaluate, start|stop|exception]`\n- `[beam_agent, audit, record|list_events, start|stop|exception]`\n- `[beam_agent, buffer, overflow]`\n\n### ETS Initialization\n\nCall `beam_agent:init/0,1` before starting sessions to initialize the\nSDK's ETS tables. The default `hardened` mode protects tables and proxies\nwrites through a linked owner process, while reads remain zero-cost from\nany process. Opt into `public` mode explicitly if you don't need\nwrite-path isolation:\n\n```erlang\n%% Default — hardened access, protected tables, proxied writes, zero-cost reads\nok = beam_agent:init().\n\n%% Public — all tables use public access, zero overhead (opt-in)\nok = beam_agent:init(#{table_access =\u003e public}).\n```\n\nIf `init/1` is never called, tables are created lazily with public access\non first use (the pre-init fallback is intentionally permissive since no\nshard owner processes exist yet).\n\n### Supervisor Integration\n\nEmbed sessions in your supervision tree:\n\n```erlang\n%% In your supervisor init/1\nok = beam_agent:init(),\nChildren = [\n    beam_agent:child_spec(#{\n        backend =\u003e claude,\n        cli_path =\u003e \"/usr/local/bin/claude\",\n        session_id =\u003e \u003c\u003c\"worker-1\"\u003e\u003e\n    })\n],\n{ok, {#{strategy =\u003e one_for_one}, Children}}.\n```\n\n### Universal Session and Thread Stores\n\nThe common session/thread APIs are backed by `beam_agent_store` inside\n`beam_agent` so every adapter can expose the same high-level capability\nsurface, even when the underlying SDK does not implement it directly.\n\n```erlang\n%% Query shared history\n{ok, Sessions} = beam_agent_session_store:list_sessions(),\n{ok, Messages} = beam_agent_session_store:get_session_messages(\u003c\u003c\"sid\"\u003e\u003e),\n\n%% Restore a previous session (native resume when available)\n{ok, Restored} = beam_agent:restore_session(\u003c\u003c\"sid\"\u003e\u003e, #{}),\n{ok, Msgs} = beam_agent:query(Restored, \u003c\u003c\"Pick up where we left off\"\u003e\u003e),\n\n%% Create and inspect universal threads\n{ok, Thread} = beam_agent_threads:thread_start(Session, #{}),\n{ok, ThreadInfo} = beam_agent_threads:thread_read(\n    Session, maps:get(thread_id, Thread), #{include_messages =\u003e true}\n).\n```\n\n## Project Structure\n\n```\nbeam-agent/\n  src/\n    public/             Canonical beam_agent public modules\n    core/               Shared runtime, routing, control, MCP, hooks\n    stores/             Store adapters (beam_agent_store_ets, beam_agent_store_dets)\n    transports/         Reusable transport-family modules\n    backends/           Internal backend implementations\n  test/\n    public/             Canonical public-surface tests\n    core/               Shared-runtime tests\n    backends/           Backend-specific tests\n  beam_agent_ex/\n    lib/\n      beam_agent/       Canonical BeamAgent modules\n      *.ex              BeamAgent + backend-specific Elixir wrappers\n    test/\n      canonical/        BeamAgent public wrapper tests\n      wrappers/         Backend-specific Elixir wrapper tests\n```\n\n## Building\n\n### Erlang\n\n```bash\nrebar3 compile          # Build the canonical beam_agent OTP app\nrebar3 eunit --app beam_agent\nrebar3 dialyzer         # Static analysis\nrebar3 check            # compile + dialyzer + eunit + ct\n```\n\n### Elixir Wrapper\n\n```bash\ncd beam_agent_ex\nmix deps.get\nmix test\nmix dialyzer               # Static analysis (via Dialyxir)\n```\n\n## Requirements\n\n- Erlang/OTP 27+\n- Elixir 1.17+ (for wrappers)\n- OTP built-ins: `crypto`, `ssl`, `inets`, `public_key` (for HTTP/WebSocket transports)\n- Optional: `telemetry` ~\u003e 1.3 (for instrumentation — see [Telemetry](#telemetry))\n- Test deps: `proper` 1.5.0\n\n**Zero external runtime dependencies.** The SDK relies only on OTP standard\nlibraries. All third-party integrations (telemetry, metrics, tracing) are\nopt-in by the consuming application.\n\n## Package Documentation\n\nThe canonical packages are documented first. Backend-specific READMEs describe\nnative escape hatches and transport-specific behavior inside those packages.\n\n**Canonical Packages:**\n- `beam_agent` — Canonical Erlang SDK at the repo root\n- [BeamAgent](beam_agent_ex/README.md) — Canonical Elixir wrapper\n\n**Backend-Native Erlang Modules Inside `beam_agent`:**\n- `claude_agent_sdk` — Claude Code adapter\n- `codex_app_server` — Codex CLI adapter\n- `gemini_cli_client` — Gemini CLI adapter\n- `opencode_client` — OpenCode adapter\n- `copilot_client` — GitHub Copilot adapter\n\n**Compatibility / Native Elixir Modules Inside `beam_agent_ex`:**\n- `ClaudeEx`\n- `CodexEx`\n- `GeminiEx`\n- `OpencodeEx`\n- `CopilotEx`\n\n## License\n\nSee [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeardedeagle%2Fbeam-agent","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbeardedeagle%2Fbeam-agent","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeardedeagle%2Fbeam-agent/lists"}