sbgnt / subagent

Push a folder. Invoke via API. Agents calling agents.

Install

curl -fsSL https://sbgnt.com/install.sh | bash

Quick Start

# 1. Log in
sbgnt login

# 2. Initialize an agents directory
mkdir agents && cd agents
sbgnt init

# 3. Push the example agent
sbgnt push wikipedia

# 4. Invoke it
sbgnt ask wikipedia "what is the Riemann hypothesis?"

# Or via API:
curl -X POST https://sbgnt.com/a/wikipedia \
  -H "Authorization: Bearer $SBGNT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "what is the Riemann hypothesis?", "effort": "fast"}'

Effort Levels

LevelModelMax IterationsUse Case
fastHaiku30Quick lookups
balancedSonnet45Standard work
comprehensiveOpus60Deep research

Subagent Structure

my-agent/
├── agent.toml          # name, route, description
├── system.md           # system prompt
├── tools.star          # starlark tool functions
└── permissions.toml    # allowed domains/methods (optional)

permissions.toml

Controls what your agent's fetch() and ask() calls can access. Without this file, your agent cannot make HTTP requests or call other agents. Only allow the specific routes and methods your tools actually need.

# Prefer fine-grained rules over blanket allowed_domains.
# Only allow the specific API routes your tools use.
[[rules]]
domain = "api.github.com"
methods = ["GET"]
allow_paths = ["/repos/**", "/users/**"]
deny_paths = ["/repos/*/actions/**"]

# OAuth connections — auto-injects auth headers for these domains
[[connections]]
name = "gmail"
domains = ["gmail.googleapis.com"]

# Allow this agent to call other agents via ask()
allowed_agents = ["my-other-agent"]

# Allow this agent to read specific secrets via secret()
allowed_secrets = ["MY_API_KEY"]

Agent-to-Agent Calls (ask)

Agents can call other agents using the built-in ask() function. This is the recommended pattern for composing agents — do not create API keys for agent-to-agent calls.

# tools.star — define a tool that delegates to another agent
TOOLS = {
    "research": "Delegate a research task to a specialist agent",
}

def research(query, effort="balanced"):
    # ask() is a built-in that calls another deployed agent
    return ask(route="my-research-agent", query=query, effort=effort)

To enable ask(), list the allowed agent routes in permissions.toml:

# permissions.toml
allowed_agents = ["my-research-agent", "gmail-agent"]

The ask() builtin runs the target agent as a sub-session (up to 3 levels deep) and returns its text response. The calling agent's user context is inherited, so the sub-agent sees the same connections and permissions.

Starlark Builtins

These functions are available in every tools.star file:

# ── HTTP ──
# fetch(url, method="GET", body="", headers={})
# Makes an HTTP request. Requires matching permissions.toml entry.
# Returns a struct with .status (int), .headers (dict), .body (string).
resp = fetch(url="https://api.example.com/data", method="POST",
             body='{"key": "value"}',
             headers={"Accept": "application/json"})
print(resp.status)   # 200
print(resp.body)     # '{"result": ...}'

# ── Sub-agent calls ──
# ask(route, query, effort="balanced")
# Calls another deployed agent. Requires allowed_agents in permissions.toml.
# Returns the agent's text response as a string.
result = ask(route="my-other-agent", query="summarize this", effort="fast")

# ── URL encoding ──
# urlencode(s)
# Percent-encodes a string for use in URL query parameters.
q = urlencode("hello world & more")  # "hello+world+%26+more"
url = "https://api.example.com/search?q=" + q

# ── JSON ──
# json_decode(s)
# Parses a JSON string into starlark values (dicts, lists, strings, ints, bools, None).
data = json_decode('{"users": [{"name": "Alice"}], "count": 1}')
print(data["users"][0]["name"])  # "Alice"
print(data["count"])             # 1

# json_encode(v)
# Serializes a starlark value (dict, list, string, int, float, bool, None) to JSON.
s = json_encode({"key": "value", "nums": [1, 2, 3]})
print(s)  # '{"key":"value","nums":[1,2,3]}'

# ── Base64 ──
# base64_encode(s)
# Encodes a string to standard base64.
encoded = base64_encode("Hello, World!")  # "SGVsbG8sIFdvcmxkIQ=="

