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:
@@ -250,22 +250,26 @@ each cascade call via ~cost-track-backend-call~. All four calls are
|
||||
(reflection-feedback (if rejection-trace
|
||||
(format nil "~%~%PREVIOUS PROPOSAL REJECTED: ~a" rejection-trace)
|
||||
""))
|
||||
(standing-mandates-text (let ((out ""))
|
||||
(dolist (fn *standing-mandates*)
|
||||
(let ((text (ignore-errors (funcall fn context))))
|
||||
(when (and text (stringp text) (> (length text) 0))
|
||||
(setf out (concatenate 'string out text (string #\Newline))))))
|
||||
(when (> (length out) 0) out)))
|
||||
(time-section (if (fboundp 'sensor-time-duration) ; v0.6.0: temporal awareness
|
||||
(standing-mandates-text (let ((out ""))
|
||||
(dolist (fn *standing-mandates*)
|
||||
(let ((text (ignore-errors (funcall fn context))))
|
||||
(when (and text (stringp text) (> (length text) 0))
|
||||
(setf out (concatenate 'string out text (string #\Newline))))))
|
||||
(when (> (length out) 0) out)))
|
||||
(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
|
||||
:session-duration-seconds (funcall (symbol-function 'session-duration)))
|
||||
(if (fboundp 'format-time-for-llm)
|
||||
(format-time-for-llm)
|
||||
"")))
|
||||
(system-prompt (if (fboundp 'prompt-prefix-cached)
|
||||
;; v0.5.0: cached prefix with optional budget enforcement
|
||||
(let* ((prefix (prompt-prefix-cached assistant-name reflection-feedback
|
||||
standing-mandates-text tool-belt)))
|
||||
(system-prompt (if (fboundp 'prompt-prefix-cached)
|
||||
;; v0.5.0: cached prefix with optional budget enforcement
|
||||
(let* ((prefix (prompt-prefix-cached assistant-name identity-content
|
||||
reflection-feedback
|
||||
standing-mandates-text tool-belt)))
|
||||
(if (fboundp 'enforce-token-budget)
|
||||
(multiple-value-bind (pfx ctxt logs _ mandates)
|
||||
(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"
|
||||
time-section prefix (or global-context "") system-logs)))
|
||||
;; Fallback when token-economics not loaded
|
||||
(format nil "~a~%~%IDENTITY: ~a~a~a~%~%TOOLS:~%~a~%~%CONTEXT:~%~a~%~%LOGS:~%~a"
|
||||
time-section
|
||||
assistant-name reflection-feedback
|
||||
(if standing-mandates-text
|
||||
(concatenate 'string (string #\Newline) standing-mandates-text)
|
||||
"")
|
||||
tool-belt (or global-context "") system-logs))))
|
||||
(format nil "~a~%~%IDENTITY: ~a~a~a~a~%~%TOOLS:~%~a~%~%CONTEXT:~%~a~%~%LOGS:~%~a"
|
||||
time-section
|
||||
assistant-name identity-content reflection-feedback
|
||||
(if standing-mandates-text
|
||||
(concatenate 'string (string #\Newline) standing-mandates-text)
|
||||
"")
|
||||
tool-belt (or global-context "") system-logs))))
|
||||
(let* ((thought (if (and reply-stream (fboundp 'cascade-stream)) ; v0.7.1: streaming
|
||||
(let ((acc (make-string-output-stream)))
|
||||
(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
|
||||
|
||||
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
|
||||
to detect changes. Returns the cached string when unchanged.
|
||||
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
|
||||
#+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.
|
||||
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-str (cdr *prompt-prefix-cache*)))
|
||||
(if (and cached-str (> (length cached-str) 0) (= hash-key cached-hash))
|
||||
cached-str
|
||||
(let ((new-prefix (format nil "IDENTITY: ~a~a~a~%~%TOOLS:~%~a"
|
||||
assistant-name feedback
|
||||
(let ((new-prefix (format nil "IDENTITY: ~a~a~a~a~%~%TOOLS:~%~a"
|
||||
assistant-name identity-content feedback
|
||||
(if (and mandates-text (> (length mandates-text) 0))
|
||||
(concatenate 'string (string #\Newline) mandates-text)
|
||||
"")
|
||||
@@ -184,11 +184,22 @@ with trimmed sections."
|
||||
:description "Prompt prefix caching, incremental context, token budget")
|
||||
(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
|
||||
"Contract 1: prompt-prefix-cached returns a string containing IDENTITY."
|
||||
(setf (car passepartout::*prompt-prefix-cache*) nil
|
||||
(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 (search "IDENTITY" prefix))
|
||||
(is (search "TOOLS" prefix))))
|
||||
@@ -197,16 +208,16 @@ with trimmed sections."
|
||||
"Contract 1: second call with same inputs returns cached result."
|
||||
(setf (car passepartout::*prompt-prefix-cache*) nil
|
||||
(cdr passepartout::*prompt-prefix-cache*) "")
|
||||
(let ((p1 (passepartout::prompt-prefix-cached "Agent" "" nil "No tools"))
|
||||
(p2 (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")))
|
||||
(is (string= p1 p2))))
|
||||
|
||||
(test test-prompt-prefix-cached-miss
|
||||
"Contract 1: different inputs rebuild the cache."
|
||||
(setf (car passepartout::*prompt-prefix-cache*) nil
|
||||
(cdr passepartout::*prompt-prefix-cache*) "")
|
||||
(let ((p1 (passepartout::prompt-prefix-cached "Agent" "" nil "No tools"))
|
||||
(p2 (passepartout::prompt-prefix-cached "Bot" "" nil "No tools")))
|
||||
(let ((p1 (passepartout::prompt-prefix-cached "Agent" "" "" nil "No tools"))
|
||||
(p2 (passepartout::prompt-prefix-cached "Bot" "" "" nil "No tools")))
|
||||
(is (not (string= p1 p2)))
|
||||
(is (search "Bot" p2))))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user