--- title: "Generic agent and frontend framework" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Generic agent and frontend framework} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- `Rbebelm` is intentionally two things at once: 1. A generic, backend-agnostic R agent/frontend framework that can be implemented by BebeLM, another local model, or a remote provider. 2. A concrete native BebeLM backend for local GGUF inference. The framework layer is deliberately small and interface-driven. The loop owns agent lifecycle, queues, events, tool dispatch, extension registration, and JSONL session persistence. Frontends such as a console, RPC server, or the standalone `tui/` Rust module consume the same loop instead of reimplementing or owning agent logic. ## Backend contract An LLM provider implements the `BebelAgentBackend` interface from `s7contract` by providing S7 methods for these generics: - `bebel_backend_append_user(agent, message)` - `bebel_backend_append_system(agent, message, tools = NULL)` - `bebel_backend_append_tool_result(agent, content)` - `bebel_backend_assistant_turn(agent, on_event, check_interrupt, stop_on_tool_call)` - `bebel_backend_info(agent)` - `bebel_backend_transcript(agent)` - `bebel_backend_clear(agent)` BebeLM implements this contract for `BebelAgent`. The following fake backend is useful for tests and demonstrates that `bebel_agent_loop()` does not require a BebeLM object. ``` r library(Rbebelm) FakeBebelAgentBackendS3 <- S7::new_S3_class("fakeBebelAgentBackendVignette") S7::method(bebel_backend_append_user, FakeBebelAgentBackendS3) <- function(agent, message) { agent$user <- c(agent$user, message) agent } S7::method(bebel_backend_append_system, FakeBebelAgentBackendS3) <- function(agent, message, tools = NULL) { agent$system <- c(agent$system, message) agent } S7::method(bebel_backend_append_tool_result, FakeBebelAgentBackendS3) <- function(agent, content) { agent$tool <- c(agent$tool, content) agent } S7::method(bebel_backend_assistant_turn, FakeBebelAgentBackendS3) <- function( agent, on_event = NULL, check_interrupt = TRUE, stop_on_tool_call = FALSE ) { if (!is.null(on_event)) on_event(list(type = "text_delta", delta = "fake reply")) list(text = "fake reply", tokens = 2L, stop = "stop") } S7::method(bebel_backend_info, FakeBebelAgentBackendS3) <- function(agent) { list(provider = "fake", model = "fake-model") } S7::method(bebel_backend_transcript, FakeBebelAgentBackendS3) <- function(agent) { paste(c(agent$system, agent$user, agent$tool), collapse = "\n") } S7::method(bebel_backend_clear, FakeBebelAgentBackendS3) <- function(agent) { agent$user <- character() agent$tool <- character() agent } backend <- structure(new.env(parent = emptyenv()), class = "fakeBebelAgentBackendVignette") backend$user <- character() backend$system <- character() backend$tool <- character() ``` ## Loop and frontend ownership `bebel_agent_loop()` is the UI-independent controller. It accepts any `BebelAgentBackend`, optional tools, hooks, extensions, and a persistence setting. The queue vocabulary follows Pi: - `bebel_loop_steer()` adds steering messages. - `bebel_loop_follow_up()` adds follow-up messages. - `bebel_loop_policy(steering_mode = ..., follow_up_mode = ...)` controls whether queued messages are drained one-at-a-time or all at once. ``` r store_dir <- file.path(tempdir(), "rbebelm-framework-sessions") store <- bebel_session_create(cwd = tempdir(), session_dir = store_dir, name = "fake backend") loop <- bebel_agent_loop(backend, session = store) run <- bebel_loop_run(loop, "Hello backend", max_steps = 1) run$done #> [1] TRUE backend$user #> [1] "Hello backend" bebel_loop_state(loop)[c("state", "turns", "session_file")] #> $state #> [1] "idle" #> #> $turns #> [1] 1 #> #> $session_file #> [1] "/tmp/RtmpKqcpr9/rbebelm-framework-sessions/2026-06-09T08-08-17-688Z_6e1a145e-bf7e-68d0-d47c-213e393f1773.jsonl" ``` The loop writes generic message entries to the session store. The backend keeps its own transcript/cache state; the session file stores portable framework history for replay, browsing, forking, sharing, and UI state. ``` r context <- bebel_session_context(store) vapply(context$messages, `[[`, character(1), "role") #> [1] "user" "assistant" readLines(bebel_session_file(store), n = 3) #> [1] "{\"type\":\"session\",\"version\":3,\"id\":\"6e1a145e-bf7e-68d0-d47c-213e393f1773\",\"timestamp\":\"2026-06-09T08:08:17.688Z\",\"cwd\":\"/tmp/RtmpKqcpr9\"}" #> [2] "{\"type\":\"session_info\",\"id\":\"909aef6a\",\"parentId\":null,\"timestamp\":\"2026-06-09T08:08:17.690Z\",\"name\":\"fake backend\"}" #> [3] "{\"type\":\"message\",\"id\":\"37150fd9\",\"parentId\":\"909aef6a\",\"timestamp\":\"2026-06-09T08:08:17.705Z\",\"message\":{\"role\":\"user\",\"content\":\"Hello backend\",\"source\":\"prompt\"}}" ``` ## JSONL session trees Sessions are inspired by Pi's JSONL session format. The first line is a session header; every other entry has an `id`, `parentId`, `timestamp`, and `type`. Entries form a tree, not only a linear log. Moving the leaf to an earlier entry and appending creates a new branch without deleting the old path. Default persisted sessions live under: ```r tools::R_user_dir("Rbebelm", "data")/sessions// ``` Set `RBEBELM_SESSION_DIR` or pass `session_dir` to override that location. ``` r s <- bebel_session_create(cwd = tempdir(), session_dir = store_dir, name = "tree demo") u1 <- bebel_session_append_message(s, "user", "first question") a1 <- bebel_session_append_message( s, "assistant", list(list(type = "text", text = "first answer")), provider = "fake", model = "fake-model", stopReason = "stop" ) bebel_session_checkout(s, u1) u2 <- bebel_session_append_message(s, "user", "alternate branch") vapply(bebel_session_branch(s), `[[`, character(1), "type") #> [1] "session_info" "message" "message" length(bebel_session_tree(s)[[1]]$children) #> [1] 1 ``` Session entries include ordinary messages plus metadata and extension entries: - `message` - `custom` for extension state that does **not** enter model context - `custom_message` for extension-injected context - `label` - `session_info` - `model_change` - `thinking_level_change` - `compaction` - `branch_summary` ``` r bebel_session_append_custom(s, "my-extension", list(counter = 1L)) #> [1] "dab41e28" bebel_session_append_custom_message(s, "my-extension", "Hidden context", display = FALSE) #> [1] "a6f1ca26" bebel_session_append_label(s, u1, "checkpoint") #> [1] "66eb54a1" tail(vapply(bebel_session_entries(s), `[[`, character(1), "type"), 3) #> [1] "custom" "custom_message" "label" ``` Forking copies all non-header entries into a new session file with a new header. Cloning a branch copies only the active path from root to a selected leaf. ``` r forked <- bebel_session_fork(bebel_session_file(s), cwd = tempdir(), session_dir = store_dir) cloned <- bebel_session_clone_branch(s, leaf_id = u2, session_dir = store_dir) bebel_session_header(forked)$parentSession #> [1] "/tmp/RtmpKqcpr9/rbebelm-framework-sessions/2026-06-09T08-08-17-731Z_be99eb8c-efee-5adf-2415-38b8e06bf33e.jsonl" vapply(bebel_session_entries(cloned), `[[`, character(1), "id") #> [1] "68de3bdd" "914845c8" "e035700f" ``` ## Extensions An extension is a backend-agnostic capability bundle registered into the loop, not into a particular terminal UI. It should implement the `BebelAgentExtension` interface: - `bebel_extension_manifest(extension)` - `bebel_extension_tools(extension)` - `bebel_extension_commands(extension)` - `bebel_extension_hooks(extension)` - `bebel_extension_skill_providers(extension)` - `bebel_extension_prompt_template_providers(extension)` The helper `bebel_extension()` creates a simple extension object implementing that interface. Extensions register into the loop; they do not own the loop or a terminal frontend. ``` r state_command <- bebel_loop_command( "state", function(args, loop, context) bebel_loop_state(loop), description = "Return loop state." ) ext <- bebel_extension( "demo-extension", commands = list(state = state_command), hooks = list(event = function(event, loop, context, ...) { context$last_event_type <- event$type }), metadata = list(ui = "frontends may render this") ) bebel_extension_manifest(ext) #> $name #> [1] "demo-extension" #> #> $tools #> NULL #> #> $commands #> $commands$state #> $commands$state$name #> [1] "state" #> #> $commands$state$description #> [1] "Return loop state." #> #> $commands$state$usage #> [1] "/state" #> #> #> #> $hooks #> [1] "event" #> #> $skill_providers #> NULL #> #> $prompt_template_providers #> NULL #> #> $keybindings #> list() #> #> $widgets #> list() #> #> $metadata #> $metadata$ui #> [1] "frontends may render this" ``` When attached to a loop, contributed commands, tools, hooks, skill providers, and prompt-template providers are available through loop state and catalogs. ``` r loop2 <- bebel_agent_loop(backend, extensions = list(ext), session = FALSE) bebel_loop_command_catalog(loop2) #> name description usage #> 1 state Return loop state. /state bebel_loop_execute_command(loop2, "/state") #> [1] TRUE loop2$context$last_event_type #> [1] "command_end" ``` ## Skills and prompt templates Skill providers and prompt-template providers are separate interfaces so system prompt loading is not tied to BebeLM. A provider can be in-memory, file-backed, or package-backed. ``` r skills <- bebel_skill_provider(list( concise = "Prefer concise, direct answers.", r_safe = "Avoid side effects unless the user asks for them." )) prompts <- bebel_prompt_template_provider(list( system = "You are {{role}} working in {{place}}." )) bebel_skill_list(skills) #> name description path #> 1 concise concise #> 2 r_safe r_safe bebel_prompt_template_list(prompts) #> name description path #> 1 system system bebel_system_prompt( prompts, "system", data = list(role = "an R coding agent", place = "Bamako"), skill_provider = skills, skills = c("concise", "r_safe") ) #> [1] "You are an R coding agent working in Bamako.\n\n# Loaded skills\n\n## Skill: concise\n\nPrefer concise, direct answers.\n\n## Skill: r_safe\n\nAvoid side effects unless the user asks for them." ``` `bebel_append_system_prompt()` renders a template, appends selected skills, and then calls `bebel_backend_append_system()`. This keeps system-prompt loading generic; BebeLM-specific tool preamble details remain inside BebeLM's backend method.