# base64_decode(s)
# Decodes a standard base64 string.
decoded = base64_decode("SGVsbG8sIFdvcmxkIQ==")  # "Hello, World!"

# ── Secrets ──
# secret(name)
# Retrieves a user secret by name. Set secrets via `sbgnt secrets set <name> <value>`.
api_key = secret("MY_API_KEY")

# ── Output ──
# print(...)
# Prints to the tool's output (visible in session logs, not in the tool result).
print("debug:", some_value)

Starlark Runtime

Tools are written in Starlark, a Python-like language designed for configuration and embedded scripting.

# Execution model
- Each tool call runs in a fresh starlark thread
- Module-level code (outside functions) runs once when the agent is loaded
- Module-level variables ARE shared across tool calls within a session
- No imports — all code must be in a single tools.star file
- Functions starting with _ are private (not exposed as tools)

# Timeouts and limits
- Tool execution timeout: 30 seconds (5 minutes if ask() is enabled)
- HTTP response body limit: 1 MB
- Test execution timeout: 30 seconds

# Available types
- Strings, ints, floats, bools, None
- Lists, dicts, tuples, sets
- Structs (returned by fetch())

# What's NOT available
- No file I/O, no network access except via fetch()
- No os, sys, or subprocess
- No third-party imports
- No threading or async

# Conventions
- TOOLS dict maps function names to descriptions (required)
- Parameters are always strings (from the model's JSON input)
- Return a string from each tool function

Testing

Test your tools locally before deploying with sbgnt test <dir>. Create a test_tools.star file alongside your tools.star:

# test_tools.star — all test_* functions run automatically

def test_hello():
    result = hello(name="world")
    assert_eq(got=result, want="Hello, world!")

def test_search_with_mock():
    # Queue a mock HTTP response for fetch()
    set_mock_responses([
        mock_fetch(status=200, body='{"results": [{"title": "Example"}]}')
    ])
    result = search(query="test")
    assert_contains(haystack=result, needle="Example")

def test_error_handling():
    set_mock_responses([
        mock_fetch(status=404, body='{"error": "not found"}')
    ])
    result = search(query="missing")
    assert_contains(haystack=result, needle="not found")
$ sbgnt test ./my-agent
PASS  test_hello
PASS  test_search_with_mock
PASS  test_error_handling

3 passed, 0 failed

Test helpers available in test_tools.star:

# Mock responses
mock_fetch(status, body, headers={})       # Create a mock fetch response struct
set_mock_responses([resp1, resp2, ...])    # Queue responses for fetch() calls
set_mock_ask_responses(["resp1", ...])     # Queue responses for ask() calls
reset_mocks()                              # Clear all mock state

# Assertions
assert_eq(got, want, msg="")               # Assert two values are equal
assert_contains(haystack, needle, msg="")  # Assert substring match

set_mock_secrets({"MY_KEY": "test_value"})  # Queue secret values for secret() calls

# All builtins (fetch, ask, urlencode, json_decode, json_encode,
# base64_encode, base64_decode, secret, print) are also available
# — fetch/ask use mock responses instead of real HTTP.

API

POST /a/{route}               Invoke a subagent
  Body: {"query": "...", "effort": "balanced", "session_id": "..."}

POST /api/agents              Push a subagent
GET  /api/agents              List subagents
GET  /api/agents/{route}      Get subagent details
DELETE /api/agents/{route}    Delete a subagent

GET  /api/sessions/{id}       Get session history

POST /api/connections            Add a connection
GET  /api/connections            List connections
GET  /api/connections/available  List available providers
DELETE /api/connections/{n}      Remove a connection

PUT  /api/secrets/{name}      Set a secret
GET  /api/secrets             List secret names
DELETE /api/secrets/{name}    Delete a secret

POST /api/tokens              Create API token
GET  /api/tokens              List tokens
DELETE /api/tokens/{id}       Revoke token

Concepts

Subagents

A folder with 4 files. Push it, invoke it. No build step, no framework.

Starlark Tools

Define tools as starlark functions. Sandboxed, inspectable, safe.

Connections

OAuth-managed credentials. Your tools just call fetch() — auth is handled.

Sessions

Every invocation is stored and resumable. Multi-turn conversations with subagents.