From bec894ca4f627859f6f78bf02bb4edbe780f5633 Mon Sep 17 00:00:00 2001 From: Amr Gharbeia Date: Fri, 8 May 2026 15:14:44 -0400 Subject: [PATCH] =?UTF-8?q?handoff:=20symbolic=20identity=20file=20?= =?UTF-8?q?=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lisp/core-reason.lisp | 40 ++++++------ lisp/symbolic-identity.lisp | 92 ++++++++++++++++++++++++++ lisp/token-economics.lisp | 29 ++++++--- org/core-reason.org | 40 ++++++------ org/symbolic-identity.org | 126 ++++++++++++++++++++++++++++++++++++ org/token-economics.org | 31 ++++++--- 6 files changed, 303 insertions(+), 55 deletions(-) create mode 100644 lisp/symbolic-identity.lisp create mode 100644 org/symbolic-identity.org diff --git a/lisp/core-reason.lisp b/lisp/core-reason.lisp index 799fe99..d08d07e 100644 --- a/lisp/core-reason.lisp +++ b/lisp/core-reason.lisp @@ -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 diff --git a/lisp/symbolic-identity.lisp b/lisp/symbolic-identity.lisp new file mode 100644 index 0000000..efbc120 --- /dev/null +++ b/lisp/symbolic-identity.lisp @@ -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)))) diff --git a/lisp/token-economics.lisp b/lisp/token-economics.lisp index ff5a65e..c3a9872 100644 --- a/lisp/token-economics.lisp +++ b/lisp/token-economics.lisp @@ -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)))) diff --git a/org/core-reason.org b/org/core-reason.org index 43eac93..7fe7ef4 100644 --- a/org/core-reason.org +++ b/org/core-reason.org @@ -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 diff --git a/org/symbolic-identity.org b/org/symbolic-identity.org new file mode 100644 index 0000000..12bbd0f --- /dev/null +++ b/org/symbolic-identity.org @@ -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 diff --git a/org/token-economics.org b/org/token-economics.org index 167eaca..1176091 100644 --- a/org/token-economics.org +++ b/org/token-economics.org @@ -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))))