Push a folder. Invoke via API. Agents calling agents.
curl -fsSL https://sbgnt.com/install.sh | bash
# 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"}'
| Level | Model | Max Iterations | Use Case |
|---|---|---|---|
fast | Haiku | 30 | Quick lookups |
balanced | Sonnet | 45 | Standard work |
comprehensive | Opus | 60 | Deep research |
my-agent/
├── agent.toml # name, route, description
├── system.md # system prompt
├── tools.star # starlark tool functions
└── permissions.toml # allowed domains/methods (optional)
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"]
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.
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)
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
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.
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
A folder with 4 files. Push it, invoke it. No build step, no framework.
Define tools as starlark functions. Sandboxed, inspectable, safe.
OAuth-managed credentials. Your tools just call fetch() — auth is handled.
Every invocation is stored and resumable. Multi-turn conversations with subagents.