about summary refs log tree commit diff
path: root/kaakaa
diff options
context:
space:
mode:
authorArun Isaac2026-04-12 18:09:49 +0100
committerArun Isaac2026-04-12 18:09:49 +0100
commitfe32909d58a59407350043851970cb3004ad351e (patch)
tree3e8d58df44ffd2de4b926f876b33081d3f285b59 /kaakaa
parent968c5f2c9df53139729aa5356ad5a802d1c88f37 (diff)
downloadkaagum-fe32909d58a59407350043851970cb3004ad351e.tar.gz
kaagum-fe32909d58a59407350043851970cb3004ad351e.tar.lz
kaagum-fe32909d58a59407350043851970cb3004ad351e.zip
Rename project to kaagum.
kaakaa reminds too many Europeans of shit. 😅
Diffstat (limited to 'kaakaa')
-rw-r--r--kaakaa/config.scm.in27
-rw-r--r--kaakaa/container.scm50
-rw-r--r--kaakaa/lens.scm62
-rw-r--r--kaakaa/openai.scm79
-rw-r--r--kaakaa/records.scm165
-rw-r--r--kaakaa/tea.scm784
-rw-r--r--kaakaa/tools.scm261
-rw-r--r--kaakaa/tools/base.scm225
-rw-r--r--kaakaa/utils.scm89
9 files changed, 0 insertions, 1742 deletions
diff --git a/kaakaa/config.scm.in b/kaakaa/config.scm.in
deleted file mode 100644
index 2c9e0d2..0000000
--- a/kaakaa/config.scm.in
+++ /dev/null
@@ -1,27 +0,0 @@
-;;; kaakaa --- Tiny, security-focused AI agent in Guile
-;;; Copyright © 2026 Arun Isaac <arunisaac@systemreboot.net>
-;;;
-;;; This file is part of kaakaa.
-;;;
-;;; kaakaa is free software: you can redistribute it and/or modify it
-;;; under the terms of the GNU General Public License as published by
-;;; the Free Software Foundation, either version 3 of the License, or
-;;; (at your option) any later version.
-;;;
-;;; kaakaa is distributed in the hope that it will be useful, but
-;;; WITHOUT ANY WARRANTY; without even the implied warranty of
-;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-;;; General Public License for more details.
-;;;
-;;; You should have received a copy of the GNU General Public License
-;;; along with kaakaa.  If not, see <https://www.gnu.org/licenses/>.
-
-(define-module (kaakaa config)
-  #:export (%project
-            %version))
-
-(define %project
-  "@PROJECT@")
-
-(define %version
-  "@VERSION@")
diff --git a/kaakaa/container.scm b/kaakaa/container.scm
deleted file mode 100644
index e7889bb..0000000
--- a/kaakaa/container.scm
+++ /dev/null
@@ -1,50 +0,0 @@
-;;; kaakaa --- Tiny, security-focused AI agent in Guile
-;;; Copyright © 2026 Arun Isaac <arunisaac@systemreboot.net>
-;;;
-;;; This file is part of kaakaa.
-;;;
-;;; kaakaa is free software: you can redistribute it and/or modify it
-;;; under the terms of the GNU General Public License as published by
-;;; the Free Software Foundation, either version 3 of the License, or
-;;; (at your option) any later version.
-;;;
-;;; kaakaa is distributed in the hope that it will be useful, but
-;;; WITHOUT ANY WARRANTY; without even the implied warranty of
-;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-;;; General Public License for more details.
-;;;
-;;; You should have received a copy of the GNU General Public License
-;;; along with kaakaa.  If not, see <https://www.gnu.org/licenses/>.
-
-(define-module (kaakaa container)
-  #:use-module (rnrs io ports)
-  #:use-module (ice-9 match)
-  #:use-module (gnu build linux-container)
-  #:use-module (guix utils)
-  #:use-module (kaakaa records)
-  #:export (call-with-container*))
-
-(define-public-record-type* (<container-result> container-result container-result?)
-  (fields (output container-result-output)
-          (exit-value container-result-exit-value)))
-
-(define (call-with-container* mounts namespaces thunk)
-  "Run @var{thunk} as a process in a container with @var{mounts} and
-@var{namespaces}, and return a @code{<container-result>} object."
-  (call-with-temporary-directory
-   (lambda (root)
-     (match (pipe)
-       ((in . out)
-        (match (waitpid (run-container root mounts namespaces 1
-                                       (lambda ()
-                                         (close-port in)
-                                         (with-output-to-port out
-                                           ;; TODO: Capture stderr too.
-                                           thunk)
-                                         (close-port out))))
-          ((_ . status)
-           (close-port out)
-           (let ((result (get-string-all in)))
-             (close-port in)
-             (container-result result
-                               (status:exit-val status))))))))))
diff --git a/kaakaa/lens.scm b/kaakaa/lens.scm
deleted file mode 100644
index f5c9370..0000000
--- a/kaakaa/lens.scm
+++ /dev/null
@@ -1,62 +0,0 @@
-;;; kaakaa --- Tiny, security-focused AI agent in Guile
-;;; Copyright © 2026 Arun Isaac <arunisaac@systemreboot.net>
-;;;
-;;; This file is part of kaakaa.
-;;;
-;;; kaakaa is free software: you can redistribute it and/or modify it
-;;; under the terms of the GNU General Public License as published by
-;;; the Free Software Foundation, either version 3 of the License, or
-;;; (at your option) any later version.
-;;;
-;;; kaakaa is distributed in the hope that it will be useful, but
-;;; WITHOUT ANY WARRANTY; without even the implied warranty of
-;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-;;; General Public License for more details.
-;;;
-;;; You should have received a copy of the GNU General Public License
-;;; along with kaakaa.  If not, see <https://www.gnu.org/licenses/>.
-
-(define-module (kaakaa lens)
-  #:use-module (srfi srfi-1)
-  #:use-module (srfi srfi-26)
-  #:use-module (srfi srfi-43)
-  #:use-module (lens)
-  #:export (vector-nth
-            in-json
-            push
-            prepend-over
-            alist-delete-over))
-
-(define (vector-nth n)
-  "Like @code{nth}, but for vectors."
-  (lens (cut vector-ref <> n)
-        (lambda (vec proc)
-          (vector-append (vector-copy vec 0 n)
-                         (vector (proc (vector-ref vec n)))
-                         (vector-copy vec (1+ n))))))
-
-(define in-json
-  (case-lambda
-    "Like @code{in}, but also allow integer components so that it is
-possible to traverse JSON trees."
-    (() (id))
-    ((key . tail)
-     (compose (apply in-json tail)
-              (if (string? key)
-                  (key-ref key)
-                  (vector-nth key))))))
-
-(define (push lens x object)
-  "Cons @var{x} onto the part of @var{object} that @var{lens} focuses
-on."
-  (over lens (cut cons x <>) object))
-
-(define (prepend-over lens lst object)
-  "Prepend @var{lst} to the part of @var{object} that @var{lens} focuses
-on."
-  (over lens (cut append lst <>) object))
-
-(define (alist-delete-over lens key object)
-  "Delete @var{key} from the association list in @var{object} that
-@var{lens} focuses on."
-  (over lens (cut alist-delete key <>) object))
diff --git a/kaakaa/openai.scm b/kaakaa/openai.scm
deleted file mode 100644
index 08a7254..0000000
--- a/kaakaa/openai.scm
+++ /dev/null
@@ -1,79 +0,0 @@
-;;; kaakaa --- Tiny, security-focused AI agent in Guile
-;;; Copyright © 2026 Arun Isaac <arunisaac@systemreboot.net>
-;;;
-;;; This file is part of kaakaa.
-;;;
-;;; kaakaa is free software: you can redistribute it and/or modify it
-;;; under the terms of the GNU General Public License as published by
-;;; the Free Software Foundation, either version 3 of the License, or
-;;; (at your option) any later version.
-;;;
-;;; kaakaa is distributed in the hope that it will be useful, but
-;;; WITHOUT ANY WARRANTY; without even the implied warranty of
-;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-;;; General Public License for more details.
-;;;
-;;; You should have received a copy of the GNU General Public License
-;;; along with kaakaa.  If not, see <https://www.gnu.org/licenses/>.
-
-(define-module (kaakaa openai)
-  #:use-module (rnrs conditions)
-  #:use-module (rnrs exceptions)
-  #:use-module (srfi srfi-11)
-  #:use-module (web client)
-  #:use-module (web http)
-  #:use-module (web response)
-  #:use-module (json)
-  #:export (get-api-key
-            openai-query))
-
-;; TODO: URIs must not be operated on using string operations. Replace with a
-;; more principled implementation involving (web uri).
-(define (uri-join base uri)
-  (string-append base uri))
-
-(define* (json-post url #:key headers json)
-  "Send a POST request to @var{url} with @var{json} body and additional
-@var{headers}. The @samp{Content-Type} header is set to @samp{application/json}
-and need not be specified in @var{headers}. Return JSON response."
-  (let-values (((response body)
-                (http-post url
-                           #:headers `((content-type application/json)
-                                       ,@headers)
-                           #:body (scm->json-string json)
-                           #:streaming? #t)))
-    ;; Guile does not consider application/json responses as textual, and does
-    ;; not automatically set the port encoding to UTF-8.
-    (set-port-encoding! body "UTF-8")
-    (case (quotient (response-code response)
-                    100)
-      ((2) (json->scm body))
-      ((4)
-       (raise-exception
-        (condition (make-violation)
-                   (make-message-condition
-                    (string-append "JSON API request failed with client error code "
-                                   (number->string (response-code response)))))))
-      (else
-       (raise-exception
-        (condition (make-error)
-                   (make-message-condition
-                    (string-append "JSON API request failed with code "
-                                   (number->string (response-code response))))))))))
-
-;; Declare the Authorization header as opaque so that Guile doesn't try to mess
-;; with it.
-(declare-opaque-header! "Authorization")
-
-(define (openai-query base-uri api-key model messages tools)
-  "Send a request to the OpenAI completions API and return the JSON response.
-@var{base-uri} is the base URI of the OpenAI-compatible service. @var{api-key}
-is the API key for authentication. @var{model} is a supported LLM model.
-@var{messages} and @var{tools} are respectively lists of JSON messages and tools
-compatible with the OpenAI API specification."
-  (json-post (uri-join base-uri "/api/v1/chat/completions")
-             #:headers `((authorization
-                          . ,(string-append "Bearer " api-key)))
-             #:json `(("model" . ,model)
-                      ("messages" . ,(list->vector messages))
-                      ("tools" . ,(list->vector tools)))))
diff --git a/kaakaa/records.scm b/kaakaa/records.scm
deleted file mode 100644
index 7cdac66..0000000
--- a/kaakaa/records.scm
+++ /dev/null
@@ -1,165 +0,0 @@
-;;; kaakaa --- Tiny, security-focused AI agent in Guile
-;;; Copyright © 2026 Arun Isaac <arunisaac@systemreboot.net>
-;;;
-;;; This file is part of kaakaa.
-;;;
-;;; kaakaa is free software: you can redistribute it and/or modify it
-;;; under the terms of the GNU General Public License as published by
-;;; the Free Software Foundation, either version 3 of the License, or
-;;; (at your option) any later version.
-;;;
-;;; kaakaa is distributed in the hope that it will be useful, but
-;;; WITHOUT ANY WARRANTY; without even the implied warranty of
-;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-;;; General Public License for more details.
-;;;
-;;; You should have received a copy of the GNU General Public License
-;;; along with kaakaa.  If not, see <https://www.gnu.org/licenses/>.
-
-(define-module (kaakaa records)
-  #:use-module (rnrs records procedural)
-  #:use-module (srfi srfi-1)
-  #:use-module (srfi srfi-26)
-  #:use-module (ice-9 match)
-  #:use-module (lens)
-  #:export (define-record-type*
-            define-public-record-type*))
-
-(define (make-record-type* record-name make-constructor . fields)
-  "Create a record type with @var{record-name} and @var{fields}.
-@var{make-constructor} is a function that is passed a basic record constructor
-accepting positional arguments and must return the record constructor. Return
-@code{length(fields) + 3} values---the record type descriptor, the record
-constructor, the record predicate and the field accessors.
-
-@var{fields} is a list of field specifiers each of which is of the form
-@code{(field-name accessor-type)}. @var{accessor-type} is either
-@code{'accessor} or @code{'lensed}."
-  (let* ((rtd (make-record-type-descriptor record-name #f #f #f #f
-                                           (list->vector (map (match-lambda
-                                                                ((field-name _)
-                                                                 field-name))
-                                                              fields))))
-         (constructor (record-constructor
-                       (make-record-constructor-descriptor rtd #f #f)))
-         (accessors (map (cut record-accessor rtd <>)
-                         (iota (length fields)))))
-    (apply values
-           rtd
-           (make-constructor constructor)
-           (record-predicate rtd)
-           (map (match-lambda*
-                  ((_ accessor (_ 'accessor))
-                   accessor)
-                  ((index accessor (_ 'lensed))
-                   (lens accessor
-                         (lambda (record proc)
-                           (apply constructor
-                                  (append (map (lambda (accessor)
-                                                 (accessor record))
-                                               (take accessors index))
-                                          (list (proc ((list-ref accessors index)
-                                                       record)))
-                                          (map (lambda (accessor)
-                                                 (accessor record))
-                                               (drop accessors (1+ index)))))))))
-                (iota (length fields))
-                accessors
-                fields))))
-
-(define-syntax define-record-type*
-  (lambda (x)
-    "Define a record type. All fields are immutable and may optionally have lenses as
-accessors.
-
-Lenses are procedures that combine getters and setters into one structure. They
-allow you to conveniently manipulate parts of deeply nested data structures in a
-composable and purely functional way.
-
-Consider the following example record definition:
-@example
-(define-record-type* (<employee> employee employee?)
-  (name employee-name)
-  (age employee-age lensed)
-  (salary employee-salary lensed))
-@end example
-In this example, @code{employee-name} is a regular accessor, while
-@code{employee-age} and @code{employee-salary} are lenses.
-
-@code{employee-name} is a regular accessor. Get with:
-@example
-(employee-name x)
-@end example
-
-@code{employee-age} is a lens. Get with:
-@example
-(focus employee-age x)
-@end example
-
-Functionally update with:
-@example
-(put employee-age 25 x)
-@end example
-
-Record definitions may also optionally specify a @code{make-constructor}
-argument which is passed to @code{make-record-type*}. For example:
-@example
-(define-record-type* (<employee> employee employee?)
-  (lambda (constructor)
-    (lambda* (name #:key age salary)
-      (constructor name age salary)))
-  (fields (name employee-name)
-          (age employee-age lensed)
-          (salary employee-salary lensed)))
-@end example
-"
-    (syntax-case x (fields)
-      ((_ (record-name constructor-name predicate-name)
-          make-constructor
-          (fields field-spec ...))
-       #`(define-values (record-name constructor-name predicate-name
-                                     #,@(map (lambda (x)
-                                               (syntax-case x ()
-                                                 ((_ accessor-name _ ...)
-                                                  #'accessor-name)))
-                                             #'(field-spec ...)))
-           (make-record-type* 'record-name
-                              make-constructor
-                              #,@(map (lambda (x)
-                                        (syntax-case x (lensed)
-                                          ((field-name accessor-name)
-                                           #''(field-name accessor))
-                                          ((field-name accessor-name lensed)
-                                           #''(field-name lensed))))
-                                      #'(field-spec ...)))
-           ))
-      ((_ (record-name constructor-name predicate-name)
-          (fields field-spec ...))
-       #'(define-record-type* (record-name constructor-name predicate-name)
-           identity
-           (fields field-spec ...))))))
-
-(define-syntax define-public-record-type*
-  (lambda (x)
-    "Like @code{define-record-type*}, but also export the constructor, the predicate
-and the accessors."
-    (syntax-case x (fields)
-      ((_ (record-name constructor-name predicate-name)
-          make-constructor
-          (fields field-spec ...))
-       #`(begin
-           (define-record-type* (record-name constructor-name predicate-name)
-             make-constructor
-             (fields field-spec ...))
-           (export constructor-name)
-           (export predicate-name)
-           #,@(map (lambda (x)
-                     (syntax-case x ()
-                       ((_ accessor-name _ ...)
-                        #'(export accessor-name))))
-                   #'(field-spec ...))))
-      ((_ (record-name constructor-name predicate-name)
-          (fields field-spec ...))
-       #'(define-public-record-type* (record-name constructor-name predicate-name)
-           identity
-           (fields field-spec ...))))))
diff --git a/kaakaa/tea.scm b/kaakaa/tea.scm
deleted file mode 100644
index 1660636..0000000
--- a/kaakaa/tea.scm
+++ /dev/null
@@ -1,784 +0,0 @@
-;;; kaakaa --- Tiny, security-focused AI agent in Guile
-;;; Copyright © 2026 Arun Isaac <arunisaac@systemreboot.net>
-;;;
-;;; This file is part of kaakaa.
-;;;
-;;; kaakaa is free software: you can redistribute it and/or modify it
-;;; under the terms of the GNU General Public License as published by
-;;; the Free Software Foundation, either version 3 of the License, or
-;;; (at your option) any later version.
-;;;
-;;; kaakaa is distributed in the hope that it will be useful, but
-;;; WITHOUT ANY WARRANTY; without even the implied warranty of
-;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-;;; General Public License for more details.
-;;;
-;;; You should have received a copy of the GNU General Public License
-;;; along with kaakaa.  If not, see <https://www.gnu.org/licenses/>.
-
-(define-module (kaakaa tea)
-  #:use-module ((rnrs base) #:select (assertion-violation))
-  #:use-module (rnrs exceptions)
-  #:use-module (rnrs io ports)
-  #:use-module (rnrs records syntactic)
-  #:use-module (srfi srfi-1)
-  #:use-module (srfi srfi-11)
-  #:use-module (srfi srfi-26)
-  #:use-module (ice-9 match)
-  #:use-module (ice-9 regex)
-  #:use-module (json)
-  #:use-module (lens)
-  #:use-module (kaakaa lens)
-  #:use-module (kaakaa openai)
-  #:use-module (kaakaa records)
-  #:use-module (kaakaa tools)
-  #:use-module (kaakaa utils)
-  #:export (initial-state
-            tea-loop))
-
-(define %tool-allow-once
-  '(("optionId" . "allow-once")
-    ("name" . "Allow once")
-    ("kind" . "allow_once")))
-
-(define %tool-allow-always
-  '(("optionId" . "allow-always")
-    ("name" . "Allow always")
-    ("kind" . "allow_always")))
-
-(define %tool-reject-once
-  '(("optionId" . "reject-once")
-    ("name" . "Reject once")
-    ("kind" . "reject_once")))
-
-(define %tool-reject-always
-  '(("optionId" . "reject-always")
-    ("name" . "Reject always")
-    ("kind" . "reject_always")))
-
-(define-record-type* (<session> session session?)
-  (lambda (constructor)
-    (lambda* (cwd #:key
-                  cancelling? (messages '()) (pending-tool-calls '())
-                  (allowed-tools '()) (rejected-tools '()))
-      (constructor cwd cancelling? messages pending-tool-calls
-                   allowed-tools rejected-tools)))
-  (fields (cwd session-cwd lensed)
-          (cancelling? session-cancelling? lensed)
-          (messages session-messages lensed)
-          (tool-calls session-tool-calls lensed)
-          ;; List of tool names that are allowlisted for the session
-          (allowed-tools session-allowed-tools lensed)
-          ;; List of tool names that are blocklisted for the session
-          (rejected-tools session-rejected-tools lensed)))
-
-(define-record-type* (<state> state state?)
-  (fields (client-request-id state-client-request-id lensed)
-          (agent-request-id state-agent-request-id lensed)
-          (next-session-id state-next-session-id lensed)
-          ;; Association list mapping agent request IDs to tool calls for which
-          ;; permission is sought
-          (requests-alist state-requests-alist lensed)
-          (sessions state-sessions lensed)))
-
-(define (initial-state)
-  (state #f 0 0 '() '()))
-
-(define (state-cwd session-id)
-  "Return a lens to focus on current working directory of session with
-@var{session-id} in state."
-  (compose session-cwd
-           (key-ref session-id)
-           state-sessions))
-
-(define (state-messages session-id)
-  "Return a lens to focus on messages of session with @var{session-id} in state."
-  (compose session-messages
-           (key-ref session-id)
-           state-sessions))
-
-(define (state-tool-calls session-id)
-  "Return a lens to focus on tool calls of session with @var{session-id} in state."
-  (compose session-tool-calls
-           (key-ref session-id)
-           state-sessions))
-
-(define (state-allowed-tools session-id)
-  "Return a lens to focus on allowed tools of session with @var{session-id} in
-state."
-  (compose session-allowed-tools
-           (key-ref session-id)
-           state-sessions))
-
-(define (state-rejected-tools session-id)
-  "Return a lens to focus on rejected tools of session with @var{session-id} in
-state."
-  (compose session-rejected-tools
-           (key-ref session-id)
-           state-sessions))
-
-(define (state-tool-call tool-call-id session-id)
-  "Return a lens to focus on tool call with @var{tool-call-id} of session with
-@var{session-id} in state."
-  (compose (key-ref tool-call-id)
-           (state-tool-calls session-id)))
-
-(define (state-session-cancelling? session-id)
-  "Return a lens to focus on the @code{cancelling?} flag of session with
-@var{session-id} in state."
-  (compose session-cancelling?
-           (key-ref session-id)
-           state-sessions))
-
-(define (state->llm-requests session-id state)
-  "Return a list of @code{<llm-request>} objects for session with @var{session-id}
-in @var{state}."
-  (if (and (null? (focus (state-tool-calls session-id)
-                         state))
-           (not (focus (state-session-cancelling? session-id)
-                       state)))
-      ;; There are no more tool calls in flight and a cancellation is not in
-      ;; progress; dispatch to LLM.
-      (list (llm-request session-id
-                         (map (lambda (message)
-                                ;; Strip unnecessary fields (such as reasoning
-                                ;; fields) based on role.
-                                (let* ((role (focus (key-ref "role") message))
-                                       (allowed-fields
-                                        (cond
-                                         ((string=? role "user")
-                                          '("role" "content"))
-                                         ((string=? role "assistant")
-                                          '("role" "content" "tool_calls"))
-                                         ((string=? role "tool")
-                                          '("role" "content" "tool_call_id")))))
-                                  (filter (match-lambda
-                                            ((key . _)
-                                             (member key allowed-fields)))
-                                          message)))
-                              ;; Reverse because we have been prepending new
-                              ;; messages onto the list.
-                              (reverse (focus (state-messages session-id)
-                                              state)))))
-      ;; There are tool calls or a cancellation in progress; do nothing.
-      (list)))
-
-(define-record-type* (<llm-request> llm-request llm-request?)
-  (fields (session-id llm-request-session-id)
-          (messages llm-request-messages)))
-
-(define-record-type* (<llm-response> llm-response llm-response?)
-  (fields (session-id llm-response-session-id)
-          (json llm-response-json)))
-
-(define-record-type* (<acp-message> acp-message acp-message?)
-  (fields (json acp-message-json lensed)))
-
-(define-record-type* (<command> command command?)
-  (fields (description command-description)
-          (next-state command-next-state)))
-
-(define (command->spec name command)
-  "Serialize @var{command} of @var{name} to ACP-compatible JSON spec."
-  `(("name" . ,name)
-    ("description" . ,(command-description command))))
-
-(define (agent-message-chunk session-id text)
-  "Return an @samp{agent_message_chunk} @samp{session/update} ACP message for
-@var{session-id} with @var{text}."
-  (acp-message `(("jsonrpc" . "2.0")
-                 ("method" . "session/update")
-                 ("params"
-                  ("sessionId" . ,session-id)
-                  ("update"
-                   ("sessionUpdate" . "agent_message_chunk")
-                   ("content"
-                    ("type" . "text")
-                    ("text" . ,text)))))))
-
-(define %cwd-command
-  (command "Print current working directory of the session"
-           (lambda (state session-id tools argument)
-             (values state
-                     (list (agent-message-chunk session-id
-                                                (focus (state-cwd session-id)
-                                                       state)))))))
-
-(define (markdown-table lines)
-  "Return a markdown table built from @var{lines}. Each line is a list of strings,
-each string the contents of a cell. The first line is considered the header of
-the table."
-  (define (cells->line cells)
-    (string-append "| "
-                   (string-join cells " | ")
-                   " |"))
-
-  (match lines
-    ((header other-lines ...)
-     (string-join (cons* (cells->line header)
-                         (cells->line (make-list (length header) "---"))
-                         (map cells->line
-                              other-lines))
-                  "\n"))))
-
-(define %tools-command
-  (command "List available tools and their permission status"
-           (lambda (state session-id tools argument)
-             (let* ((allowed-tools (focus (state-allowed-tools session-id)
-                                          state))
-                    (rejected-tools (focus (state-rejected-tools session-id)
-                                           state))
-                    (lines
-                     (cons (list "Tool" "Permission")
-                           (map (match-lambda
-                                  ((name . _)
-                                   (list name
-                                         (cond
-                                          ((member name allowed-tools) "allow")
-                                          ((member name rejected-tools) "reject")
-                                          (else "prompt user")))))
-                                tools))))
-               (values state
-                       (list (agent-message-chunk session-id
-                                                  (markdown-table lines))))))))
-
-(define %commands
-  `(("cwd" . ,%cwd-command)
-    ("tools" . ,%tools-command)))
-
-(define (next-state-slash-command state session-id tools command-name argument)
-  "Given current @var{state} and an invocation of slash var{command-name} with
-@var{argument} for @var{session-id}, return the next state and a list of
-effects.
-
-@var{tools} is the same as in @code{tea-loop}."
-  (cond
-   ;; command exists
-   ((focus (key-ref command-name)
-           %commands)
-    => (lambda (command)
-         ((command-next-state command) state session-id tools argument)))
-   ;; command not found
-   (else
-    (values state
-            (list (agent-message-chunk session-id
-                                       "Error: Unknown command"))))))
-
-(define (next-state-client-request state request tools)
-  "Given current @var{state} and a new ACP @var{request}, return the next state and
-a list of effects.
-
-@var{tools} is the same as in @code{tea-loop}."
-  (let ((request-id (focus (key-ref "id")
-                           request)))
-    (cond
-     ;; There is a pending request from the client; process it.
-     ((focus (key-ref "method") request)
-      => (match-lambda
-           ("initialize"
-            (values state
-                    (list (acp-message `(("jsonrpc" . "2.0")
-                                         ("id" . ,request-id)
-                                         ("result"
-                                          ("protocolVersion" . 1)
-                                          ("agentCapabilities")
-                                          ("agentInfo"
-                                           ("name" . "kaakaa")
-                                           ("title" . "Kaakaa")
-                                           ("version" . "0.1.0"))
-                                          ("authMethods" . #())))))))
-           ("session/new"
-            (let ((session-id
-                   (string-append "session-"
-                                  (number->string
-                                   (focus state-next-session-id state)))))
-              (values (-> state
-                          ;; Push new session onto list.
-                          (push state-sessions
-                                (cons session-id
-                                      ;; TODO: Check if cwd is an absolute path.
-                                      (session (focus (in "params" "cwd")
-                                                      request)))
-                                <>)
-                          ;; Increment next session ID.
-                          (over state-next-session-id
-                                1+
-                                <>))
-                      (map acp-message
-                           `(;; Return new session.
-                             (("jsonrpc" . "2.0")
-                              ("id" . ,request-id)
-                              ("result"
-                               ("sessionId" . ,session-id)))
-                             ;; Advertise available commands.
-                             (("jsonrpc" . "2.0")
-                              ("method" . "session/update")
-                              ("params"
-                               ("sessionId" . ,session-id)
-                               ("update"
-                                ("sessionUpdate" . "available_commands_update")
-                                ("availableCommands" .
-                                 ,(list->vector (map (match-lambda
-                                                       ((name . command)
-                                                        (command->spec name command)))
-                                                     %commands)))))))))))
-           ("session/prompt"
-            (let ((session-id (focus (in "params" "sessionId")
-                                     request))
-                  ;; TODO: Filter to only allow "text" type content blocks.
-                  (prompt (focus (in "params" "prompt")
-                                 request)))
-              (cond
-               ;; slash command
-               ((string-match "^/([a-z0-9]*)(.*)"
-                              (focus (in-json 0 "text")
-                                     prompt))
-                => (lambda (mtch)
-                     (let ((command-name (match:substring mtch 1))
-                           (argument (string-trim (match:substring mtch 2))))
-                       (let-values (((state effects)
-                                     (next-state-slash-command
-                                      state session-id tools command-name argument)))
-                         (values state
-                                 ;; End prompt turn immediately. This means
-                                 ;; slash commands cannot send LLM requests or
-                                 ;; initiate other exchanges.
-                                 (append effects
-                                         (list (acp-message
-                                                `(("jsonrpc" . "2.0")
-                                                  ("id" . ,request-id)
-                                                  ("result"
-                                                   ("stopReason" . "end_turn")))))))))))
-               ;; regular prompt
-               (else
-                (let ((state (push (state-messages session-id)
-                                   `(("role" . "user")
-                                     ("content" . ,prompt))
-                                   state)))
-                  (values state
-                          (state->llm-requests session-id state)))))))
-           ("session/cancel"
-            (let ((session-id (focus (in "params" "sessionId")
-                                     request)))
-              ;; Reset state and end the prompt turn.
-              (values (-> state
-                          (put (state-session-cancelling? session-id)
-                               #f
-                               <>)
-                          (put (state-tool-calls session-id)
-                               '()
-                               <>)
-                          (put state-requests-alist
-                               '()
-                               <>))
-                      (list (acp-message `(("jsonrpc" . "2.0")
-                                           ("id" . ,(focus state-client-request-id
-                                                           state))
-                                           ("result"
-                                            ("stopReason" . "cancelled")))))))))))))
-
-(define (next-state-client-response state response)
-  "Given current @var{state} and a new ACP @var{response}, return the next state
-and a list of effects."
-  (let ((request-id (focus (key-ref "id")
-                           response)))
-    (cond
-     ;; The tool call for this response has been found; process it.
-     ((focus (compose (key-ref request-id)
-                      state-requests-alist)
-             state)
-      => (lambda (tool-call)
-           (let* ((session-id (tool-call-session-id tool-call))
-                  (outcome (focus (in "result" "outcome" "outcome")
-                                  response))
-                  (option-id (focus (in "result" "outcome" "optionId")
-                                    response))
-                  ;; Decode permission selection to symbol.
-                  (selection
-                   (cond
-                    ((and (string=? outcome "selected")
-                          (string=? option-id
-                                    (focus (key-ref "optionId") %tool-allow-once)))
-                     'allow-once)
-                    ((and (string=? outcome "selected")
-                          (string=? option-id
-                                    (focus (key-ref "optionId") %tool-allow-always)))
-                     'allow-always)
-                    ((and (string=? outcome "selected")
-                          (string=? option-id
-                                    (focus (key-ref "optionId") %tool-reject-once)))
-                     'reject-once)
-                    ((and (string=? outcome "selected")
-                          (string=? option-id
-                                    (focus (key-ref "optionId") %tool-reject-always)))
-                     'reject-always)
-                    ;; We don't explicitly look for "cancelled". We defensively
-                    ;; assume anything other than "selected" is "cancelled".
-                    ;; This protects us from buggy clients.
-                    (else 'cancel)))
-                  (state
-                   (-> state
-                       ;; If the tool was "allowed always", add it to the list
-                       ;; of allowed tools.
-                       (over (state-allowed-tools session-id)
-                             (lambda (allowed-tools)
-                               (if (eq? selection 'allow-always)
-                                   (cons (tool-call-function tool-call)
-                                         allowed-tools)
-                                   allowed-tools))
-                             <>)
-                       ;; If the tool was "rejected always", add it to the list
-                       ;; of rejected tools.
-                       (over (state-rejected-tools session-id)
-                             (lambda (rejected-tools)
-                               (if (eq? selection 'reject-always)
-                                   (cons (tool-call-function tool-call)
-                                         rejected-tools)
-                                   rejected-tools))
-                             <>)
-                       ;; Update tool status.
-                       (put (compose tool-call-status
-                                     (state-tool-call (tool-call-id tool-call)
-                                                      session-id))
-                            (cond
-                             ((eq? selection 'cancel) 'cancelled)
-                             ((memq selection '(allow-once allow-always))
-                              'approved)
-                             ((memq selection '(reject-once reject-always))
-                              'rejected)
-                             ;; This branch should be unreachable.
-                             (else
-                              (assertion-violation selection
-                                                   "Invalid selection")))
-                            <>)
-                       ;; Update tool status.
-                       (put (compose tool-call-status
-                                     (state-tool-call (tool-call-id tool-call)
-                                                      session-id))
-                            (cond
-                             ((eq? selection 'cancel) 'cancelled)
-                             ((memq selection '(allow-once allow-always))
-                              'approved)
-                             ((memq selection '(reject-once reject-always))
-                              'rejected)
-                             ;; This branch should be unreachable.
-                             (else
-                              (assertion-violation selection
-                                                   "Invalid selection")))
-                            <>)
-                       ;; If the tool call was cancelled, set the cancelling flag
-                       ;; to indicate that a cancellation is in progress.
-                       (put (state-session-cancelling? session-id)
-                            (eq? selection 'cancel)
-                            <>)
-                       ;; Unregister request corresponding to this response.
-                       (alist-delete-over state-requests-alist
-                                          request-id
-                                          <>))))
-             (values state
-                     ;; Request tool call evaluation. eval-tool-call handles
-                     ;; cancelled and rejected tool calls correctly. We don't
-                     ;; have to worry about it here.
-                     (list (focus (state-tool-call (tool-call-id tool-call)
-                                                   (tool-call-session-id tool-call))
-                                  state))))))
-     ;; Client response is stale (it pertains to a request not in
-     ;; requests-alist). Silently ignore it.
-     (else (values state '())))))
-
-(define (next-state-send-agent-request state request context)
-  "Return an updated @var{state} and a list of effects sending @var{request} from
-the agent to the client. Stash @var{context} against request ID in
-@code{requests-alist} for future recall."
-  (let ((request-id (focus state-agent-request-id state)))
-    (values (-> state
-                ;; Register this request so we can recall the details later when
-                ;; the response comes in.
-                (push state-requests-alist
-                      (cons request-id
-                            context)
-                      <>)
-                ;; Bump agent request ID.
-                (over state-agent-request-id
-                      1+
-                      <>))
-            ;; Set request ID in request JSON.
-            (list (if (assoc "id" (focus acp-message-json request))
-                      ;; The request JSON already has an "id" field; overwrite
-                      ;; it.
-                      (put (compose (key-ref "id") acp-message-json)
-                           request-id
-                           request)
-                      ;; The request JSON has no "id" field; cons it on;
-                      (over acp-message-json
-                            (cut cons
-                                 (cons "id" request-id)
-                                 <>)
-                            request))))))
-
-(define (next-state-tool-call state session-id call-json tools)
-  "Given current @var{state} and a new tool @var{call-json}, return the next state
-and a list of effects.
-
-@var{tools} is the same as in @code{tea-loop}."
-  (guard (c ((tool-call-parse-failure? c)
-             (let ((call-id (focus (key-ref "id") call-json)))
-               (values (-> state
-                           ;; Push tool call response onto messages.
-                           (push (state-messages session-id)
-                                 `(("role" . "tool")
-                                   ("tool_call_id" . ,call-id)
-                                   ("content" . ,(tool-call-parse-failure-message c)))
-                                 <>))
-                       ;; Notify client about invalid tool call.
-                       (list (acp-message `(("jsonrpc" . "2.0")
-                                            ("method" . "session/update")
-                                            ("params"
-                                             ("sessionId" . ,session-id)
-                                             ("update"
-                                              ("sessionUpdate" . "tool_call_update")
-                                              ("toolCallId" . ,call-id)
-                                              ("status" . "failed"))))))))))
-    (let* ((call (spec->tool-call session-id
-                                  (focus (state-cwd session-id)
-                                         state)
-                                  tools
-                                  (focus (state-allowed-tools session-id)
-                                         state)
-                                  (focus (state-rejected-tools session-id)
-                                         state)
-                                  call-json))
-           (state
-            ;; Queue tool call.
-            (push (state-tool-calls session-id)
-                  (cons (tool-call-id call)
-                        call)
-                  state)))
-      (let-values (((state effects)
-                    (if (eq? (focus tool-call-status call)
-                             'pending-approval)
-                        ;; Tool call requires permission from the client;
-                        ;; dispatch a request.
-                        (next-state-send-agent-request
-                         state
-                         (acp-message `(("jsonrpc" . "2.0")
-                                        ("method" . "session/request_permission")
-                                        ("params"
-                                         ("sessionId" . ,session-id)
-                                         ("toolCall"
-                                          ("toolCallId" . ,(tool-call-id call)))
-                                         ("options" .
-                                          ,(vector %tool-allow-once
-                                                   %tool-allow-always
-                                                   %tool-reject-once
-                                                   %tool-reject-always)))))
-                         call)
-                        ;; Tool call is already pre-approved or pre-rejected;
-                        ;; schedule it.
-                        (values state
-                                (list call)))))
-        (values state
-                ;; Notify client about new tool call before other effects.
-                (cons (let ((tool (focus (key-ref (tool-call-function call))
-                                         tools))
-                            (args (alist->plist (tool-call-arguments call))))
-                        (acp-message `(("jsonrpc" . "2.0")
-                                       ("method" . "session/update")
-                                       ("params"
-                                        ("sessionId" . ,session-id)
-                                        ("update"
-                                         ("sessionUpdate" . "tool_call")
-                                         ("toolCallId" . ,(tool-call-id call))
-                                         ("title" . ,(apply (tool-title tool) args))
-                                         ("kind" . ,(tool-kind tool))
-                                         ("rawInput" . ,(tool-call-arguments call))
-                                         ("status" . "pending"))))))
-                      effects))))))
-
-(define (next-state-llm-response state response tools)
-  "Given current @var{state} and a new LLM @var{response}, return the next state
-and a list of effects.
-
-@var{tools} is the same as in @code{tea-loop}."
-  (let* ((session-id (llm-response-session-id response))
-         (llm-reply (focus (in-json "choices" 0 "message")
-                           (llm-response-json response)))
-         (tool-calls-json (cond
-                           ((focus (key-ref "tool_calls")
-                                   llm-reply)
-                            => vector->list)
-                           (else '()))))
-    (let-values (((state tool-call-effects)
-                  (foldn (lambda (call-json state effects)
-                           (let-values (((state new-effects)
-                                         (next-state-tool-call
-                                          state session-id call-json tools)))
-                             (values state
-                                     (append new-effects effects))))
-                         tool-calls-json
-                         (-> state
-                             ;; Push LLM response onto messages.
-                             (push (state-messages session-id)
-                                   llm-reply
-                                   <>))
-                         '())))
-      (values state
-              (cons (agent-message-chunk session-id
-                                         ;; Send LLM's text response.
-                                         (focus (key-ref "content")
-                                                llm-reply))
-                    (append tool-call-effects
-                            ;; End prompt turn if there are no further tool
-                            ;; calls and a cancellation is not in progress.
-                            (if (and (null? tool-calls-json)
-                                     (not (focus (state-session-cancelling? session-id)
-                                                 state)))
-                                (list (acp-message `(("jsonrpc" . "2.0")
-                                                     ("id" . ,(focus state-client-request-id
-                                                                     state))
-                                                     ("result"
-                                                      ("stopReason" . "end_turn")))))
-                                '())
-                            ;; All tool calls may have been invalid. Maybe
-                            ;; dispatch LLM requests, but only if there were any
-                            ;; in the first place.
-                            (if (not (null? tool-calls-json))
-                                (state->llm-requests session-id state)
-                                '())))))))
-
-(define (next-state-tool-call-result state result)
-  "Given current @var{state} and a new tool call @var{result}, return the next
-state and a list of effects."
-  (let* ((session-id (tool-call-result-session-id result))
-         (state (-> state
-                    ;; Push tool call result onto messages.
-                    (push (state-messages session-id)
-                          (tool-call-result-json result)
-                          <>)
-                    ;; Delete tool call from session tool call list.
-                    (alist-delete-over (state-tool-calls session-id)
-                                       (tool-call-result-call-id result)
-                                       <>))))
-    (values state
-            ;; Send a notification for each tool call evaluated.
-            (cons (acp-message `(("jsonrpc" . "2.0")
-                                 ("method" . "session/update")
-                                 ("params"
-                                  ("sessionId" . ,session-id)
-                                  ("update"
-                                   ;; TODO: Add locations and rawOutput.
-                                   ("sessionUpdate" . "tool_call_update")
-                                   ("toolCallId" . ,(tool-call-result-call-id result))
-                                   ("status" .
-                                    ,(if (tool-call-result-success? result)
-                                         "completed"
-                                         "failed"))
-                                   ("content" .
-                                    ,(vector `(("type" . "content")
-                                               ("content"
-                                                ("type" . "text")
-                                                ("text" .
-                                                 ,(focus (key-ref "content")
-                                                         (tool-call-result-json result)))))))))))
-                  (state->llm-requests session-id state)))))
-
-(define (next-state state message tools)
-  "Given current @var{state} and a new @var{message}, return the next state and a
-list of effects.
-
-@var{tools} is the same as in @code{tea-loop}."
-  (cond
-   ((acp-message? message)
-    (let ((json-message (focus acp-message-json message)))
-      (if (focus (key-ref "result") json-message)
-          ;; message is a response from the client.
-          (next-state-client-response state json-message)
-          ;; message is a request/notification from the client.
-          (let-values (((state effects)
-                        (next-state-client-request state json-message tools)))
-            (values (cond
-                     ;; message is a request from the client.
-                     ((focus (key-ref "id") json-message)
-                      => (cut put
-                              state-client-request-id
-                              <>
-                              state))
-                     ;; message is a notification from the client.
-                     (else state))
-                    effects)))))
-   ((llm-response? message)
-    (next-state-llm-response state message tools))
-   ((tool-call-result? message)
-    (next-state-tool-call-result state message))))
-
-(define (tea-loop state llm-base-uri llm-api-key model tools)
-  "Run a @acronym{TEA, The Elm Architecture} loop starting with @var{state}.
-
-@var{llm-base-uri} is the base URI of the LLM provider. @var{llm-api-key} is the
-API key to authenticate with the LLM provider. @var{model} is the name of the
-model. @var{tools} is the list of tools made available to the LLM. It is an
-association list matching tool names to @code{<tool>} objects."
-  ;; Read a JSON-RPC message, handle it, and loop.
-  (let ((line (get-line (current-input-port))))
-    (unless (eof-object? line)
-      (tea-loop (handle-event (acp-message (json-string->scm line))
-                              state
-                              llm-base-uri
-                              llm-api-key
-                              model
-                              tools)
-                llm-base-uri
-                llm-api-key
-                model
-                tools))))
-
-(define (handle-event event state llm-base-uri llm-api-key model tools)
-  "Handle @var{event} with @var{state} and return a new state.
-
-@var{llm-base-uri}, @var{llm-api-key}, @var{model} and @var{tools} are the same
-as in @code{tea-loop}."
-  (let-values (((state effects)
-                ;; Compute the next state and collect the effects.
-                (next-state state event tools)))
-    ;; Do the effects.
-    (fold (cut do-effect <> <> llm-base-uri llm-api-key model tools)
-          state
-          effects)))
-
-(define (do-effect effect state llm-base-uri llm-api-key model tools)
-  "Do @var{effect} with @var{state} and return a new state.
-
-@var{llm-base-uri}, @var{llm-api-key}, @var{model} and @var{tools} are the same
-as in @code{tea-loop}."
-  (cond
-   ;; Send message to client, and return the state unchanged.
-   ((acp-message? effect)
-    (display (scm->json-string (focus acp-message-json effect)))
-    (newline)
-    (flush-output-port (current-output-port))
-    state)
-   ;; Send request to LLM, handle the response, and return the new state.
-   ((llm-request? effect)
-    ;; TODO: Handle network failures in OpenAI query.
-    (handle-event (llm-response (llm-request-session-id effect)
-                                (openai-query llm-base-uri
-                                              llm-api-key
-                                              model
-                                              (llm-request-messages effect)
-                                              (map (match-lambda
-                                                     ((name . tool)
-                                                      (tool->spec name tool)))
-                                                   tools)))
-                  state
-                  llm-base-uri
-                  llm-api-key
-                  model
-                  tools))
-   ;; Evaluate tool, handle the result, and return the new state.
-   ((tool-call? effect)
-    (handle-event (eval-tool-call effect tools)
-                  state
-                  llm-base-uri
-                  llm-api-key
-                  model
-                  tools))))
diff --git a/kaakaa/tools.scm b/kaakaa/tools.scm
deleted file mode 100644
index ef5d7ed..0000000
--- a/kaakaa/tools.scm
+++ /dev/null
@@ -1,261 +0,0 @@
-;;; kaakaa --- Tiny, security-focused AI agent in Guile
-;;; Copyright © 2026 Arun Isaac <arunisaac@systemreboot.net>
-;;;
-;;; This file is part of kaakaa.
-;;;
-;;; kaakaa is free software: you can redistribute it and/or modify it
-;;; under the terms of the GNU General Public License as published by
-;;; the Free Software Foundation, either version 3 of the License, or
-;;; (at your option) any later version.
-;;;
-;;; kaakaa is distributed in the hope that it will be useful, but
-;;; WITHOUT ANY WARRANTY; without even the implied warranty of
-;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-;;; General Public License for more details.
-;;;
-;;; You should have received a copy of the GNU General Public License
-;;; along with kaakaa.  If not, see <https://www.gnu.org/licenses/>.
-
-(define-module (kaakaa tools)
-  #:use-module ((rnrs base) #:select (assertion-violation))
-  #:use-module (rnrs conditions)
-  #:use-module (rnrs exceptions)
-  #:use-module (rnrs records syntactic)
-  #:use-module (srfi srfi-1)
-  #:use-module (ice-9 match)
-  #:use-module (gnu build linux-container)
-  #:use-module (gnu system file-systems)
-  #:use-module (json)
-  #:use-module (lens)
-  #:use-module (kaakaa container)
-  #:use-module (kaakaa records)
-  #:use-module (kaakaa utils)
-  #:export (tool-call-parse-failure?
-            tool-call-parse-failure-message
-
-            tool->spec
-            spec->tool-call
-            eval-tool-call))
-
-(define (readonly-cwd-only-mappings cwd arguments)
-  (list (file-system-mapping
-          (source cwd)
-          (target source)
-          (writable? #f))))
-
-(define-public-record-type* (<tool> tool tool?)
-  (lambda (constructor)
-    (lambda* (#:key
-              description (parameters '()) proc
-              (container-mappings readonly-cwd-only-mappings)
-              (container-namespaces %namespaces)
-              title kind)
-      (unless description
-        (raise-exception
-         (condition (make-violation)
-                    (make-message-condition "tool requires description"))))
-      (unless proc
-        (raise-exception
-         (condition (make-violation)
-                    (make-message-condition "tool requires proc"))))
-      (constructor description parameters proc
-                   container-mappings container-namespaces title kind)))
-  (fields (description tool-description)
-          ;; Association list mapping parameter names to <tool-parameter>
-          ;; objects.
-          (parameters tool-parameters)
-          (proc tool-proc)
-          (container-mappings tool-container-mappings)
-          (container-namespaces tool-container-namespaces)
-          (title tool-title)
-          (kind tool-kind)))
-
-(define-public-record-type* (<tool-parameter> tool-parameter tool-parameter?)
-  (lambda (constructor)
-    (lambda* (#:key type description required?)
-      (unless type
-        (raise-exception
-         (condition (make-violation)
-                    (make-message-condition "tool parameter requires type"))))
-      (unless description
-        (raise-exception
-         (condition (make-violation)
-                    (make-message-condition "tool parameter requires description"))))
-      (constructor type description required?)))
-  (fields (type tool-parameter-type)
-          (description tool-parameter-description)
-          (required? tool-parameter-required?)))
-
-(define-public-record-type* (<array-type> array-type array-type?)
-  (fields (subtype array-type-subtype)))
-
-(define (tool->spec name tool)
-  "Serialize @var{tool} of @var{name} to OpenAI-compatible JSON spec."
-  `(("type" . "function")
-    ("function"
-     ("name" . ,name)
-     ("description" . ,(tool-description tool))
-     ("parameters"
-      ("type" . "object")
-      ("properties" .
-       ,(map (match-lambda
-               ((name . parameter)
-                ;; TODO: Check if the OpenAI API supports arrays of arrays and
-                ;; other more deeply nested types.
-                (let ((type (tool-parameter-type parameter)))
-                  `(,name
-                    ("description" . ,(tool-parameter-description parameter))
-                    ("type" . ,(if (array-type? type)
-                                   "array"
-                                   type))
-                    ,@(if (array-type? type)
-                          `(("items" . ,(array-type-subtype type)))
-                          '())))))
-             (tool-parameters tool)))
-      ("required" .
-       ,(list->vector
-         (filter-map (match-lambda
-                       ((name . parameter)
-                        (and (tool-parameter-required? parameter)
-                             name)))
-                     (tool-parameters tool))))))))
-
-(define-public-record-type* (<tool-call> tool-call tool-call?)
-  (fields (session-id tool-call-session-id)
-          (session-cwd tool-call-session-cwd)
-          (id tool-call-id)
-          (function tool-call-function)
-          (arguments tool-call-arguments)
-          ;; One of 'pending-approval, 'approved, 'rejected or 'cancelled
-          (status tool-call-status lensed)))
-
-(define-public-record-type* (<tool-call-result> tool-call-result tool-call-result?)
-  (fields (session-id tool-call-result-session-id)
-          (call-id tool-call-result-call-id)
-          (title tool-call-result-title)
-          (kind tool-call-result-kind)
-          (arguments tool-call-result-arguments)
-          (json tool-call-result-json)
-          (success? tool-call-result-success?)))
-
-(define-condition-type &tool-call-parse-failure &serious
-  tool-call-parse-failure tool-call-parse-failure?
-  (message tool-call-parse-failure-message))
-
-(define-condition-type &tool-call-failure &serious
-  tool-call-failure tool-call-failure?
-  (message tool-call-failure-message))
-
-(define (spec->tool-call session-id session-cwd tools allowed-tools rejected-tools spec)
-  "Deserialize JSON tool call @var{spec} into a @code{<tool-call>}
-object. Raise a @code{&tool-call-parse-failure} condition if deserialization
-fails.
-
-@var{session-id} and @var{session-cwd} are the ID and current working directory
-of the session the tool call pertains to. @var{tools} is an association list
-mapping the names of all available tools to their respective @code{<tool>}
-objects. @var{allowed-tools} and @var{rejected-tools} are the lists of tool
-names that have been respectively allowed and rejected by the user in advance."
-  ;; TODO: Assert that type is function, and do more sanitization.
-  (let* ((args (guard (c (else
-                          (raise-exception
-                           (tool-call-parse-failure
-                            "Error: Arguments are not valid JSON"))))
-                 (json-string->scm (focus (in "function" "arguments")
-                                          spec))))
-         (name (focus (in "function" "name")
-                      spec))
-         (tool (cond
-                ((focus (key-ref name) tools)
-                 => identity)
-                (else
-                 (raise-exception
-                  (tool-call-parse-failure
-                   (string-append "Error: Function " name " does not exist")))))))
-    (tool-call session-id
-               session-cwd
-               (focus (key-ref "id")
-                      spec)
-               name
-               ;; Only pick out valid arguments, and error out if any required
-               ;; arguments are missing.
-               (filter-map (match-lambda
-                             ((arg-name . parameter)
-                              (cond
-                               ((assoc arg-name args)
-                                => identity)
-                               (else
-                                (and (tool-parameter-required? parameter)
-                                     (raise-exception
-                                      (tool-call-parse-failure
-                                       (string-append "Error: Missing required argument "
-                                                      arg-name))))))))
-                           (tool-parameters tool))
-               ;; Set tool call status based on pre-approved and pre-rejected
-               ;; tools.
-               (cond
-                ((member name allowed-tools) 'approved)
-                ((member name rejected-tools) 'rejected)
-                (else 'pending-approval)))))
-
-(define (eval-tool-call tool-call tools)
-  "Evaluate @var{tool-call} and return a @code{<tool-call-result>}
-object. @var{tools} is an association list mapping the names of all
-available tools to their respective @code{<tool>} objects."
-  (let ((tool (focus (key-ref (tool-call-function tool-call))
-                     tools)))
-    (case (focus tool-call-status tool-call)
-      ;; User approved tool call.
-      ((approved)
-       (guard (c ((tool-call-failure? c)
-                  (tool-call-result (tool-call-session-id tool-call)
-                                    (tool-call-id tool-call)
-                                    #f
-                                    #f
-                                    (tool-call-arguments tool-call)
-                                    `(("role" . "tool")
-                                      ("tool_call_id" . ,(tool-call-id tool-call))
-                                      ("content" . ,(tool-call-failure-message c)))
-                                    #f)))
-         (let* ((args (alist->plist (tool-call-arguments tool-call)))
-                ;; Actually evaluate tool call.
-                (tool-result
-                 (call-with-container*
-                     (map file-system-mapping->bind-mount
-                          ((tool-container-mappings tool)
-                           (tool-call-session-cwd tool-call)
-                           args))
-                     (tool-container-namespaces tool)
-                   (lambda ()
-                     (chdir (tool-call-session-cwd tool-call))
-                     (apply (tool-proc tool)
-                            args)))))
-           ;; Build result.
-           (tool-call-result (tool-call-session-id tool-call)
-                             (tool-call-id tool-call)
-                             (apply (tool-title tool) args)
-                             (tool-kind tool)
-                             (tool-call-arguments tool-call)
-                             `(("role" . "tool")
-                               ("tool_call_id" . ,(tool-call-id tool-call))
-                               ("content" . ,(container-result-output tool-result)))
-                             (zero? (container-result-exit-value tool-result))))))
-      ;; User cancelled or rejected tool call.
-      ((cancelled rejected)
-       (tool-call-result (tool-call-session-id tool-call)
-                         (tool-call-id tool-call)
-                         #f
-                         #f
-                         (tool-call-arguments tool-call)
-                         `(("role" . "tool")
-                           ("tool_call_id" . ,(tool-call-id tool-call))
-                           ("content" .
-                            ,(case (focus tool-call-status tool-call)
-                               ((rejected)
-                                "Error: User denied permission for this tool call")
-                               ((cancelled)
-                                "Error: User cancelled this tool call"))))
-                         #f))
-      (else
-       (assertion-violation (focus tool-call-status tool-call)
-                            "Invalid tool call status")))))
diff --git a/kaakaa/tools/base.scm b/kaakaa/tools/base.scm
deleted file mode 100644
index cbbb6a5..0000000
--- a/kaakaa/tools/base.scm
+++ /dev/null
@@ -1,225 +0,0 @@
-;;; kaakaa --- Tiny, security-focused AI agent in Guile
-;;; Copyright © 2026 Arun Isaac <arunisaac@systemreboot.net>
-;;;
-;;; This file is part of kaakaa.
-;;;
-;;; kaakaa is free software: you can redistribute it and/or modify it
-;;; under the terms of the GNU General Public License as published by
-;;; the Free Software Foundation, either version 3 of the License, or
-;;; (at your option) any later version.
-;;;
-;;; kaakaa is distributed in the hope that it will be useful, but
-;;; WITHOUT ANY WARRANTY; without even the implied warranty of
-;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-;;; General Public License for more details.
-;;;
-;;; You should have received a copy of the GNU General Public License
-;;; along with kaakaa.  If not, see <https://www.gnu.org/licenses/>.
-
-(define-module (kaakaa tools base)
-  #:use-module (rnrs exceptions)
-  #:use-module (rnrs io ports)
-  #:use-module (srfi srfi-1)
-  #:use-module (srfi srfi-26)
-  #:use-module (srfi srfi-171)
-  #:use-module (ice-9 format)
-  #:use-module (ice-9 ftw)
-  #:use-module (ice-9 match)
-  #:use-module (ice-9 regex)
-  #:use-module (kaakaa tools)
-  #:export (%list-files
-            %base-tools))
-
-(define (binary-file? file)
-  "Return @code{#t} if @var{file} is a binary file. Else, return @code{#f}."
-  ;; We use the following heuristic: If there are character decoding errors in
-  ;; the first 10K characters, we assume that this is a binary file.
-  (guard (c ((or (i/o-decoding-error? c)
-                 (eq? (exception-kind c)
-                      'decoding-error))
-             #t))
-    (call-with-input-file file
-      (lambda (port)
-        (set-port-conversion-strategy! port 'error)
-        (get-string-n port (* 10 1024))))
-    #f))
-
-(define (make-regexp* pattern)
-  "Like @code{make-regexp}, but report an error and abort if @var{pattern} is not a
-valid regular expression."
-  (guard (c ((eq? (exception-kind c)
-                  'regular-expression-syntax)
-             (format (current-output-port)
-                     "Error: Invalid regular expression: ~s~%"
-                     pattern)
-             (exit #f)))
-    (make-regexp pattern)))
-
-(define (files-recursively directory pattern)
-  "Return a list of all files recursively down @var{directory} whose basename
-matches regular expression @var{pattern}. Hidden directories are not traversed."
-  (cond
-   ((not (file-exists? directory))
-    (format (current-output-port)
-            "Error: Directory ~a does not exist~%"
-            directory)
-    (exit #f))
-   ((not (eq? (stat:type (stat directory))
-              'directory))
-    (format (current-output-port)
-            "Error: ~a is not a directory~%"
-            directory)
-    (exit #f))
-   (else
-    (let ((pattern-rx (make-regexp* pattern)))
-      (file-system-fold (lambda (path stat result)
-                          (not (string-prefix? "." (basename path))))
-                        (lambda (path stat result)
-                          (if (regexp-exec pattern-rx (basename path))
-                              (cons path result)
-                              result))
-                        (lambda (path stat result)
-                          result)
-                        (lambda (path stat result)
-                          result)
-                        (lambda (path stat result)
-                          result)
-                        (lambda (path stat errno result)
-                          (format (current-output-port)
-                                  "Error: ~a: ~a~%"
-                                  path
-                                  (strerror errno))
-                          result)
-                        (list)
-                        (canonicalize-path directory))))))
-
-(define %read
-  (tool #:description "Read whole text file, or optionally a subset of its lines.
-
-Line numbers start from 1. Output is the raw file contents without line numbers."
-        #:parameters `(("path" . ,(tool-parameter
-                                   #:type "string"
-                                   #:description "File path to read"
-                                   #:required? #t))
-                       ("start-line" . ,(tool-parameter
-                                         #:type "integer"
-                                         #:description "Read file starting from this line number (inclusive). Default is 1."))
-                       ("end-line" . ,(tool-parameter
-                                       #:type "integer"
-                                       #:description "Read up to this line number (inclusive). Default is the last line of the file.")))
-        #:proc (lambda* (#:key path (start-line 1) end-line)
-                 (cond
-                  ((not (file-exists? path))
-                   (format (current-output-port)
-                           "Error: File ~a does not exist~%"
-                           path)
-                   (exit #f))
-                  ((binary-file? path)
-                   (format (current-output-port)
-                           "Error: File ~a is binary, not text~%"
-                           path)
-                   (exit #f))
-                  (else
-                   (call-with-input-file path
-                     (cut port-transduce
-                          (compose (tdrop (1- start-line))
-                                   (if end-line
-                                       (ttake (- end-line (1- start-line)))
-                                       (tmap identity))
-                                   (tlog (lambda (result input)
-                                           (display input)
-                                           (newline))))
-                          (const #t)
-                          get-line
-                          <>)))))
-        #:title (lambda* (#:key path (start-line 1) end-line)
-                  (format #f "read ~a:~a-~a"
-                          path start-line (or end-line "")))
-        #:kind "read"))
-
-(define %list
-  (tool #:description "List files recursively.
-
-The output is in three columns—the file type, the file size and the file path."
-        #:parameters `(("root" . ,(tool-parameter
-                                   #:type "string"
-                                   #:description "Root path to list from"
-                                   #:required? #t))
-                       ("pattern" . ,(tool-parameter
-                                      #:type "string"
-                                      #:description
-                                      "POSIX extended regular expression to match basename (including extension) of
-file against. Default matches all files.
-
-For example, to match all scheme (.scm) files, use \"\\.scm$\"")))
-        #:proc (lambda* (#:key root (pattern "."))
-                 (for-each (lambda (path)
-                             (let ((st (stat path)))
-                               (format (current-output-port)
-                                       "~a~/~a~/~a~%"
-                                       (stat:type st)
-                                       (stat:size st)
-                                       path)))
-                           (files-recursively root pattern)))
-        #:title (lambda* (#:key root pattern)
-                  (if pattern
-                      (format #f "list ~s in ~a" pattern root)
-                      (format #f "list ~a" root)))
-        #:kind "search"))
-
-(define %search
-  (tool #:description "Print lines that match a pattern (similar to grep)
-
-Similar to grep, the output is three colon separated columns—the file path, the
-line number and the matching line. Line numbers start from 1."
-        #:parameters `(("pattern" . ,(tool-parameter
-                                      #:type "string"
-                                      #:description "POSIX extended regular expression to search for"
-                                      #:required? #t))
-                       ("files-root" . ,(tool-parameter
-                                         #:type "string"
-                                         #:description "Root path to start search from"
-                                         #:required? #t))
-                       ("files-pattern" . ,(tool-parameter
-                                            #:type "string"
-                                            #:description
-                                            "POSIX extended regular expression to match basename (including extension) of
-file against. Default matches all files.
-
-For example, to match all scheme (.scm) files, use \"\\.scm$\"")))
-        #:proc (lambda* (#:key pattern files-root (files-pattern "."))
-                 (let* ((pattern-rx (make-regexp* pattern))
-                        (line-matcher (match-lambda
-                                        ((_ . line)
-                                         (regexp-exec pattern-rx line))))
-                        (make-line-logger (lambda (file)
-                                            (match-lambda*
-                                              ((result (line-number . line))
-                                               (format (current-output-port)
-                                                       "~a:~a:~a~%"
-                                                       file line-number line))))))
-                   (for-each (lambda (file)
-                               (call-with-input-file file
-                                 (cut port-transduce
-                                      (compose (tenumerate 1)
-                                               (tfilter line-matcher)
-                                               (tlog (make-line-logger file)))
-                                      (const #t)
-                                      get-line
-                                      <>)))
-                             (remove binary-file?
-                                     (files-recursively files-root files-pattern)))))
-        #:title (lambda* (#:key pattern files-root files-pattern)
-                  (if files-pattern
-                      (format #f "shortlist files matching ~s in ~a, then search for lines matching ~s"
-                              files-pattern files-root pattern)
-                      (format #f "search for lines matching ~s in files under ~a"
-                              pattern files-root)))
-        #:kind "search"))
-
-(define %base-tools
-  `(("read" . ,%read)
-    ("list" . ,%list)
-    ("search" . ,%search)))
-
-;; TODO: Implement write.
diff --git a/kaakaa/utils.scm b/kaakaa/utils.scm
deleted file mode 100644
index a922ef2..0000000
--- a/kaakaa/utils.scm
+++ /dev/null
@@ -1,89 +0,0 @@
-;;; kaakaa --- Tiny, security-focused AI agent in Guile
-;;; Copyright © 2026 Arun Isaac <arunisaac@systemreboot.net>
-;;;
-;;; This file is part of kaakaa.
-;;;
-;;; kaakaa is free software: you can redistribute it and/or modify it
-;;; under the terms of the GNU General Public License as published by
-;;; the Free Software Foundation, either version 3 of the License, or
-;;; (at your option) any later version.
-;;;
-;;; kaakaa is distributed in the hope that it will be useful, but
-;;; WITHOUT ANY WARRANTY; without even the implied warranty of
-;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-;;; General Public License for more details.
-;;;
-;;; You should have received a copy of the GNU General Public License
-;;; along with kaakaa.  If not, see <https://www.gnu.org/licenses/>.
-
-(define-module (kaakaa utils)
-  #:use-module (srfi srfi-1)
-  #:use-module (srfi srfi-26)
-  #:use-module (ice-9 match)
-  #:use-module (ice-9 popen)
-  #:export (->
-            foldn
-            alist->plist
-            call-with-input-pipe))
-
-(define (->-helper x . procs)
-  "Thread @var{x} through @var{procs}."
-  (match procs
-    (() x)
-    ((head tail ...)
-     (apply ->-helper (head x) tail))))
-
-(define-syntax-rule (-> x (proc ...) ...)
-  "Thread @var{x} through @var{procs}.
-
-For example:
-(-> 1
-    (1+ <>)
-    (* 2 <>)
-    (expt <> 2))
-=> 16"
-  (->-helper x (cut proc ...) ...))
-
-(define (foldn proc lst . inits)
-  "Apply @var{proc} to the elements of @var{lst} to build a result, and return
-that result. @var{proc} may return multiple values, in which case, an equal
-number of values are returned. Each @var{proc} call is @code{(proc element
-previous ...)} where @code{element} is an element of @var{lst}, and
-@code{(previous ...)} is the return from the previous call to @var{proc} or the
-given @var{inits} for the first call. For example,
-
-(foldn (lambda (n sum sum-of-squares)
-         (values (+ sum n)
-                 (+ sum-of-squares (expt n 2))))
-       (iota 10)
-       0 0)
-=> 45
-=> 285"
-  (apply values
-         (fold (lambda (element results)
-                 (call-with-values (cut apply proc element results) list))
-               inits
-               lst)))
-
-(define (alist->plist alist)
-  "Convert association list @var{alist} to a property list. Keys in
-@var{alist} are converted to keywords."
-  (append-map (match-lambda
-                ((key . value)
-                 (list (symbol->keyword (string->symbol key))
-                       value)))
-              alist))
-
-(define (call-with-input-pipe command proc)
-  "Call @var{proc} with input pipe to @var{command}. @var{command} is a
-list of program arguments."
-  (match command
-    ((prog args ...)
-     (let ((port #f))
-       (dynamic-wind
-         (lambda ()
-           (set! port (apply open-pipe* OPEN_READ prog args)))
-         (cut proc port)
-         (lambda ()
-           (unless (zero? (close-pipe port))
-             (error "Command invocation failed" command))))))))