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:
2026-05-08 15:14:44 -04:00
parent b40e1e2844
commit bec894ca4f
6 changed files with 303 additions and 55 deletions

View File

@@ -95,22 +95,26 @@
(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
@@ -122,13 +126,13 @@
(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

View 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))))

View File

@@ -6,16 +6,16 @@
(defvar *context-cache* (list :foveal-id nil :scope nil :memory-timestamp 0 :rendered "")
"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.
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)
"")
@@ -115,11 +115,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))))
@@ -128,16 +139,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))))

View File

@@ -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
View 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

View File

@@ -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))))