The agent loop: prompt in, tool calls executed, final answer out.
The agent starts with no abilities at all. What it can do is exactly what you
pass in :capabilities - see jj.bettong.capability.
(require '[jj.bettong.agent :as agent] '[jj.bettong.capabilities :as caps])
(def assistant (agent/deepseek {:capabilities [(caps/filesystem)]})) (agent/run assistant "Create file /tmp/foo.txt with content 'foo'")
The agent loop: prompt in, tool calls executed, final answer out.
The agent starts with no abilities at all. What it can do is exactly what you
pass in :capabilities - see `jj.bettong.capability`.
(require '[jj.bettong.agent :as agent]
'[jj.bettong.capabilities :as caps])
(def assistant (agent/deepseek {:capabilities [(caps/filesystem)]}))
(agent/run assistant "Create file /tmp/foo.txt with content 'foo'")Every capability that ships with bettong, in one place to require.
Each also lives in its own namespace - jj.bettong.capability.filesystem,
jj.bettong.capability.wikipedia and so on - if you would rather name them
individually. None of them is on unless you pass it to an agent.
The search sources are separate capabilities with separate tool names, so an agent can hold several at once and pick between them.
Every capability that ships with bettong, in one place to require. Each also lives in its own namespace - `jj.bettong.capability.filesystem`, `jj.bettong.capability.wikipedia` and so on - if you would rather name them individually. None of them is on unless you pass it to an agent. The search sources are separate capabilities with separate tool names, so an agent can hold several at once and pick between them.
The capability protocol.
An agent can do nothing on its own. Everything it can do - touch the
filesystem, run a command, call your API - comes from a capability you hand
to jj.bettong.agent/run explicitly. No capability, no tool, no way in.
A capability is anything satisfying Capability. A plain map already does:
{:id :weather :instructions "Temperatures are in celsius." :tools [{:name "get_weather" :description "Current weather for a city." :parameters {:city {:type "string" :description "City name." :required true}} :handler (fn [{:keys [city]}] (str "It is raining in " city))}]}
so does a defrecord or a reify, when you need state or a lifecycle.
The capability protocol.
An agent can do nothing on its own. Everything it can do - touch the
filesystem, run a command, call your API - comes from a capability you hand
to `jj.bettong.agent/run` explicitly. No capability, no tool, no way in.
A capability is anything satisfying `Capability`. A plain map already does:
{:id :weather
:instructions "Temperatures are in celsius."
:tools [{:name "get_weather"
:description "Current weather for a city."
:parameters {:city {:type "string" :description "City name." :required true}}
:handler (fn [{:keys [city]}] (str "It is raining in " city))}]}
so does a defrecord or a reify, when you need state or a lifecycle.Searching through duckduckgo.
Searching through duckduckgo.
Reading and changing values inside EDN files.
Reading and changing values inside EDN files.
Reading files, and writing them only when asked.
Reading files, and writing them only when asked.
Turning a search backend into a capability.
Each search source ships as its own capability with its own tool name, so an
agent can be given several at once and choose between them by name -
wikipedia_search for an encyclopaedic fact, duckduckgo_search for the open
web. One capability per source is also what lets the model see, in the tool
descriptions, what each one actually covers.
Turning a search backend into a capability. Each search source ships as its own capability with its own tool name, so an agent can be given several at once and choose between them by name - `wikipedia_search` for an encyclopaedic fact, `duckduckgo_search` for the open web. One capability per source is also what lets the model see, in the tool descriptions, what each one actually covers.
Searching through wikipedia.
Searching through wikipedia.
CLI entry point. Grants a writable filesystem and a shell - the explicit opt-in a program has to make, since reading is all a capability gives by default: lein run "Create file /tmp/foo.txt with content 'foo'"
CLI entry point. Grants a writable filesystem and a shell - the explicit opt-in a program has to make, since reading is all a capability gives by default: lein run "Create file /tmp/foo.txt with content 'foo'"
Reading and updating values inside EDN files.
Edits are spliced into the file's text, so comments, key order and hand
formatting survive - see jj.bettong.impl.edn-text. Pass
{:preserve-formatting? false} to rewrite the file from its data instead,
which is pretty-printed and loses comments.
Reading and updating values inside EDN files.
Edits are spliced into the file's text, so comments, key order and hand
formatting survive - see `jj.bettong.impl.edn-text`. Pass
{:preserve-formatting? false} to rewrite the file from its data instead,
which is pretty-printed and loses comments.Editing EDN through a syntax tree that keeps everything a reader throws away.
clojure.edn/read-string gives you data, and data has no comments, no key
order and no indentation - so no writer can put them back. The tree here
keeps whitespace, comments and #_ discards as nodes of their own, which
makes rendering it the exact original text and an edit a change to one node.
Nodes are plain maps:
{:type :whitespace :text " \n\n"} {:type :comment :text ";; why this is 3000"} {:type :token :text "8080"} {:type :map :open "{" :close "}" :children [...]} {:type :wrapper :text "#inst" :children [...]} ; #_ ^meta 'quote #tag
parse and render are inverses: (= s (render (parse s))) for any EDN.
Editing EDN through a syntax tree that keeps everything a reader throws away.
`clojure.edn/read-string` gives you data, and data has no comments, no key
order and no indentation - so no writer can put them back. The tree here
keeps whitespace, comments and `#_` discards as nodes of their own, which
makes rendering it the exact original text and an edit a change to one node.
Nodes are plain maps:
{:type :whitespace :text " \n\n"}
{:type :comment :text ";; why this is 3000"}
{:type :token :text "8080"}
{:type :map :open "{" :close "}" :children [...]}
{:type :wrapper :text "#inst" :children [...]} ; #_ ^meta 'quote #tag
`parse` and `render` are inverses: (= s (render (parse s))) for any EDN.Path resolution and the file operations behind the filesystem capability.
Every path goes through resolve-path, which is where confinement to a root
and the refusal to touch credential files live.
Path resolution and the file operations behind the filesystem capability. Every path goes through `resolve-path`, which is where confinement to a root and the refusal to touch credential files live.
The one place JSON lives, so the parser behind it is a single-file change.
clojure.data.json is chosen for its dependency profile rather than its
speed: it is pure Clojure and pulls nothing else in, which matters more in a
library other applications embed than raw throughput does on payloads this
size. The JDK's own JSON API (JEP 540) lands in jdk.incubator.json in JDK 28
and would slot in here.
The one place JSON lives, so the parser behind it is a single-file change. `clojure.data.json` is chosen for its dependency profile rather than its speed: it is pure Clojure and pulls nothing else in, which matters more in a library other applications embed than raw throughput does on payloads this size. The JDK's own JSON API (JEP 540) lands in jdk.incubator.json in JDK 28 and would slot in here.
Option checks shared by the capabilities.
Option checks shared by the capabilities.
Running commands behind the shell capability: the command policy, the process itself, and the truncation that keeps its output from swallowing the transcript.
Running commands behind the shell capability: the command policy, the process itself, and the truncation that keeps its output from swallowing the transcript.
Thin client for the DeepSeek chat-completions API (OpenAI-compatible).
Thin client for the DeepSeek chat-completions API (OpenAI-compatible).
Search backends: the functions the search capabilities are built on.
A backend is just (fn [{:keys [query max-results]}] -> [{:title :url :snippet}]), so anything can be one: a public search API, your own index, an internal wiki. The named ones below are conveniences.
Search backends: the functions the search capabilities are built on.
A backend is just (fn [{:keys [query max-results]}] -> [{:title :url :snippet}]),
so anything can be one: a public search API, your own index, an internal wiki.
The named ones below are conveniences.Keeping credentials out of transcripts.
Anything a tool returns is sent to the model provider, so a command like
echo $API_KEY does not just print a secret - it uploads one. Two defences
here, both on by default: the child process never sees credentials in its
environment, and whatever a tool does return is scrubbed of their values on
the way back.
Keeping credentials out of transcripts. Anything a tool returns is sent to the model provider, so a command like `echo $API_KEY` does not just print a secret - it uploads one. Two defences here, both on by default: the child process never sees credentials in its environment, and whatever a tool does return is scrubbed of their values on the way back.
Token accounting. Every model response carries a usage block; the agent adds them up across the run so you can see what a task cost.
Token accounting. Every model response carries a usage block; the agent adds them up across the run so you can see what a task cost.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |