handoff: symbolic identity file — TDD
Agent identity loaded from ~/memex/IDENTITY.org at skill startup. Injected into system prompt IDENTITY section between assistant name and reflection feedback. fboundp-guarded in think(). - symbolic-identity.lisp: load-identity-file, agent-identity (skill) - token-economics: prompt-prefix-cached +identity-content param - core-reason: identity-content binding in think(), both code paths - Identity: 6/6 Token-econ: 10/10 new Core: 65/65 TUI View: 28/28 TUI Main: 70/70 Total: 179/179
This commit is contained in:
@@ -95,22 +95,26 @@
|
|||||||
(reflection-feedback (if rejection-trace
|
(reflection-feedback (if rejection-trace
|
||||||
(format nil "~%~%PREVIOUS PROPOSAL REJECTED: ~a" rejection-trace)
|
(format nil "~%~%PREVIOUS PROPOSAL REJECTED: ~a" rejection-trace)
|
||||||
""))
|
""))
|
||||||
(standing-mandates-text (let ((out ""))
|
(standing-mandates-text (let ((out ""))
|
||||||
(dolist (fn *standing-mandates*)
|
(dolist (fn *standing-mandates*)
|
||||||
(let ((text (ignore-errors (funcall fn context))))
|
(let ((text (ignore-errors (funcall fn context))))
|
||||||
(when (and text (stringp text) (> (length text) 0))
|
(when (and text (stringp text) (> (length text) 0))
|
||||||
(setf out (concatenate 'string out text (string #\Newline))))))
|
(setf out (concatenate 'string out text (string #\Newline))))))
|
||||||
(when (> (length out) 0) out)))
|
(when (> (length out) 0) out)))
|
||||||
(time-section (if (fboundp 'sensor-time-duration) ; v0.6.0: temporal awareness
|
(identity-content (if (fboundp 'agent-identity) ; v0.7.2: symbolic identity
|
||||||
|
(agent-identity)
|
||||||
|
""))
|
||||||
|
(time-section (if (fboundp 'sensor-time-duration) ; v0.6.0: temporal awareness
|
||||||
(format-time-for-llm
|
(format-time-for-llm
|
||||||
:session-duration-seconds (funcall (symbol-function 'session-duration)))
|
:session-duration-seconds (funcall (symbol-function 'session-duration)))
|
||||||
(if (fboundp 'format-time-for-llm)
|
(if (fboundp 'format-time-for-llm)
|
||||||
(format-time-for-llm)
|
(format-time-for-llm)
|
||||||
"")))
|
"")))
|
||||||
(system-prompt (if (fboundp 'prompt-prefix-cached)
|
(system-prompt (if (fboundp 'prompt-prefix-cached)
|
||||||
;; v0.5.0: cached prefix with optional budget enforcement
|
;; v0.5.0: cached prefix with optional budget enforcement
|
||||||
(let* ((prefix (prompt-prefix-cached assistant-name reflection-feedback
|
(let* ((prefix (prompt-prefix-cached assistant-name identity-content
|
||||||
standing-mandates-text tool-belt)))
|
reflection-feedback
|
||||||
|
standing-mandates-text tool-belt)))
|
||||||
(if (fboundp 'enforce-token-budget)
|
(if (fboundp 'enforce-token-budget)
|
||||||
(multiple-value-bind (pfx ctxt logs _ mandates)
|
(multiple-value-bind (pfx ctxt logs _ mandates)
|
||||||
(enforce-token-budget prefix global-context system-logs
|
(enforce-token-budget prefix global-context system-logs
|
||||||
@@ -122,13 +126,13 @@
|
|||||||
(format nil "~a~%~%~a~%~%CONTEXT:~%~a~%~%LOGS:~%~a"
|
(format nil "~a~%~%~a~%~%CONTEXT:~%~a~%~%LOGS:~%~a"
|
||||||
time-section prefix (or global-context "") system-logs)))
|
time-section prefix (or global-context "") system-logs)))
|
||||||
;; Fallback when token-economics not loaded
|
;; Fallback when token-economics not loaded
|
||||||
(format nil "~a~%~%IDENTITY: ~a~a~a~%~%TOOLS:~%~a~%~%CONTEXT:~%~a~%~%LOGS:~%~a"
|
(format nil "~a~%~%IDENTITY: ~a~a~a~a~%~%TOOLS:~%~a~%~%CONTEXT:~%~a~%~%LOGS:~%~a"
|
||||||
time-section
|
time-section
|
||||||
assistant-name reflection-feedback
|
assistant-name identity-content reflection-feedback
|
||||||
(if standing-mandates-text
|
(if standing-mandates-text
|
||||||
(concatenate 'string (string #\Newline) standing-mandates-text)
|
(concatenate 'string (string #\Newline) standing-mandates-text)
|
||||||
"")
|
"")
|
||||||
tool-belt (or global-context "") system-logs))))
|
tool-belt (or global-context "") system-logs))))
|
||||||
(let* ((thought (if (and reply-stream (fboundp 'cascade-stream)) ; v0.7.1: streaming
|
(let* ((thought (if (and reply-stream (fboundp 'cascade-stream)) ; v0.7.1: streaming
|
||||||
(let ((acc (make-string-output-stream)))
|
(let ((acc (make-string-output-stream)))
|
||||||
(funcall 'cascade-stream raw-prompt system-prompt
|
(funcall 'cascade-stream raw-prompt system-prompt
|
||||||
|
|||||||
92
lisp/symbolic-identity.lisp
Normal file
92
lisp/symbolic-identity.lisp
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
(in-package :passepartout)
|
||||||
|
|
||||||
|
(defvar *agent-identity* ""
|
||||||
|
"Identity text loaded from ~/memex/IDENTITY.org at startup.
|
||||||
|
|
||||||
|
This variable holds the contents of the user's identity file.
|
||||||
|
Loaded by `load-identity-file` at daemon/skill initialization,
|
||||||
|
called from `agent-identity` for system prompt injection.
|
||||||
|
|
||||||
|
The file is user-editable and persists across restarts.
|
||||||
|
If the file is missing or empty, this variable remains \"\".")
|
||||||
|
|
||||||
|
(defun load-identity-file (&optional (path nil path-p))
|
||||||
|
"Load agent identity from an org file.
|
||||||
|
|
||||||
|
Reads the identity text file and caches it in
|
||||||
|
`*agent-identity*`. If PATH is not provided, defaults to
|
||||||
|
`~/memex/IDENTITY.org`.
|
||||||
|
|
||||||
|
Returns the file content string on success, or NIL if the file
|
||||||
|
does not exist or cannot be read."
|
||||||
|
(let* ((file-path (if path-p
|
||||||
|
(uiop:ensure-pathname path :ensure-absolute t)
|
||||||
|
(merge-pathnames "memex/IDENTITY.org"
|
||||||
|
(user-homedir-pathname)))))
|
||||||
|
(when (uiop:file-exists-p file-path)
|
||||||
|
(handler-case
|
||||||
|
(let ((content (uiop:read-file-string file-path)))
|
||||||
|
(setf *agent-identity* content)
|
||||||
|
content)
|
||||||
|
(error () nil)))))
|
||||||
|
|
||||||
|
(defun agent-identity ()
|
||||||
|
"Return the currently loaded agent identity string."
|
||||||
|
(or *agent-identity* ""))
|
||||||
|
|
||||||
|
;; Auto-load identity at skill init
|
||||||
|
(load-identity-file)
|
||||||
|
|
||||||
|
(defpackage :passepartout-identity-tests
|
||||||
|
(:use :common-lisp :fiveam :passepartout)
|
||||||
|
(:export :identity-suite))
|
||||||
|
|
||||||
|
(in-package :passepartout-identity-tests)
|
||||||
|
|
||||||
|
(def-suite identity-suite
|
||||||
|
:description "Agent identity loading and caching")
|
||||||
|
(in-suite identity-suite)
|
||||||
|
|
||||||
|
(test test-load-identity-file-returns-content
|
||||||
|
"Contract 1: load-identity-file reads an existing file, returns content."
|
||||||
|
(let* ((path "/tmp/memex-test-identity.org")
|
||||||
|
(content "### Personality
|
||||||
|
- Friendly
|
||||||
|
- Concise"))
|
||||||
|
(with-open-file (f path :direction :output :if-exists :supersede)
|
||||||
|
(write-string content f))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((result (passepartout::load-identity-file path)))
|
||||||
|
(is (stringp result))
|
||||||
|
(is (search "Friendly" result))
|
||||||
|
(is (search "Concise" result)))
|
||||||
|
(ignore-errors (delete-file path)))))
|
||||||
|
|
||||||
|
(test test-load-identity-file-missing-nil
|
||||||
|
"Contract 1: nil when file does not exist."
|
||||||
|
(let ((result (passepartout::load-identity-file
|
||||||
|
"/tmp/memex-nonexistent-xxxx.org")))
|
||||||
|
(is (null result))))
|
||||||
|
|
||||||
|
(test test-agent-identity-cached
|
||||||
|
"Contract 2+3: agent-identity returns cached value after load."
|
||||||
|
(let* ((path "/tmp/memex-test-identity2.org")
|
||||||
|
(content "### Preferences
|
||||||
|
- Use shell cautiously"))
|
||||||
|
(with-open-file (f path :direction :output :if-exists :supersede)
|
||||||
|
(write-string content f))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(passepartout::load-identity-file path)
|
||||||
|
(let ((id (passepartout::agent-identity)))
|
||||||
|
(is (search "shell cautiously" id))))
|
||||||
|
(ignore-errors (delete-file path)))))
|
||||||
|
|
||||||
|
(test test-agent-identity-empty-default
|
||||||
|
"Contract 2: returns empty string when nothing was loaded."
|
||||||
|
(let ((prev passepartout::*agent-identity*))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(setf passepartout::*agent-identity* nil)
|
||||||
|
(is (string= "" (passepartout::agent-identity))))
|
||||||
|
(setf passepartout::*agent-identity* prev))))
|
||||||
@@ -6,16 +6,16 @@
|
|||||||
(defvar *context-cache* (list :foveal-id nil :scope nil :memory-timestamp 0 :rendered "")
|
(defvar *context-cache* (list :foveal-id nil :scope nil :memory-timestamp 0 :rendered "")
|
||||||
"Context assembly cache: metadata + last rendered context string.")
|
"Context assembly cache: metadata + last rendered context string.")
|
||||||
|
|
||||||
(defun prompt-prefix-cached (assistant-name feedback mandates-text tool-belt)
|
(defun prompt-prefix-cached (assistant-name identity-content feedback mandates-text tool-belt)
|
||||||
"Build the static IDENTITY+TOOLS system prompt prefix.
|
"Build the static IDENTITY+TOOLS system prompt prefix.
|
||||||
Uses sxhash on inputs to detect changes; returns cached string on cache hit."
|
Uses sxhash on inputs to detect changes; returns cached string on cache hit."
|
||||||
(let* ((hash-key (sxhash (list assistant-name feedback mandates-text tool-belt)))
|
(let* ((hash-key (sxhash (list assistant-name identity-content feedback mandates-text tool-belt)))
|
||||||
(cached-hash (car *prompt-prefix-cache*))
|
(cached-hash (car *prompt-prefix-cache*))
|
||||||
(cached-str (cdr *prompt-prefix-cache*)))
|
(cached-str (cdr *prompt-prefix-cache*)))
|
||||||
(if (and cached-str (> (length cached-str) 0) (= hash-key cached-hash))
|
(if (and cached-str (> (length cached-str) 0) (= hash-key cached-hash))
|
||||||
cached-str
|
cached-str
|
||||||
(let ((new-prefix (format nil "IDENTITY: ~a~a~a~%~%TOOLS:~%~a"
|
(let ((new-prefix (format nil "IDENTITY: ~a~a~a~a~%~%TOOLS:~%~a"
|
||||||
assistant-name feedback
|
assistant-name identity-content feedback
|
||||||
(if (and mandates-text (> (length mandates-text) 0))
|
(if (and mandates-text (> (length mandates-text) 0))
|
||||||
(concatenate 'string (string #\Newline) mandates-text)
|
(concatenate 'string (string #\Newline) mandates-text)
|
||||||
"")
|
"")
|
||||||
@@ -115,11 +115,22 @@ with trimmed sections."
|
|||||||
:description "Prompt prefix caching, incremental context, token budget")
|
:description "Prompt prefix caching, incremental context, token budget")
|
||||||
(in-suite token-economics-suite)
|
(in-suite token-economics-suite)
|
||||||
|
|
||||||
|
(test test-prompt-prefix-cached-identity
|
||||||
|
"Contract 1: prompt-prefix-cached includes identity-content when provided."
|
||||||
|
(setf (car passepartout::*prompt-prefix-cache*) nil
|
||||||
|
(cdr passepartout::*prompt-prefix-cache*) "")
|
||||||
|
(let ((prefix (passepartout::prompt-prefix-cached
|
||||||
|
"Agent" "### Mode: concise" "" nil "No tools")))
|
||||||
|
(is (stringp prefix))
|
||||||
|
(is (search "IDENTITY" prefix))
|
||||||
|
(is (search "Mode: concise" prefix))
|
||||||
|
(is (search "TOOLS" prefix))))
|
||||||
|
|
||||||
(test test-prompt-prefix-cached-builds
|
(test test-prompt-prefix-cached-builds
|
||||||
"Contract 1: prompt-prefix-cached returns a string containing IDENTITY."
|
"Contract 1: prompt-prefix-cached returns a string containing IDENTITY."
|
||||||
(setf (car passepartout::*prompt-prefix-cache*) nil
|
(setf (car passepartout::*prompt-prefix-cache*) nil
|
||||||
(cdr passepartout::*prompt-prefix-cache*) "")
|
(cdr passepartout::*prompt-prefix-cache*) "")
|
||||||
(let ((prefix (passepartout::prompt-prefix-cached "Agent" "" nil "No tools")))
|
(let ((prefix (passepartout::prompt-prefix-cached "Agent" "" "" nil "No tools")))
|
||||||
(is (stringp prefix))
|
(is (stringp prefix))
|
||||||
(is (search "IDENTITY" prefix))
|
(is (search "IDENTITY" prefix))
|
||||||
(is (search "TOOLS" prefix))))
|
(is (search "TOOLS" prefix))))
|
||||||
@@ -128,16 +139,16 @@ with trimmed sections."
|
|||||||
"Contract 1: second call with same inputs returns cached result."
|
"Contract 1: second call with same inputs returns cached result."
|
||||||
(setf (car passepartout::*prompt-prefix-cache*) nil
|
(setf (car passepartout::*prompt-prefix-cache*) nil
|
||||||
(cdr passepartout::*prompt-prefix-cache*) "")
|
(cdr passepartout::*prompt-prefix-cache*) "")
|
||||||
(let ((p1 (passepartout::prompt-prefix-cached "Agent" "" nil "No tools"))
|
(let ((p1 (passepartout::prompt-prefix-cached "Agent" "" "" nil "No tools"))
|
||||||
(p2 (passepartout::prompt-prefix-cached "Agent" "" nil "No tools")))
|
(p2 (passepartout::prompt-prefix-cached "Agent" "" "" nil "No tools")))
|
||||||
(is (string= p1 p2))))
|
(is (string= p1 p2))))
|
||||||
|
|
||||||
(test test-prompt-prefix-cached-miss
|
(test test-prompt-prefix-cached-miss
|
||||||
"Contract 1: different inputs rebuild the cache."
|
"Contract 1: different inputs rebuild the cache."
|
||||||
(setf (car passepartout::*prompt-prefix-cache*) nil
|
(setf (car passepartout::*prompt-prefix-cache*) nil
|
||||||
(cdr passepartout::*prompt-prefix-cache*) "")
|
(cdr passepartout::*prompt-prefix-cache*) "")
|
||||||
(let ((p1 (passepartout::prompt-prefix-cached "Agent" "" nil "No tools"))
|
(let ((p1 (passepartout::prompt-prefix-cached "Agent" "" "" nil "No tools"))
|
||||||
(p2 (passepartout::prompt-prefix-cached "Bot" "" nil "No tools")))
|
(p2 (passepartout::prompt-prefix-cached "Bot" "" "" nil "No tools")))
|
||||||
(is (not (string= p1 p2)))
|
(is (not (string= p1 p2)))
|
||||||
(is (search "Bot" p2))))
|
(is (search "Bot" p2))))
|
||||||
|
|
||||||
|
|||||||
@@ -250,22 +250,26 @@ each cascade call via ~cost-track-backend-call~. All four calls are
|
|||||||
(reflection-feedback (if rejection-trace
|
(reflection-feedback (if rejection-trace
|
||||||
(format nil "~%~%PREVIOUS PROPOSAL REJECTED: ~a" rejection-trace)
|
(format nil "~%~%PREVIOUS PROPOSAL REJECTED: ~a" rejection-trace)
|
||||||
""))
|
""))
|
||||||
(standing-mandates-text (let ((out ""))
|
(standing-mandates-text (let ((out ""))
|
||||||
(dolist (fn *standing-mandates*)
|
(dolist (fn *standing-mandates*)
|
||||||
(let ((text (ignore-errors (funcall fn context))))
|
(let ((text (ignore-errors (funcall fn context))))
|
||||||
(when (and text (stringp text) (> (length text) 0))
|
(when (and text (stringp text) (> (length text) 0))
|
||||||
(setf out (concatenate 'string out text (string #\Newline))))))
|
(setf out (concatenate 'string out text (string #\Newline))))))
|
||||||
(when (> (length out) 0) out)))
|
(when (> (length out) 0) out)))
|
||||||
(time-section (if (fboundp 'sensor-time-duration) ; v0.6.0: temporal awareness
|
(identity-content (if (fboundp 'agent-identity) ; v0.7.2: symbolic identity
|
||||||
|
(agent-identity)
|
||||||
|
""))
|
||||||
|
(time-section (if (fboundp 'sensor-time-duration) ; v0.6.0: temporal awareness
|
||||||
(format-time-for-llm
|
(format-time-for-llm
|
||||||
:session-duration-seconds (funcall (symbol-function 'session-duration)))
|
:session-duration-seconds (funcall (symbol-function 'session-duration)))
|
||||||
(if (fboundp 'format-time-for-llm)
|
(if (fboundp 'format-time-for-llm)
|
||||||
(format-time-for-llm)
|
(format-time-for-llm)
|
||||||
"")))
|
"")))
|
||||||
(system-prompt (if (fboundp 'prompt-prefix-cached)
|
(system-prompt (if (fboundp 'prompt-prefix-cached)
|
||||||
;; v0.5.0: cached prefix with optional budget enforcement
|
;; v0.5.0: cached prefix with optional budget enforcement
|
||||||
(let* ((prefix (prompt-prefix-cached assistant-name reflection-feedback
|
(let* ((prefix (prompt-prefix-cached assistant-name identity-content
|
||||||
standing-mandates-text tool-belt)))
|
reflection-feedback
|
||||||
|
standing-mandates-text tool-belt)))
|
||||||
(if (fboundp 'enforce-token-budget)
|
(if (fboundp 'enforce-token-budget)
|
||||||
(multiple-value-bind (pfx ctxt logs _ mandates)
|
(multiple-value-bind (pfx ctxt logs _ mandates)
|
||||||
(enforce-token-budget prefix global-context system-logs
|
(enforce-token-budget prefix global-context system-logs
|
||||||
@@ -277,13 +281,13 @@ each cascade call via ~cost-track-backend-call~. All four calls are
|
|||||||
(format nil "~a~%~%~a~%~%CONTEXT:~%~a~%~%LOGS:~%~a"
|
(format nil "~a~%~%~a~%~%CONTEXT:~%~a~%~%LOGS:~%~a"
|
||||||
time-section prefix (or global-context "") system-logs)))
|
time-section prefix (or global-context "") system-logs)))
|
||||||
;; Fallback when token-economics not loaded
|
;; Fallback when token-economics not loaded
|
||||||
(format nil "~a~%~%IDENTITY: ~a~a~a~%~%TOOLS:~%~a~%~%CONTEXT:~%~a~%~%LOGS:~%~a"
|
(format nil "~a~%~%IDENTITY: ~a~a~a~a~%~%TOOLS:~%~a~%~%CONTEXT:~%~a~%~%LOGS:~%~a"
|
||||||
time-section
|
time-section
|
||||||
assistant-name reflection-feedback
|
assistant-name identity-content reflection-feedback
|
||||||
(if standing-mandates-text
|
(if standing-mandates-text
|
||||||
(concatenate 'string (string #\Newline) standing-mandates-text)
|
(concatenate 'string (string #\Newline) standing-mandates-text)
|
||||||
"")
|
"")
|
||||||
tool-belt (or global-context "") system-logs))))
|
tool-belt (or global-context "") system-logs))))
|
||||||
(let* ((thought (if (and reply-stream (fboundp 'cascade-stream)) ; v0.7.1: streaming
|
(let* ((thought (if (and reply-stream (fboundp 'cascade-stream)) ; v0.7.1: streaming
|
||||||
(let ((acc (make-string-output-stream)))
|
(let ((acc (make-string-output-stream)))
|
||||||
(funcall 'cascade-stream raw-prompt system-prompt
|
(funcall 'cascade-stream raw-prompt system-prompt
|
||||||
|
|||||||
126
org/symbolic-identity.org
Normal file
126
org/symbolic-identity.org
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
#+TITLE: Symbolic Identity — Agent Self-Concept
|
||||||
|
#+FILETAGS: :skill:identity:
|
||||||
|
#+PROPERTY: header-args:lisp :tangle ../lisp/symbolic-identity.lisp
|
||||||
|
|
||||||
|
* Overview
|
||||||
|
Load `~/memex/IDENTITY.org` into the agent's self-concept at daemon
|
||||||
|
startup. The identity text is injected into the system prompt's
|
||||||
|
`IDENTITY` section, between assistant name and reflection feedback.
|
||||||
|
|
||||||
|
The file is user-editable and survives restarts. If the file is
|
||||||
|
missing or empty, identity is silently `""` (no-op).
|
||||||
|
|
||||||
|
* Contract
|
||||||
|
|
||||||
|
1. `(load-identity-file &optional path)`:
|
||||||
|
Reads IDENTITY.org from `path` (default `~/memex/IDENTITY.org`).
|
||||||
|
Sets `*agent-identity*` to the file content string.
|
||||||
|
Returns the content string, or NIL if file missing/unreadable.
|
||||||
|
2. `(agent-identity)`:
|
||||||
|
Returns the cached identity string (`*agent-identity*`), or `""` if
|
||||||
|
identity has not been loaded.
|
||||||
|
3. `*agent-identity*`:
|
||||||
|
Special variable holding the loaded identity text (string).
|
||||||
|
|
||||||
|
#+begin_src lisp
|
||||||
|
(in-package :passepartout)
|
||||||
|
|
||||||
|
(defvar *agent-identity* ""
|
||||||
|
"Identity text loaded from ~/memex/IDENTITY.org at startup.
|
||||||
|
|
||||||
|
This variable holds the contents of the user's identity file.
|
||||||
|
Loaded by `load-identity-file` at daemon/skill initialization,
|
||||||
|
called from `agent-identity` for system prompt injection.
|
||||||
|
|
||||||
|
The file is user-editable and persists across restarts.
|
||||||
|
If the file is missing or empty, this variable remains \"\".")
|
||||||
|
|
||||||
|
(defun load-identity-file (&optional (path nil path-p))
|
||||||
|
"Load agent identity from an org file.
|
||||||
|
|
||||||
|
Reads the identity text file and caches it in
|
||||||
|
`*agent-identity*`. If PATH is not provided, defaults to
|
||||||
|
`~/memex/IDENTITY.org`.
|
||||||
|
|
||||||
|
Returns the file content string on success, or NIL if the file
|
||||||
|
does not exist or cannot be read."
|
||||||
|
(let* ((file-path (if path-p
|
||||||
|
(uiop:ensure-pathname path :ensure-absolute t)
|
||||||
|
(merge-pathnames "memex/IDENTITY.org"
|
||||||
|
(user-homedir-pathname)))))
|
||||||
|
(when (uiop:file-exists-p file-path)
|
||||||
|
(handler-case
|
||||||
|
(let ((content (uiop:read-file-string file-path)))
|
||||||
|
(setf *agent-identity* content)
|
||||||
|
content)
|
||||||
|
(error () nil)))))
|
||||||
|
|
||||||
|
(defun agent-identity ()
|
||||||
|
"Return the currently loaded agent identity string."
|
||||||
|
(or *agent-identity* ""))
|
||||||
|
|
||||||
|
;; Auto-load identity at skill init
|
||||||
|
(load-identity-file)
|
||||||
|
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
* Test Squad
|
||||||
|
** Test Package
|
||||||
|
#+begin_src lisp
|
||||||
|
(defpackage :passepartout-identity-tests
|
||||||
|
(:use :common-lisp :fiveam :passepartout)
|
||||||
|
(:export :identity-suite))
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
** Test Suite
|
||||||
|
#+begin_src lisp
|
||||||
|
(in-package :passepartout-identity-tests)
|
||||||
|
|
||||||
|
(def-suite identity-suite
|
||||||
|
:description "Agent identity loading and caching")
|
||||||
|
(in-suite identity-suite)
|
||||||
|
|
||||||
|
(test test-load-identity-file-returns-content
|
||||||
|
"Contract 1: load-identity-file reads an existing file, returns content."
|
||||||
|
(let* ((path "/tmp/memex-test-identity.org")
|
||||||
|
(content "### Personality
|
||||||
|
- Friendly
|
||||||
|
- Concise"))
|
||||||
|
(with-open-file (f path :direction :output :if-exists :supersede)
|
||||||
|
(write-string content f))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((result (passepartout::load-identity-file path)))
|
||||||
|
(is (stringp result))
|
||||||
|
(is (search "Friendly" result))
|
||||||
|
(is (search "Concise" result)))
|
||||||
|
(ignore-errors (delete-file path)))))
|
||||||
|
|
||||||
|
(test test-load-identity-file-missing-nil
|
||||||
|
"Contract 1: nil when file does not exist."
|
||||||
|
(let ((result (passepartout::load-identity-file
|
||||||
|
"/tmp/memex-nonexistent-xxxx.org")))
|
||||||
|
(is (null result))))
|
||||||
|
|
||||||
|
(test test-agent-identity-cached
|
||||||
|
"Contract 2+3: agent-identity returns cached value after load."
|
||||||
|
(let* ((path "/tmp/memex-test-identity2.org")
|
||||||
|
(content "### Preferences
|
||||||
|
- Use shell cautiously"))
|
||||||
|
(with-open-file (f path :direction :output :if-exists :supersede)
|
||||||
|
(write-string content f))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(passepartout::load-identity-file path)
|
||||||
|
(let ((id (passepartout::agent-identity)))
|
||||||
|
(is (search "shell cautiously" id))))
|
||||||
|
(ignore-errors (delete-file path)))))
|
||||||
|
|
||||||
|
(test test-agent-identity-empty-default
|
||||||
|
"Contract 2: returns empty string when nothing was loaded."
|
||||||
|
(let ((prev passepartout::*agent-identity*))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(setf passepartout::*agent-identity* nil)
|
||||||
|
(is (string= "" (passepartout::agent-identity))))
|
||||||
|
(setf passepartout::*agent-identity* prev))))
|
||||||
|
#+end_src
|
||||||
@@ -29,7 +29,7 @@ Depends on: tokenizer.lisp, cost-tracker.lisp
|
|||||||
|
|
||||||
** Contract
|
** Contract
|
||||||
|
|
||||||
1. (prompt-prefix-cached assistant-name feedback mandates-text tool-belt):
|
1. (prompt-prefix-cached assistant-name identity-content feedback mandates-text tool-belt):
|
||||||
Build the IDENTITY+TOOLS system prompt prefix. Uses ~sxhash~ on the inputs
|
Build the IDENTITY+TOOLS system prompt prefix. Uses ~sxhash~ on the inputs
|
||||||
to detect changes. Returns the cached string when unchanged.
|
to detect changes. Returns the cached string when unchanged.
|
||||||
2. (context-assemble-cached context sensor): Incrementally assemble awareness
|
2. (context-assemble-cached context sensor): Incrementally assemble awareness
|
||||||
@@ -63,16 +63,16 @@ Depends on: tokenizer.lisp, cost-tracker.lisp
|
|||||||
|
|
||||||
** Contract 1: prompt prefix caching
|
** Contract 1: prompt prefix caching
|
||||||
#+begin_src lisp
|
#+begin_src lisp
|
||||||
(defun prompt-prefix-cached (assistant-name feedback mandates-text tool-belt)
|
(defun prompt-prefix-cached (assistant-name identity-content feedback mandates-text tool-belt)
|
||||||
"Build the static IDENTITY+TOOLS system prompt prefix.
|
"Build the static IDENTITY+TOOLS system prompt prefix.
|
||||||
Uses sxhash on inputs to detect changes; returns cached string on cache hit."
|
Uses sxhash on inputs to detect changes; returns cached string on cache hit."
|
||||||
(let* ((hash-key (sxhash (list assistant-name feedback mandates-text tool-belt)))
|
(let* ((hash-key (sxhash (list assistant-name identity-content feedback mandates-text tool-belt)))
|
||||||
(cached-hash (car *prompt-prefix-cache*))
|
(cached-hash (car *prompt-prefix-cache*))
|
||||||
(cached-str (cdr *prompt-prefix-cache*)))
|
(cached-str (cdr *prompt-prefix-cache*)))
|
||||||
(if (and cached-str (> (length cached-str) 0) (= hash-key cached-hash))
|
(if (and cached-str (> (length cached-str) 0) (= hash-key cached-hash))
|
||||||
cached-str
|
cached-str
|
||||||
(let ((new-prefix (format nil "IDENTITY: ~a~a~a~%~%TOOLS:~%~a"
|
(let ((new-prefix (format nil "IDENTITY: ~a~a~a~a~%~%TOOLS:~%~a"
|
||||||
assistant-name feedback
|
assistant-name identity-content feedback
|
||||||
(if (and mandates-text (> (length mandates-text) 0))
|
(if (and mandates-text (> (length mandates-text) 0))
|
||||||
(concatenate 'string (string #\Newline) mandates-text)
|
(concatenate 'string (string #\Newline) mandates-text)
|
||||||
"")
|
"")
|
||||||
@@ -184,11 +184,22 @@ with trimmed sections."
|
|||||||
:description "Prompt prefix caching, incremental context, token budget")
|
:description "Prompt prefix caching, incremental context, token budget")
|
||||||
(in-suite token-economics-suite)
|
(in-suite token-economics-suite)
|
||||||
|
|
||||||
|
(test test-prompt-prefix-cached-identity
|
||||||
|
"Contract 1: prompt-prefix-cached includes identity-content when provided."
|
||||||
|
(setf (car passepartout::*prompt-prefix-cache*) nil
|
||||||
|
(cdr passepartout::*prompt-prefix-cache*) "")
|
||||||
|
(let ((prefix (passepartout::prompt-prefix-cached
|
||||||
|
"Agent" "### Mode: concise" "" nil "No tools")))
|
||||||
|
(is (stringp prefix))
|
||||||
|
(is (search "IDENTITY" prefix))
|
||||||
|
(is (search "Mode: concise" prefix))
|
||||||
|
(is (search "TOOLS" prefix))))
|
||||||
|
|
||||||
(test test-prompt-prefix-cached-builds
|
(test test-prompt-prefix-cached-builds
|
||||||
"Contract 1: prompt-prefix-cached returns a string containing IDENTITY."
|
"Contract 1: prompt-prefix-cached returns a string containing IDENTITY."
|
||||||
(setf (car passepartout::*prompt-prefix-cache*) nil
|
(setf (car passepartout::*prompt-prefix-cache*) nil
|
||||||
(cdr passepartout::*prompt-prefix-cache*) "")
|
(cdr passepartout::*prompt-prefix-cache*) "")
|
||||||
(let ((prefix (passepartout::prompt-prefix-cached "Agent" "" nil "No tools")))
|
(let ((prefix (passepartout::prompt-prefix-cached "Agent" "" "" nil "No tools")))
|
||||||
(is (stringp prefix))
|
(is (stringp prefix))
|
||||||
(is (search "IDENTITY" prefix))
|
(is (search "IDENTITY" prefix))
|
||||||
(is (search "TOOLS" prefix))))
|
(is (search "TOOLS" prefix))))
|
||||||
@@ -197,16 +208,16 @@ with trimmed sections."
|
|||||||
"Contract 1: second call with same inputs returns cached result."
|
"Contract 1: second call with same inputs returns cached result."
|
||||||
(setf (car passepartout::*prompt-prefix-cache*) nil
|
(setf (car passepartout::*prompt-prefix-cache*) nil
|
||||||
(cdr passepartout::*prompt-prefix-cache*) "")
|
(cdr passepartout::*prompt-prefix-cache*) "")
|
||||||
(let ((p1 (passepartout::prompt-prefix-cached "Agent" "" nil "No tools"))
|
(let ((p1 (passepartout::prompt-prefix-cached "Agent" "" "" nil "No tools"))
|
||||||
(p2 (passepartout::prompt-prefix-cached "Agent" "" nil "No tools")))
|
(p2 (passepartout::prompt-prefix-cached "Agent" "" "" nil "No tools")))
|
||||||
(is (string= p1 p2))))
|
(is (string= p1 p2))))
|
||||||
|
|
||||||
(test test-prompt-prefix-cached-miss
|
(test test-prompt-prefix-cached-miss
|
||||||
"Contract 1: different inputs rebuild the cache."
|
"Contract 1: different inputs rebuild the cache."
|
||||||
(setf (car passepartout::*prompt-prefix-cache*) nil
|
(setf (car passepartout::*prompt-prefix-cache*) nil
|
||||||
(cdr passepartout::*prompt-prefix-cache*) "")
|
(cdr passepartout::*prompt-prefix-cache*) "")
|
||||||
(let ((p1 (passepartout::prompt-prefix-cached "Agent" "" nil "No tools"))
|
(let ((p1 (passepartout::prompt-prefix-cached "Agent" "" "" nil "No tools"))
|
||||||
(p2 (passepartout::prompt-prefix-cached "Bot" "" nil "No tools")))
|
(p2 (passepartout::prompt-prefix-cached "Bot" "" "" nil "No tools")))
|
||||||
(is (not (string= p1 p2)))
|
(is (not (string= p1 p2)))
|
||||||
(is (search "Bot" p2))))
|
(is (search "Bot" p2))))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user