Skip to contents

gptr logo

Add a tool, a policy or a hook when an existing capability needs a small change. These features, providers, commands and prompt sections share the extension registry with gptr’s built-ins. gptr_api()$features lists the supported extension kinds and services.

For reusable instructions and packaged extensions, see Skills and plugins. For existing helpers, see Tools and artifacts; choose a model in Language models. The displayed responses below are scripted offline, so building this guide needs no credentials or network.

library(gptr)
gptr_api()$version
#> [1] '1.0'
head(grep("^kind[.]", gptr_api()$features, value = TRUE))
#> [1] "kind.adapter"       "kind.agent"         "kind.artifact_type"
#> [4] "kind.backend"       "kind.cache_policy"  "kind.checkpointer"

A tool the model calls as an R function

Most capabilities are members of the peter$ namespace rather than separate model-visible tools: the model calls them from R code, which costs a few tokens per tool instead of a full schema. gptr_check() runs the conformance checks on a spec before you register it.

add_tool = gptr_tool(
  "add", "Add two numbers",
  parameters = list(
    type = "object", required = I(c("a", "b")),
    properties = list(a = list(type = "number"), b = list(type = "number"))
  ),
  fun = function(a, b) a + b,
  exposure = "r", namespace = "demo"
)
gptr_check(add_tool)
#> <gptr_check> 4 check(s), 0 failed
#>   ok    tool:add  spec.class
#>   ok    tool:add  spec.fields
#>   ok    tool:add  tool.schema
#>   ok    tool:add  tool.empty_input: empty input fails schema validation before execute()
off_tool = gptr_register(add_tool)
peter$demo$add(2, 3)
#> [1] 5

Ask Peter to use it:

session = peter("Add 40 and 2 with the demo tool.")
session$text
#> [1] "The sum is 42."
off_tool()

gptr_register() returns a function that unregisters the record. To add a tool to one session only, pass it as peter(..., tools = list(add_tool)).

exposure = "r" makes a callable peter$demo$add() member. A "direct" tool goes into the model’s tool declarations; give it an execute = function(input, ctx) handler. A "deferred" tool is found through peter$search(), while "hidden" omits it from the model’s catalog. An R package’s tools should use the package name as their namespace. Use peter$help("demo/add") to inspect a registered member’s schema.

An R helper runs in the current R process and can compute on already-loaded objects without a file export. Its allocations follow ordinary R rules; an external backend may need its own serialization. See Execution model for these boundaries.

Choose execution = "sequential" for handlers that evaluate R or change files. A tool’s risk = function(input, ctx) classifies its actions; its result still passes the active permission policies. A custom tool does not grant permission to run its own side effects.

A permission policy

A policy sees every tool call before it runs and can deny, ask, modify or allow it. The strictest answer of all policies wins, and a policy that throws an error denies.

keep_data = gptr_policy("keep_data", check = function(call, ctx) {
  if (identical(call$name, "r") && grepl("rm(", call$input$code, fixed = TRUE)) {
    list(decision = "deny", reason = "removing objects is not allowed in this session")
  } else {
    NULL
  }
})
off_policy = gptr_register(keep_data)
session = peter("Remove the session's objects.", envir = new.env(), mode = auto)
session$text

The offline example requests rm(list = ls()), which this policy denies:

#> [1] "I was not allowed to remove the objects."
off_policy()

The denial reached the model as an error result, even in auto mode. This text match illustrates the callback; it is not a general R code security filter. See Configuration for modes, trust and permission rules.

A hook on a session

gptr_on() attaches a handler to one session’s events; gptr_hook() specs registered with gptr_register() apply to every session.

This example uses an offline provider so the event sequence is reproducible:

fake = gptr_fake_provider(list("hello"))
session = peter("Say hello.", model = fake, .run = FALSE)
seen = new.env()
seen$roles = character()
off_hook = gptr_on(session, "message_end", function(event, ctx) {
  seen$roles = c(seen$roles, event$message$role)
  NULL
})
gptr_step(session)
off_hook()
seen$roles
#> [1] "user"      "assistant"

Prompt sections, context blocks and commands

A prompt section adds text to the system prompt of new sessions; a context block adds data to the first message or to every turn; a command adds a slash command to the console.

rules = gptr_prompt_section(
  "house_rules", "Report every quantity with its unit.", tier = "T1", order = 780L
)
notebook = gptr_context_block(
  "lab_notebook", function(ctx, budget) "Experiment 12: cohort B.",
  placement = "first", order = 650L
)
rows = gptr_command(
  "rows", function(args, ctx) paste("rows:", nrow(mtcars)),
  description = "Show the rows of mtcars"
)
offs = lapply(list(rules, notebook, rows), gptr_register)
reg = gptr_registry(c("prompt_section", "context_block", "command"))
reg[reg$name %in% c("house_rules", "lab_notebook", "rows"), c("kind", "name", "source")]
#> <gptr_registry> 3 record(s)
#>            kind         name source
#>  prompt_section  house_rules   user
#>   context_block lab_notebook   user
#>         command         rows   user
for (off in offs) off()

gptr_prompt() shows what a new session would freeze, section by section, with token estimates. The command’s optional complete callback is metadata for frontend integrations; the built-in console does not currently invoke it for Tab completion.

Other kinds

The eleven exported constructors cover the common kinds: gptr_tool(), gptr_provider(), gptr_adapter(), gptr_router(), gptr_hook(), gptr_policy(), gptr_agent(), gptr_command(), gptr_prompt_section(), gptr_context_block() and gptr_backend(). Every other kind uses gptr_spec():

gptr_spec("env_alias", "SLACK_BOT_TOKEN", aliases = "slack-token")
#> <gptr_env_alias SLACK_BOT_TOKEN>
#>   aliases: slack-token
#>   api_version: 1.0

A provider for an OpenAI-compatible endpoint is data:

gptr_register(gptr_provider(
  "corp", api = "openai-completions",
  base_url = "https://llm.corp.example/v1", auth = "CORP_LLM_KEY",
  models = list(list(id = "corp-large", context = 128000))
))
peter("Summarize this table.", mtcars, model = "corp/corp-large")

Extensions and plugins

An extension is a function of the extension API object. It registers records with gptr$register() and can be passed to one call with extensions =, or saved in .gptr/extensions/ of a trusted project.

lab_rules = function(gptr) {
  gptr$register(gptr_prompt_section(
    "lab_rules", "Report concentrations in mmol/L.", tier = "T1", order = 790L
  ))
}
peter("Summarize the assay results.", assay, extensions = lab_rules)

In an extension file, the last expression must be this factory function. Review the whole file: earlier expressions are evaluated when it loads. The API object has $register(spec), $on(event, handler, matcher = NULL), $require(">= 1.0, < 2") and $has(feature), along with plugin-local $state. Check a factory with gptr_check(lab_rules) before enabling it.

Use gptr_registry(kind = NULL, diagnostics = FALSE) to inspect records, or gptr_registry(diagnostics = TRUE) to see validation and collision messages. For the same kind and name, session records win over trusted project records, user records, plugins and built-ins, in that order. A tie keeps the first registration and reports a collision. Additive kinds such as hooks keep multiple records. Removing a user record exposes the next matching record.

gptr_reload() rereads configuration and extensions for later use; existing sessions keep their frozen prompt. Plugin manifests, activation and compatibility are covered in Skills and plugins.

Packages can teach gptr about their classes through methods for gptr_describe() (compact object descriptions) and gptr_preimage() (undo support). These are separate from a tool registration.