RELEASE: Finalize Semantic Restructuring v0.1.0
Some checks failed
Deploy-Agent-V15-Stdin / JOB-V15-STDIN (push) Failing after 3s
Some checks failed
Deploy-Agent-V15-Stdin / JOB-V15-STDIN (push) Failing after 3s
- Folders: literate->harness, src->library, system->environment, scripts->interfaces. - Synchronized all :tangle paths and system definitions. - Hardened .gitignore for binary and log artifacts. - Consolidated all documentation into docs/.
This commit is contained in:
151
library/act.lisp
Normal file
151
library/act.lisp
Normal file
@@ -0,0 +1,151 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *default-actuator* :cli
|
||||
"The fallback actuator used if a signal has no source or target metadata.")
|
||||
|
||||
(defvar *silent-actuators* '(:cli :system-message :emacs)
|
||||
"List of actuators whose feedback should not re-enter the Reasoning stage.")
|
||||
|
||||
(defun initialize-actuators ()
|
||||
"Loads actuator routing defaults from environment variables and registers core harness actuators."
|
||||
(let ((def (uiop:getenv "DEFAULT_ACTUATOR"))
|
||||
(silent (uiop:getenv "SILENT_ACTUATORS")))
|
||||
(when def
|
||||
(setf *default-actuator* (intern (string-upcase def) "KEYWORD")))
|
||||
(when silent
|
||||
(setf *silent-actuators*
|
||||
(mapcar (lambda (s) (intern (string-upcase (string-trim '(#\Space) s)) "KEYWORD"))
|
||||
(str:split "," silent)))))
|
||||
|
||||
;; Register core harness actuators
|
||||
(register-actuator :system #'execute-system-action)
|
||||
(register-actuator :tool #'execute-tool-action)
|
||||
(register-actuator :tui (lambda (action context)
|
||||
(let* ((meta (getf context :meta))
|
||||
(stream (getf meta :reply-stream)))
|
||||
(when (and stream (open-stream-p stream))
|
||||
(format stream "~a" (frame-message action))
|
||||
(finish-output stream))))))
|
||||
|
||||
(defun dispatch-action (action context)
|
||||
"Routes an approved action to its registered physical actuator."
|
||||
(let ((payload (proto-get action :payload)))
|
||||
;; Optimization: Heartbeats are system events, not actions.
|
||||
(when (eq (proto-get payload :sensor) :heartbeat)
|
||||
(return-from dispatch-action nil)))
|
||||
|
||||
(when (and action (listp action))
|
||||
(let* ((meta (proto-get context :meta))
|
||||
(source (proto-get meta :source))
|
||||
(raw-target (or (ignore-errors (getf action :TARGET))
|
||||
(ignore-errors (getf action :target))
|
||||
source
|
||||
*default-actuator*))
|
||||
(target (intern (string-upcase (string raw-target)) :keyword))
|
||||
(actuator-fn (gethash target *actuator-registry*)))
|
||||
;; Propagation: Ensure outbound action inherits metadata
|
||||
(when (and meta (null (getf action :meta)))
|
||||
(setf (getf action :meta) meta))
|
||||
(if actuator-fn
|
||||
(funcall actuator-fn action context)
|
||||
(harness-log "ACT ERROR: No actuator for ~s (from ~s)" target raw-target)))))
|
||||
|
||||
(defun execute-system-action (action context)
|
||||
"Processes internal harness commands. (ACTUATOR)"
|
||||
(declare (ignore context))
|
||||
(let* ((payload (ignore-errors (getf action :payload)))
|
||||
(cmd (ignore-errors (getf payload :action))))
|
||||
(case cmd
|
||||
(:eval (let ((code (getf payload :code)))
|
||||
(eval (read-from-string code))))
|
||||
(:create-skill (let* ((filename (getf payload :filename)) (content (getf payload :content))
|
||||
(skills-dir (merge-pathnames "skills/" (asdf:system-source-directory :opencortex)))
|
||||
(full-path (merge-pathnames filename skills-dir)))
|
||||
(with-open-file (out full-path :direction :output :if-exists :supersede) (write-string content out))
|
||||
(load-skill-from-org full-path)))
|
||||
(:message (harness-log "ACT [System]: ~a" (getf payload :text)))
|
||||
(t (harness-log "ACT ERROR [System]: Unknown command ~s" cmd)))))
|
||||
|
||||
(defun format-tool-result (tool-name result)
|
||||
"Intelligently formats a tool result for user display."
|
||||
(if (listp result)
|
||||
(let ((status (getf result :status))
|
||||
(content (getf result :content))
|
||||
(msg (getf result :message)))
|
||||
(cond ((and (eq status :success) content) (format nil "~a" content))
|
||||
((and (eq status :error) msg) (format nil "ERROR [~a]: ~a" tool-name msg))
|
||||
(t (format nil "TOOL [~a] RESULT: ~s" tool-name result))))
|
||||
(format nil "TOOL [~a] RESULT: ~a" tool-name result)))
|
||||
|
||||
(defun execute-tool-action (action context)
|
||||
"Executes a registered cognitive tool and generates feedback signals. (ACTUATOR)"
|
||||
(let* ((payload (getf action :payload))
|
||||
(tool-name (getf payload :tool))
|
||||
(tool-args (getf payload :args))
|
||||
(depth (getf context :depth 0))
|
||||
(meta (getf context :meta))
|
||||
(source (getf meta :source))
|
||||
(tool (gethash (string-downcase (string tool-name)) *cognitive-tools*)))
|
||||
(if tool
|
||||
(handler-case
|
||||
(let* ((clean-args (if (and (listp tool-args) (listp (car tool-args))) (car tool-args) tool-args))
|
||||
(result (funcall (cognitive-tool-body tool) clean-args)))
|
||||
(let ((feedback (list :TYPE :EVENT :DEPTH (1+ depth) :META meta
|
||||
:PAYLOAD (list :SENSOR :tool-output :RESULT result :TOOL tool-name))))
|
||||
;; UI Propagation: Send distilled text result back to the source client
|
||||
(when source
|
||||
(dispatch-action (list :TYPE :REQUEST :TARGET source
|
||||
:PAYLOAD (list :ACTION :MESSAGE :TEXT (format-tool-result tool-name result)))
|
||||
context))
|
||||
feedback))
|
||||
(error (c)
|
||||
(list :TYPE :EVENT :DEPTH (1+ depth) :META meta
|
||||
:PAYLOAD (list :SENSOR :tool-error :tool tool-name :message (format nil "~a" c)))))
|
||||
(list :TYPE :EVENT :DEPTH (1+ depth) :META meta
|
||||
:PAYLOAD (list :SENSOR :tool-error :message "Tool not found")))))
|
||||
|
||||
(defun act-gate (signal)
|
||||
"Final Stage: Actuation and feedback generation."
|
||||
(let* ((approved (getf signal :approved-action))
|
||||
(type (getf signal :type))
|
||||
(meta (getf signal :meta))
|
||||
(source (getf meta :source))
|
||||
(feedback nil)
|
||||
;; context must keep internal objects for actuators to function
|
||||
(context signal))
|
||||
|
||||
;; 1. Last-Mile Safety Check (The Bouncer & Deterministic Gates)
|
||||
(when approved
|
||||
(let* ((original-type (getf approved :type))
|
||||
(verified (deterministic-verify approved signal)))
|
||||
(if (and (listp verified)
|
||||
(member (getf verified :type) '(:LOG :EVENT :log :event))
|
||||
(not (member original-type '(:LOG :EVENT :log :event))))
|
||||
(progn
|
||||
(harness-log "ACT BLOCKED: Action failed last-mile deterministic check.")
|
||||
(setf (getf signal :approved-action) nil)
|
||||
(setf approved nil)
|
||||
(setf feedback verified))
|
||||
(progn
|
||||
(setf (getf signal :approved-action) verified)
|
||||
(setf approved verified)))))
|
||||
|
||||
;; 2. Actuation Logic
|
||||
(case type
|
||||
(:REQUEST (dispatch-action signal context))
|
||||
(:LOG (dispatch-action signal context))
|
||||
(:EVENT
|
||||
(if approved
|
||||
(let* ((target (getf approved :target))
|
||||
(result (dispatch-action approved context)))
|
||||
(cond ((and (listp result) (member (getf result :type) '(:EVENT :LOG)))
|
||||
(setf feedback result))
|
||||
((and result (not (member target *silent-actuators*)))
|
||||
(setf feedback (list :type :EVENT :depth (1+ (getf signal :depth 0)) :meta meta
|
||||
:payload (list :sensor :tool-output :result result :tool approved))))))
|
||||
;; Fallback: route generic stimuli back to their origin
|
||||
(when source
|
||||
(dispatch-action signal context)))))
|
||||
|
||||
(setf (getf signal :status) :acted)
|
||||
feedback))
|
||||
39
library/communication-validator.lisp
Normal file
39
library/communication-validator.lisp
Normal file
@@ -0,0 +1,39 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun validate-communication-protocol-schema (msg)
|
||||
"Strict structural validation for incoming communication protocol messages."
|
||||
(unless (listp msg)
|
||||
(error "Communication Protocol Schema Error: Message must be a property list (got ~s)" (type-of msg)))
|
||||
|
||||
(let ((type (let ((raw (proto-get msg :type))) (if (keywordp raw) (intern (string-upcase (string raw)) :keyword) raw))))
|
||||
(unless (member type '(:REQUEST :EVENT :RESPONSE :LOG :STATUS :CHAT))
|
||||
(progn (harness-log "REJECTED MSG: ~s" msg) (error "Communication Protocol Schema Error: Invalid message type '~a'" type)))
|
||||
|
||||
(case type
|
||||
(:REQUEST
|
||||
(unless (proto-get msg :target)
|
||||
(error "Communication Protocol Schema Error: REQUEST missing mandatory :target"))
|
||||
(unless (proto-get msg :payload)
|
||||
(error "Communication Protocol Schema Error: REQUEST missing mandatory :payload")))
|
||||
|
||||
(:EVENT
|
||||
(let ((payload (proto-get msg :payload)))
|
||||
(unless (and payload (listp payload))
|
||||
(error "Communication Protocol Schema Error: EVENT missing or invalid :payload"))
|
||||
(unless (or (proto-get payload :action) (proto-get payload :sensor))
|
||||
(error "Communication Protocol Schema Error: EVENT payload must contain :action or :sensor"))))
|
||||
|
||||
(:RESPONSE
|
||||
(unless (proto-get msg :payload)
|
||||
(error "Communication Protocol Schema Error: RESPONSE missing mandatory :payload"))))
|
||||
|
||||
t))
|
||||
|
||||
(defskill :skill-communication-protocol-validator
|
||||
:priority 95
|
||||
:trigger (lambda (ctx) (member (getf (getf ctx :payload) :sensor) '(:protocol-received)))
|
||||
:probabilistic nil
|
||||
:deterministic (lambda (action ctx)
|
||||
(declare (ignore ctx))
|
||||
(validate-communication-protocol-schema action)
|
||||
action))
|
||||
46
library/communication.lisp
Normal file
46
library/communication.lisp
Normal file
@@ -0,0 +1,46 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun sanitize-protocol-message (msg)
|
||||
"Recursively strips non-serializable objects (streams, sockets) from a protocol plist."
|
||||
(if (and msg (listp msg))
|
||||
(let ((clean nil))
|
||||
(loop for (k v) on msg by #'cddr
|
||||
do (unless (member k '(:reply-stream :socket :stream))
|
||||
(push k clean)
|
||||
(push (if (listp v) (sanitize-protocol-message v) v) clean)))
|
||||
(nreverse clean))
|
||||
msg))
|
||||
|
||||
(defun frame-message (msg)
|
||||
"Serializes a message plist and prefixes it with a 6-character hex length."
|
||||
(let* ((sanitized (sanitize-protocol-message msg))
|
||||
(payload (let ((*print-pretty* nil) (*read-eval* nil)) (format nil "~s" sanitized)))
|
||||
(len (length payload)))
|
||||
(format nil "~6,'0x~a" len payload)))
|
||||
|
||||
(defun read-framed-message (stream)
|
||||
"Reads a hex-prefixed message from a stream. Returns the parsed Lisp plist or :EOF."
|
||||
(handler-case
|
||||
(let ((len-buf (make-string 6)))
|
||||
;; 1. Read the length prefix
|
||||
(let ((count (read-sequence len-buf stream)))
|
||||
(if (< count 6)
|
||||
:eof
|
||||
(let ((len (ignore-errors (parse-integer len-buf :radix 16))))
|
||||
(if (and len (> len 0))
|
||||
;; 2. Read exactly 'len' bytes
|
||||
(let ((payload-buf (make-string len)))
|
||||
(read-sequence payload-buf stream)
|
||||
(let ((*read-eval* nil))
|
||||
(read-from-string payload-buf)))
|
||||
:error)))))
|
||||
(error (c)
|
||||
(harness-log "PROTOCOL ERROR: ~a" c)
|
||||
:error)))
|
||||
|
||||
(defun make-hello-message (version)
|
||||
"Constructs the standard HELLO handshake message."
|
||||
(list :TYPE :EVENT
|
||||
:PAYLOAD (list :ACTION :handshake
|
||||
:VERSION version
|
||||
:CAPABILITIES '(:AUTH :SWANK :ORG-AST))))
|
||||
41
library/context.lisp
Normal file
41
library/context.lisp
Normal file
@@ -0,0 +1,41 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun context-get-active-projects ()
|
||||
"Retrieves a list of project headlines currently marked as NEXT or in progress."
|
||||
(let ((all-projects (list-objects-with-attribute :CATEGORY "Project")))
|
||||
(loop for p in all-projects
|
||||
collect (list :id (org-object-id p)
|
||||
:title (getf (org-object-attributes p) :TITLE)))))
|
||||
|
||||
(defun context-get-recent-completed-tasks (&optional (limit 5))
|
||||
"Retrieves the last N tasks marked as DONE from the memory history."
|
||||
(let ((all-completed (list-objects-with-attribute :TODO "DONE")))
|
||||
(subseq (sort all-completed #'> :key #'org-object-version)
|
||||
0 (min limit (length all-completed)))))
|
||||
|
||||
(defun context-list-all-skills ()
|
||||
"Returns a list of registered skills and their documentation."
|
||||
(let ((results nil))
|
||||
(maphash (lambda (id skill)
|
||||
(push (list :id id :name (skill-name skill)) results))
|
||||
*skills-registry*)
|
||||
results))
|
||||
|
||||
(defun context-get-system-logs ()
|
||||
"Retrieves the in-memory circular log buffer."
|
||||
(bt:with-lock-held (*logs-lock*)
|
||||
(format nil "~{~a~%~}" (reverse *system-logs*))))
|
||||
|
||||
(defun context-assemble-global-awareness ()
|
||||
"Assembles the full context block for a neural request."
|
||||
(let ((projects (context-get-active-projects))
|
||||
(time (multiple-value-bind (s m h d mo y) (get-decoded-time) (format nil "~a-~a-~a ~a:~a:~a" y mo d h m s))))
|
||||
(format nil "CURRENT_TIME: ~a. ACTIVE_PROJECTS: ~s. FOVEAL_FOCUS: ~a"
|
||||
time
|
||||
projects
|
||||
(or *foveal-focus-id* "None"))))
|
||||
|
||||
(defun context-query-store (query &key (limit 5))
|
||||
"Placeholder for semantic/vector search over the Memex."
|
||||
(declare (ignore query limit))
|
||||
nil)
|
||||
92
library/loop.lisp
Normal file
92
library/loop.lisp
Normal file
@@ -0,0 +1,92 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *interrupt-flag* nil
|
||||
"Thread-safe signal to halt the metabolic pipeline and daemon.")
|
||||
|
||||
(defvar *interrupt-lock* (bt:make-lock "harness-interrupt-lock")
|
||||
"Protects the interrupt flag from concurrent access.")
|
||||
|
||||
(defvar *heartbeat-thread* nil
|
||||
"Reference to the background thread driving autonomous reflection.")
|
||||
|
||||
(defun process-signal (signal)
|
||||
"The entry point to the Metabolic Pipeline: Perceive -> Reason -> Act."
|
||||
(let ((current-signal signal))
|
||||
(loop while current-signal do
|
||||
(let ((depth (getf current-signal :depth 0))
|
||||
(meta (getf current-signal :meta)))
|
||||
;; Safety: Prevent infinite cognitive recursion.
|
||||
(when (> depth 10) (harness-log "METABOLISM ERROR: Max depth reached.") (return nil))
|
||||
|
||||
;; Check for graceful shutdown.
|
||||
(when (bt:with-lock-held (*interrupt-lock*) *interrupt-flag*)
|
||||
(harness-log "METABOLISM: Interrupted.")
|
||||
(bt:with-lock-held (*interrupt-lock*) (setf *interrupt-flag* nil))
|
||||
(return nil))
|
||||
|
||||
(handler-case
|
||||
(progn
|
||||
;; Stage 1: Ingest and Normalize
|
||||
(setf current-signal (perceive-gate current-signal))
|
||||
;; Stage 2: Cogitate and Verify
|
||||
(setf current-signal (reason-gate current-signal))
|
||||
;; Stage 3: Actuate and Generate Feedback
|
||||
(let ((feedback (act-gate current-signal)))
|
||||
(if feedback
|
||||
(progn
|
||||
;; Inheritance: Metadata must persist across recursive cycles.
|
||||
(unless (getf feedback :meta) (setf (getf feedback :meta) meta))
|
||||
(setf current-signal feedback))
|
||||
(setf current-signal nil))))
|
||||
(error (c)
|
||||
(let ((sensor (ignore-errors (getf (getf current-signal :payload) :sensor))))
|
||||
(harness-log "METABOLISM CRASH [~a]: ~a" (or sensor :unknown) c)
|
||||
;; Resilience: Only rollback on critical system errors.
|
||||
(unless (member sensor '(:loop-error :tool-error :syntax-error))
|
||||
(harness-log "CRITICAL ERROR: Initiating Micro-Rollback.")
|
||||
(rollback-memory 0))
|
||||
;; If recursion is shallow, attempt to notify the user of the error.
|
||||
(if (or (> depth 2) (member sensor '(:loop-error :tool-error)))
|
||||
(setf current-signal nil)
|
||||
(setf current-signal (list :type :EVENT :depth (1+ depth) :meta meta
|
||||
:payload (list :sensor :loop-error :message (format nil "~a" c) :depth depth)))))))))))
|
||||
|
||||
(defun start-heartbeat ()
|
||||
"Starts the background heartbeat thread. Interval is loaded from HEARTBEAT_INTERVAL (default: 60s)."
|
||||
(let ((interval (or (ignore-errors (parse-integer (uiop:getenv "HEARTBEAT_INTERVAL"))) 60)))
|
||||
(setf *heartbeat-thread*
|
||||
(bt:make-thread
|
||||
(lambda ()
|
||||
(loop
|
||||
(sleep interval)
|
||||
;; Note: inject-stimulus is synchronous for heartbeats to prevent task accumulation.
|
||||
(inject-stimulus (list :type :EVENT :payload (list :sensor :heartbeat :unix-time (get-universal-time))))))
|
||||
:name "opencortex-heartbeat"))))
|
||||
|
||||
(defun main ()
|
||||
"Primary entry point for the OpenCortex daemon."
|
||||
;; 1. Environment Hydration
|
||||
(let* ((home (uiop:getenv "HOME"))
|
||||
(env-file (uiop:merge-pathnames* ".local/share/opencortex/.env" (uiop:ensure-directory-pathname home))))
|
||||
(when (uiop:file-exists-p env-file) (cl-dotenv:load-env env-file)))
|
||||
|
||||
;; 2. System Bootstrap
|
||||
(initialize-actuators)
|
||||
(initialize-all-skills)
|
||||
|
||||
;; 3. Wake up the heart.
|
||||
(start-heartbeat)
|
||||
|
||||
;; 4. OS Signal Handling (SBCL specific)
|
||||
#+sbcl
|
||||
(sb-sys:enable-interrupt sb-unix:sigint
|
||||
(lambda (sig code scp)
|
||||
(declare (ignore sig code scp))
|
||||
(harness-log "SHUTDOWN: SIGINT received. Exiting...")
|
||||
(uiop:quit 0)))
|
||||
|
||||
;; 5. Primary Idle Loop
|
||||
(let ((sleep-interval (or (ignore-errors (parse-integer (uiop:getenv "DAEMON_SLEEP_INTERVAL"))) 3600)))
|
||||
(loop
|
||||
(when (bt:with-lock-held (*interrupt-lock*) *interrupt-flag*) (return))
|
||||
(sleep sleep-interval))))
|
||||
81
library/memory.lisp
Normal file
81
library/memory.lisp
Normal file
@@ -0,0 +1,81 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *memory* (make-hash-table :test 'equal)
|
||||
"The primary in-memory graph of all Org-mode entities, keyed by their unique ID.")
|
||||
|
||||
(defvar *history-store* (make-array 0 :fill-pointer 0 :adjustable t)
|
||||
"A versioned log of the memory state, allowing for temporal traversal and rollback.")
|
||||
|
||||
(defstruct org-object
|
||||
"The fundamental unit of knowledge in the OpenCortex."
|
||||
id
|
||||
type
|
||||
attributes
|
||||
parent-id
|
||||
children
|
||||
version
|
||||
last-sync
|
||||
vector
|
||||
content
|
||||
hash)
|
||||
|
||||
(defun compute-merkle-hash (id type attributes content child-hashes)
|
||||
"Computes a SHA-256 Merkle hash for a node based on its core properties and children's hashes."
|
||||
(let* ((alist (loop for (k v) on attributes by #'cddr collect (cons k v)))
|
||||
(sorted-alist (sort alist #'string< :key (lambda (x) (format nil "~a" (car x)))))
|
||||
(attr-string (format nil "~s" sorted-alist))
|
||||
(children-string (format nil "~{~a~}" child-hashes))
|
||||
(raw-data (format nil "~a|~a|~a|~a|~a" id type attr-string (or content "") children-string)))
|
||||
(ironclad:byte-array-to-hex-string
|
||||
(ironclad:digest-sequence :sha256 (ironclad:ascii-string-to-byte-array raw-data)))))
|
||||
|
||||
(defun ingest-ast (ast &optional parent-id)
|
||||
"Recursively parses an Org AST into the Lisp Memory registry."
|
||||
(let* ((type (getf ast :type))
|
||||
(properties (getf ast :properties))
|
||||
(id (or (getf properties :ID) (uuid:make-v4-uuid)))
|
||||
(content (getf ast :content))
|
||||
(children (getf ast :contents))
|
||||
(child-ids nil))
|
||||
|
||||
;; Recursively ingest children and collect their IDs
|
||||
(dolist (child children)
|
||||
(let ((child-obj (ingest-ast child id)))
|
||||
(when child-obj (push (org-object-id child-obj) child-ids))))
|
||||
|
||||
(let ((obj (make-org-object :id id
|
||||
:type type
|
||||
:attributes properties
|
||||
:parent-id parent-id
|
||||
:children (nreverse child-ids)
|
||||
:content content
|
||||
:version (get-universal-time))))
|
||||
(setf (gethash id *memory*) obj)
|
||||
obj)))
|
||||
|
||||
(defun lookup-object (id)
|
||||
"Retrieves an object from memory by its ID."
|
||||
(gethash id *memory*))
|
||||
|
||||
(defun list-objects-with-attribute (key value)
|
||||
"Returns a list of objects that possess the specified attribute pair."
|
||||
(let ((results nil))
|
||||
(maphash (lambda (id obj)
|
||||
(declare (ignore id))
|
||||
(when (equal (getf (org-object-attributes obj) key) value)
|
||||
(push obj results)))
|
||||
*memory*)
|
||||
results))
|
||||
|
||||
(defun snapshot-memory ()
|
||||
"Creates a deep copy of the memory hash table and pushes it to the history store."
|
||||
(let ((new-snap (make-hash-table :test 'equal)))
|
||||
(maphash (lambda (k v) (setf (gethash k new-snap) (copy-org-object v))) *memory*)
|
||||
(vector-push-extend new-snap *history-store*)))
|
||||
|
||||
(defun rollback-memory (&optional (steps 1))
|
||||
"Restores the memory to a previous snapshot state."
|
||||
(let ((index (- (length *history-store*) steps 1)))
|
||||
(when (>= index 0)
|
||||
(setf *memory* (aref *history-store* index))
|
||||
(harness-log "IMMUNE SYSTEM: Memory rolled back ~a steps." steps))))
|
||||
424
library/opencortex.el
Normal file
424
library/opencortex.el
Normal file
@@ -0,0 +1,424 @@
|
||||
;;; opencortex.el --- Probabilistic-Deterministic Lisp Machine Kernel for Org-mode -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2026 Amr
|
||||
;;
|
||||
;; Author: Amr
|
||||
;; Version: 0.1.0
|
||||
;; Package-Requires: ((emacs "27.1"))
|
||||
;; Keywords: convenience, org
|
||||
;; URL: https://github.com/amr/opencortex
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; opencortex provides a Probabilistic-Deterministic Lisp Machine interface for Emacs.
|
||||
;; It acts as the sensor/actuator array, communicating with a persistent
|
||||
;; Common Lisp daemon over a high-speed communication protocol socket.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'json)
|
||||
(require 'cl-lib)
|
||||
(require 'org-id)
|
||||
(require 'org-element)
|
||||
|
||||
(defgroup opencortex nil
|
||||
"Emacs interface for the opencortex Common Lisp daemon."
|
||||
:group 'org)
|
||||
|
||||
(defcustom opencortex-port 9105
|
||||
"The port the opencortex daemon is listening on."
|
||||
:type 'integer
|
||||
:group 'opencortex)
|
||||
|
||||
(defcustom opencortex-host "127.0.0.1"
|
||||
"The host the opencortex daemon is running on."
|
||||
:type 'string
|
||||
:group 'opencortex)
|
||||
|
||||
(defcustom opencortex-executable-path "opencortex-server"
|
||||
"Path to the compiled opencortex-server binary.
|
||||
If nil, Emacs will not attempt to start the daemon automatically and
|
||||
will assume you have started it manually (e.g., via SBCL)."
|
||||
:type '(choice (string :tag "Path to executable")
|
||||
(const :tag "Manual daemon management" nil))
|
||||
:group 'opencortex)
|
||||
|
||||
(defvar opencortex--network-process nil
|
||||
"The network process connected to the daemon.")
|
||||
|
||||
(defvar opencortex--daemon-process nil
|
||||
"The spawned daemon child process.")
|
||||
|
||||
(defun opencortex--start-daemon ()
|
||||
"Start the daemon binary if not already running."
|
||||
(when (and opencortex-executable-path
|
||||
(not (process-live-p opencortex--daemon-process)))
|
||||
(message "opencortex: Starting daemon (%s)..." opencortex-executable-path)
|
||||
(setq opencortex--daemon-process
|
||||
(make-process
|
||||
:name "opencortex-daemon"
|
||||
:buffer "*opencortex-daemon*"
|
||||
:command (list opencortex-executable-path (number-to-string opencortex-port))
|
||||
:connection-type 'pipe))
|
||||
;; Give it a moment to bind to the port
|
||||
(sleep-for 1.0)))
|
||||
|
||||
(defun opencortex-connect ()
|
||||
"Connect to the opencortex daemon, starting it if necessary."
|
||||
(interactive)
|
||||
(when opencortex--network-process
|
||||
(delete-process opencortex--network-process))
|
||||
|
||||
(opencortex--start-daemon)
|
||||
|
||||
(condition-case err
|
||||
(progn
|
||||
(setq opencortex--network-process
|
||||
(make-network-process
|
||||
:name "opencortex"
|
||||
:buffer "*opencortex*"
|
||||
:family 'ipv4
|
||||
:host opencortex-host
|
||||
:service opencortex-port
|
||||
:filter #'opencortex--filter
|
||||
:sentinel #'opencortex--sentinel))
|
||||
(message "opencortex: Connected to daemon."))
|
||||
(error
|
||||
(message "opencortex: Failed to connect to daemon at %s:%s. Ensure it is running. Error: %s"
|
||||
opencortex-host opencortex-port (error-message-string err)))))
|
||||
|
||||
(defun opencortex-disconnect ()
|
||||
"Disconnect from the opencortex daemon."
|
||||
(interactive)
|
||||
(when opencortex--network-process
|
||||
(delete-process opencortex--network-process)
|
||||
(setq opencortex--network-process nil)
|
||||
(message "opencortex: Disconnected from network."))
|
||||
(when opencortex--daemon-process
|
||||
(delete-process opencortex--daemon-process)
|
||||
(setq opencortex--daemon-process nil)
|
||||
(message "opencortex: Killed daemon process.")))
|
||||
|
||||
(defun opencortex--filter (proc string)
|
||||
"Handle incoming communication protocol messages from the daemon via PROC with STRING."
|
||||
(let ((buf (process-buffer proc)))
|
||||
(when (buffer-live-p buf)
|
||||
(with-current-buffer buf
|
||||
(goto-char (point-max))
|
||||
(insert string)
|
||||
(opencortex--process-buffer buf proc)))))
|
||||
|
||||
(defun opencortex--process-buffer (buffer &optional proc)
|
||||
"Process the communication protocol message BUFFER, optionally using PROC."
|
||||
(with-current-buffer buffer
|
||||
(goto-char (point-min))
|
||||
(while (>= (buffer-size) 6)
|
||||
(let* ((len-str (buffer-substring (point-min) (+ (point-min) 6)))
|
||||
(msg-len (string-to-number len-str 16)))
|
||||
(if (>= (buffer-size) (+ 6 msg-len))
|
||||
(let* ((msg-start (+ (point-min) 6))
|
||||
(msg-end (+ msg-start msg-len))
|
||||
(msg-str (buffer-substring msg-start msg-end))
|
||||
(plist (car (read-from-string msg-str))))
|
||||
(delete-region (point-min) msg-end)
|
||||
(opencortex--handle-message proc plist))
|
||||
;; Message incomplete, stop loop
|
||||
(goto-char (point-max))
|
||||
(setq msg-len 1000000)))))) ; Break loop
|
||||
|
||||
(defun opencortex--plist-get (plist prop)
|
||||
"Case-insensitive keyword lookup for communication protocol compatibility."
|
||||
(or (plist-get plist prop)
|
||||
(plist-get plist (intern (upcase (symbol-name prop))))
|
||||
(plist-get plist (intern (downcase (symbol-name prop))))))
|
||||
|
||||
(defun opencortex--handle-message (proc plist)
|
||||
"Route and execute incoming communication protocol messages from PROC using PLIST."
|
||||
(let ((type (opencortex--plist-get plist :type))
|
||||
(id (opencortex--plist-get plist :id))
|
||||
(payload (or (opencortex--plist-get plist :payload) plist)))
|
||||
(cond
|
||||
((member type '(:request :REQUEST))
|
||||
(opencortex--execute-request proc id payload))
|
||||
((member type '(:response :RESPONSE))
|
||||
(message "opencortex: Received response for ID %s" id))
|
||||
((member type '(:log :LOG))
|
||||
(let ((text (opencortex--plist-get payload :text))
|
||||
(meta (opencortex--plist-get plist :meta)))
|
||||
(opencortex--insert-to-history (concat "[reasoning" (if meta (format " (%s)" (opencortex--plist-get meta :source)) "") "] " text "\n") 'opencortex-system-face)))
|
||||
(t (message "opencortex: Received unknown message type %s" type)))))
|
||||
|
||||
(defun opencortex--execute-request (proc id payload)
|
||||
"Execute an actuator request from the daemon via PROC with ID and PAYLOAD."
|
||||
(let ((action (opencortex--plist-get payload :action)))
|
||||
(cond
|
||||
((member action '(:eval :EVAL))
|
||||
(let ((code (opencortex--plist-get payload :code)))
|
||||
(condition-case err
|
||||
(let ((result (eval (read code))))
|
||||
(opencortex-send
|
||||
`(:type :RESPONSE :id ,id :payload (:status :success :result ,(format "%s" result)))))
|
||||
(error
|
||||
(opencortex-send
|
||||
`(:type :RESPONSE :id ,id :payload (:status :error :message ,(error-message-string err))))))))
|
||||
((member action '(:message :MESSAGE))
|
||||
(message "opencortex [DAEMON]: %s" (opencortex--plist-get payload :text))
|
||||
(opencortex-send `(:type :RESPONSE :id ,id :payload (:status :success))))
|
||||
((member action '(:insert-at-end :INSERT-AT-END))
|
||||
(let ((text (opencortex--plist-get payload :text)))
|
||||
(opencortex--insert-to-history (concat "\nAGENT: " text "\n\n"))
|
||||
(opencortex-send `(:type :RESPONSE :id ,id :payload (:status :success)))))
|
||||
((member action '(:refactor-subtree :REFACTOR-SUBTREE))
|
||||
(let ((target-id (opencortex--plist-get payload :target-id))
|
||||
(properties (opencortex--plist-get payload :properties)))
|
||||
(condition-case err
|
||||
(save-excursion
|
||||
(when target-id (org-id-goto target-id))
|
||||
(dolist (prop properties)
|
||||
(org-set-property (car prop) (cdr prop)))
|
||||
(opencortex-send `(:type :RESPONSE :id ,id :payload (:status :success))))
|
||||
(error
|
||||
(opencortex-send
|
||||
`(:type :RESPONSE :id ,id :payload (:status :error :message ,(error-message-string err))))))))
|
||||
(t
|
||||
(message "opencortex: Unknown action %s" action)
|
||||
(opencortex-send `(:type :RESPONSE :id ,id :payload (:status :unsupported)))))))
|
||||
|
||||
(defun opencortex--sentinel (proc event)
|
||||
"Handle network process PROC lifecycle EVENT."
|
||||
(when (string-match "finished" event)
|
||||
(setq opencortex--network-process nil)
|
||||
(message "opencortex: Connection lost.")))
|
||||
|
||||
(defun opencortex-send (plist)
|
||||
"Send a Lisp PLIST to the daemon using communication protocol framing."
|
||||
(let* ((msg (prin1-to-string plist))
|
||||
(len (length msg))
|
||||
(framed (format "%06x%s" len msg)))
|
||||
(if (and opencortex--network-process (process-live-p opencortex--network-process))
|
||||
(process-send-string opencortex--network-process framed)
|
||||
(message "opencortex (offline): %s" framed))))
|
||||
|
||||
(defun opencortex--buffer-to-sexp ()
|
||||
"Transform the current Org buffer into a pure Lisp AST (plist)."
|
||||
(opencortex--clean-element (org-element-parse-buffer)))
|
||||
|
||||
(defun opencortex--clean-element (element)
|
||||
"Recursively transform an Org ELEMENT into a pure Lisp plist."
|
||||
(cond
|
||||
((listp element)
|
||||
(let* ((type (car element))
|
||||
(props (nth 1 element))
|
||||
(children (nthcdr 2 element))
|
||||
(cleaned-props nil))
|
||||
;; Filter and transform properties
|
||||
(cl-loop for (key val) on props by 'cddr do
|
||||
(unless (member key '(:standard-properties :parent :buffer))
|
||||
(let ((json-val (cond
|
||||
((stringp val) val)
|
||||
((numberp val) val)
|
||||
((booleanp val) val)
|
||||
(t (format "%s" val)))))
|
||||
(setq cleaned-props (plist-put cleaned-props key json-val)))))
|
||||
;; Explicitly capture TODO state
|
||||
(let ((todo (org-element-property :todo-keyword element)))
|
||||
(when todo
|
||||
(setq cleaned-props (plist-put cleaned-props :TODO-STATE (format "%s" todo)))))
|
||||
(list :type type
|
||||
:properties cleaned-props
|
||||
:contents (mapcar #'opencortex--clean-element children))))
|
||||
((stringp element) element)
|
||||
(t (format "%s" element))))
|
||||
|
||||
;;; Sensors
|
||||
|
||||
(defun opencortex-notify-save ()
|
||||
"Sensor: Notify daemon with full Semantic Perception (AST) when saved."
|
||||
(when (and opencortex--network-process (derived-mode-p 'org-mode))
|
||||
(opencortex-send
|
||||
`(:type :EVENT
|
||||
:payload (:sensor :buffer-update
|
||||
:file ,(buffer-file-name)
|
||||
:state :saved
|
||||
:ast ,(opencortex--buffer-to-sexp))))))
|
||||
|
||||
(defun opencortex-notify-point ()
|
||||
"Sensor: Notify daemon of the element currently at point (Incremental Perception).
|
||||
This is much faster than parsing the entire buffer and allows for real-time
|
||||
responsiveness to the user's cursor position."
|
||||
(when (and opencortex--network-process (derived-mode-p 'org-mode))
|
||||
(let ((element (org-element-at-point)))
|
||||
(opencortex-send
|
||||
`(:type :EVENT
|
||||
:payload (:sensor :point-update
|
||||
:file ,(buffer-file-name)
|
||||
:element ,(opencortex--clean-element element)))))))
|
||||
|
||||
;;; Interaction Commands
|
||||
|
||||
(defun opencortex-set-model-cascade (cascade-string)
|
||||
"Set the ordered list of LLM providers to use as fallbacks.
|
||||
CASCADE-STRING should be a comma-separated list of keywords,
|
||||
e.g., ':gemini,:openai,:ollama'."
|
||||
(interactive "sEnter model cascade (e.g. :gemini,:openai): ")
|
||||
(unless opencortex--network-process
|
||||
(opencortex-connect))
|
||||
(let ((cascade (mapcar #'intern (split-string cascade-string ","))))
|
||||
(opencortex-send
|
||||
`(:type :REQUEST
|
||||
:id ,(truncate (float-time))
|
||||
:target :system
|
||||
:payload (:action :set-cascade :cascade ,cascade)))
|
||||
(message "opencortex: Requesting model cascade update to %s" cascade)))
|
||||
(defgroup opencortex-faces nil
|
||||
"Faces for the opencortex chat interface."
|
||||
:group 'opencortex)
|
||||
|
||||
(defface opencortex-user-face
|
||||
'((((class color) (background dark)) :foreground "LightSkyBlue" :weight bold)
|
||||
(((class color) (background light)) :foreground "blue" :weight bold)
|
||||
(t :weight bold :underline t))
|
||||
"Face for user messages in chat history."
|
||||
:group 'opencortex-faces)
|
||||
|
||||
(defface opencortex-system-face
|
||||
'((t :slant italic :foreground "gray50"))
|
||||
"Face for system and reasoning logs."
|
||||
:group 'opencortex-faces)
|
||||
|
||||
(defun opencortex-chat ()
|
||||
"Modern chat interface for the opencortex kernel.
|
||||
Opens a history buffer and a dedicated input area."
|
||||
(interactive)
|
||||
(let ((chat-buf (get-buffer-create "*opencortex-chat*"))
|
||||
(input-buf (get-buffer-create "*opencortex-input*")))
|
||||
;; History Buffer Setup
|
||||
(with-current-buffer chat-buf
|
||||
(unless (eq major-mode 'special-mode)
|
||||
(special-mode)
|
||||
(let ((inhibit-read-only t))
|
||||
(erase-buffer)
|
||||
(insert "--- opencortex History ---\n\n"))))
|
||||
|
||||
;; Input Buffer Setup
|
||||
(with-current-buffer input-buf
|
||||
(unless (eq major-mode 'org-mode)
|
||||
(org-mode)
|
||||
(local-set-key (kbd "C-c C-c") #'opencortex-chat-send)
|
||||
(local-set-key (kbd "C-c C-k") #'opencortex-interrupt))
|
||||
(let ((inhibit-read-only t))
|
||||
(delete-region (point-min) (point-max))
|
||||
(insert "# Type your message and press C-c C-c to send.\n")))
|
||||
|
||||
;; Layout: Chat History (Top), Input Area (Bottom)
|
||||
(delete-other-windows)
|
||||
(switch-to-buffer chat-buf)
|
||||
(let ((win (split-window-below -6))) ; 6 lines for input
|
||||
(set-window-buffer win input-buf)
|
||||
(select-window win))))
|
||||
(defun opencortex-interrupt ()
|
||||
"Interrupt the opencortex reasoning loop."
|
||||
(interactive)
|
||||
(unless opencortex--network-process
|
||||
(opencortex-connect))
|
||||
(opencortex-send
|
||||
`(:type :EVENT
|
||||
:payload (:sensor :interrupt)))
|
||||
(message "opencortex: Interrupt signal sent."))
|
||||
|
||||
(defun opencortex--insert-to-history (text &optional face)
|
||||
"Insert TEXT into the chat history buffer with optional FACE and scroll."
|
||||
(let ((buf (get-buffer-create "*opencortex-chat*")))
|
||||
(with-current-buffer buf
|
||||
(let ((inhibit-read-only t))
|
||||
(save-excursion
|
||||
(goto-char (point-max))
|
||||
(insert (if face (propertize text 'face face) text)))
|
||||
;; Force scroll in all windows showing this buffer
|
||||
(walk-windows
|
||||
(lambda (w)
|
||||
(when (eq (window-buffer w) buf)
|
||||
(set-window-point w (point-max))))
|
||||
nil t)))))
|
||||
|
||||
(defun opencortex-chat-send ()
|
||||
"Send the current chat buffer content to the agent."
|
||||
(interactive)
|
||||
(unless opencortex--network-process
|
||||
(opencortex-connect))
|
||||
(let* ((text (buffer-substring-no-properties (point-min) (point-max)))
|
||||
(clean-text (string-trim (replace-regexp-in-string "^#.*\n" "" text))))
|
||||
(when (> (length clean-text) 0)
|
||||
;; Append to history with styling
|
||||
(opencortex--insert-to-history (concat "YOU: " clean-text "\n\n") 'opencortex-user-face)
|
||||
|
||||
;; Clear input buffer
|
||||
(let ((inhibit-read-only t))
|
||||
(delete-region (point-min) (point-max))
|
||||
(insert "# Type your message and press C-c C-c to send.\n"))
|
||||
|
||||
;; Send to daemon
|
||||
(opencortex-send
|
||||
`(:type :EVENT
|
||||
:meta (:source :emacs)
|
||||
:payload (:sensor :user-input
|
||||
:text ,clean-text)))
|
||||
(message "opencortex: Message sent."))))
|
||||
|
||||
(defun opencortex-auth-google (code)
|
||||
"Submit the Google OAuth authorization CODE to the daemon."
|
||||
(interactive "sEnter Google Authorization Code: ")
|
||||
(unless opencortex--network-process
|
||||
(opencortex-connect))
|
||||
(opencortex-send
|
||||
`(:type :REQUEST
|
||||
:id ,(truncate (float-time))
|
||||
:target :system
|
||||
:payload (:action :auth-google-code :code ,code)))
|
||||
(message "opencortex: Authorization code sent to daemon."))
|
||||
|
||||
(defun opencortex-organize-subtree ()
|
||||
...
|
||||
"Command: Ask the agent to organize the current Org subtree."
|
||||
(interactive)
|
||||
(opencortex-run-command :organize-subtree))
|
||||
|
||||
(defun opencortex-summarize-buffer ()
|
||||
"Command: Ask the agent to summarize the current buffer."
|
||||
(interactive)
|
||||
(opencortex-run-command :summarize-buffer))
|
||||
|
||||
(defun opencortex-run-command (command-type)
|
||||
"Generic runner for high-level COMMAND-TYPE."
|
||||
(unless opencortex--network-process
|
||||
(opencortex-connect))
|
||||
(let ((ast (opencortex--buffer-to-sexp)))
|
||||
(opencortex-send
|
||||
`(:type :EVENT
|
||||
:payload (:sensor :user-command
|
||||
:command ,command-type
|
||||
:file ,(buffer-file-name)
|
||||
:ast ,ast)))
|
||||
(message "opencortex: Requesting '%s'..." command-type)))
|
||||
|
||||
;;;###autoload
|
||||
(define-minor-mode opencortex-mode
|
||||
"Global minor mode for the opencortex Probabilistic-Deterministic kernel.
|
||||
When enabled, this mode starts the Lisp daemon (if configured)
|
||||
and establishes the network connection to enable proactive
|
||||
Org-mode sensing."
|
||||
:global t
|
||||
:group 'opencortex
|
||||
(if opencortex-mode
|
||||
(progn
|
||||
(add-hook 'after-save-hook #'opencortex-notify-save)
|
||||
(add-hook 'post-command-hook #'opencortex-notify-point)
|
||||
(add-hook 'kill-emacs-hook #'opencortex-disconnect)
|
||||
(opencortex-connect))
|
||||
(remove-hook 'after-save-hook #'opencortex-notify-save)
|
||||
(remove-hook 'post-command-hook #'opencortex-notify-point)
|
||||
(remove-hook 'kill-emacs-hook #'opencortex-disconnect)
|
||||
(opencortex-disconnect)))
|
||||
|
||||
(provide 'opencortex)
|
||||
;;; opencortex.el ends here
|
||||
189
library/package.lisp
Normal file
189
library/package.lisp
Normal file
@@ -0,0 +1,189 @@
|
||||
(defpackage :opencortex
|
||||
(:use :cl)
|
||||
(:export
|
||||
;; --- Communication Protocol ---
|
||||
#:frame-message
|
||||
#:read-framed-message
|
||||
#:PROTO-GET
|
||||
#:LIST-OBJECTS-WITH-ATTRIBUTE
|
||||
#:COSINE-SIMILARITY
|
||||
#:VAULT-MASK-STRING
|
||||
#:*VAULT-MEMORY*
|
||||
#:parse-message
|
||||
#:make-hello-message
|
||||
#:validate-communication-protocol-schema
|
||||
|
||||
;; --- Daemon Lifecycle ---
|
||||
#:start-daemon
|
||||
#:stop-daemon
|
||||
#:harness-log
|
||||
#:main
|
||||
|
||||
;; --- Memory (CLOSOS) ---
|
||||
#:ingest-ast
|
||||
#:lookup-object
|
||||
#:list-objects-by-type
|
||||
#:org-id-new
|
||||
#:*memory*
|
||||
#:*history-store*
|
||||
#:org-object
|
||||
#:make-org-object
|
||||
#:org-object-id
|
||||
#:org-object-type
|
||||
#:org-object-attributes
|
||||
#:org-object-parent-id
|
||||
#:org-object-children
|
||||
#:org-object-version
|
||||
#:org-object-last-sync
|
||||
#:org-object-vector
|
||||
#:org-object-content
|
||||
#:org-object-hash
|
||||
#:snapshot-memory
|
||||
#:rollback-memory
|
||||
|
||||
;; --- Context API (Peripheral Vision) ---
|
||||
#:context-query-store
|
||||
#:context-get-active-projects
|
||||
#:context-get-recent-completed-tasks
|
||||
#:context-list-all-skills
|
||||
#:context-get-skill-source
|
||||
#:context-get-system-logs
|
||||
#:context-resolve-path
|
||||
#:context-get-skill-telemetry
|
||||
#:harness-track-telemetry
|
||||
#:context-assemble-global-awareness
|
||||
|
||||
;; --- Reactive Signal Pipeline ---
|
||||
#:process-signal
|
||||
#:perceive-gate
|
||||
#:probabilistic-gate
|
||||
#:consensus-gate
|
||||
#:act-gate
|
||||
#:reason-gate
|
||||
#:perceive-gate
|
||||
#:dispatch-gate
|
||||
#:inject-stimulus
|
||||
#:initialize-actuators
|
||||
#:dispatch-action
|
||||
#:register-actuator
|
||||
|
||||
;; --- Skill Engine ---
|
||||
#:load-skill-from-org
|
||||
#:initialize-all-skills
|
||||
#:load-skill-with-timeout
|
||||
#:topological-sort-skills
|
||||
#:validate-lisp-syntax
|
||||
#:defskill
|
||||
#:*skills-registry*
|
||||
#:skill
|
||||
#:skill-name
|
||||
#:skill-priority
|
||||
#:skill-dependencies
|
||||
#:skill-trigger-fn
|
||||
#:skill-probabilistic-prompt
|
||||
#:skill-deterministic-fn
|
||||
|
||||
;; --- Tool Registry ---
|
||||
#:def-cognitive-tool
|
||||
#:*cognitive-tools*
|
||||
#:cognitive-tool
|
||||
#:cognitive-tool-name
|
||||
#:cognitive-tool-description
|
||||
#:cognitive-tool-parameters
|
||||
#:cognitive-tool-guard
|
||||
#:cognitive-tool-body
|
||||
|
||||
;; --- Emacs Client Registry ---
|
||||
#:*emacs-clients*
|
||||
#:*clients-lock*
|
||||
#:register-emacs-client
|
||||
#:unregister-emacs-client
|
||||
|
||||
;; --- Probabilistic Engine ---
|
||||
#:ask-probabilistic
|
||||
#:register-probabilistic-backend
|
||||
#:distill-prompt
|
||||
#:*provider-cascade*
|
||||
|
||||
;; --- Security Vault ---
|
||||
#:vault-get-secret
|
||||
#:vault-set-secret
|
||||
|
||||
;; --- Deterministic Logic ---
|
||||
#:list-objects-with-attribute
|
||||
#:deterministic-verify
|
||||
|
||||
;; --- AST Helpers ---
|
||||
#:find-headline-missing-id))
|
||||
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *system-logs* nil
|
||||
"Thread-safe list of the most recent system messages.")
|
||||
|
||||
(defvar *logs-lock* (bt:make-lock "harness-logs-lock")
|
||||
"Protects the circular log buffer from race conditions during concurrent skill execution.")
|
||||
|
||||
(defvar *max-log-history* 100
|
||||
"The maximum number of entries to preserve in the in-memory log buffer.")
|
||||
|
||||
(defvar *skills-registry* (make-hash-table :test 'equal)
|
||||
"Global registry of all loaded skills, keyed by their unique identifier.")
|
||||
|
||||
(defvar *skill-telemetry* (make-hash-table :test 'equal)
|
||||
"Stores execution duration and failure counts for every registered skill.")
|
||||
|
||||
(defvar *telemetry-lock* (bt:make-lock "harness-telemetry-lock")
|
||||
"Protects the telemetry store from concurrent updates.")
|
||||
|
||||
(defun proto-get (plist key)
|
||||
"Robustly retrieves a value from a plist, checking both uppercase and lowercase keyword versions."
|
||||
(let* ((s (string key))
|
||||
(up (intern (string-upcase s) :keyword))
|
||||
(dn (intern (string-downcase s) :keyword)))
|
||||
(or (getf plist up) (getf plist dn))))
|
||||
|
||||
(defun harness-track-telemetry (skill-name duration status)
|
||||
"Updates performance metrics for a specific skill. Status should be :success or :rejected."
|
||||
(when skill-name
|
||||
(bt:with-lock-held (*telemetry-lock*)
|
||||
(let ((entry (or (gethash skill-name *skill-telemetry*) (list :executions 0 :total-time 0 :failures 0))))
|
||||
(incf (getf entry :executions))
|
||||
(incf (getf entry :total-time) duration)
|
||||
(when (eq status :rejected) (incf (getf entry :failures)))
|
||||
(setf (gethash skill-name *skill-telemetry*) entry)))))
|
||||
|
||||
(defvar *cognitive-tools* (make-hash-table :test 'equal)
|
||||
"The active set of physical capabilities available to the agent.")
|
||||
|
||||
(defstruct cognitive-tool
|
||||
"Represents a physical or virtual capability with explicit documentation and security guards."
|
||||
name
|
||||
description
|
||||
parameters
|
||||
guard
|
||||
body)
|
||||
|
||||
(defmacro def-cognitive-tool (name description parameters &key guard body)
|
||||
"Registers a new cognitive tool.
|
||||
NAME: Keyword identifier.
|
||||
DESCRIPTION: Human-readable intent (used in LLM prompts).
|
||||
PARAMETERS: List of property lists defining arguments.
|
||||
GUARD: (context -> boolean) function to prevent unsafe calls.
|
||||
BODY: The actual Lisp execution logic."
|
||||
`(setf (gethash (string-downcase (string ',name)) *cognitive-tools*)
|
||||
(make-cognitive-tool :name (string-downcase (string ',name))
|
||||
:description ,description
|
||||
:parameters ',parameters
|
||||
:guard ,guard
|
||||
:body ,body)))
|
||||
|
||||
(defun harness-log (msg &rest args)
|
||||
"Centralized logging for the harness. Writes to STDOUT and the thread-safe circular buffer."
|
||||
(let ((formatted-msg (apply #'format nil msg args)))
|
||||
(bt:with-lock-held (*logs-lock*)
|
||||
(push formatted-msg *system-logs*)
|
||||
(when (> (length *system-logs*) *max-log-history*)
|
||||
(setq *system-logs* (subseq *system-logs* 0 *max-log-history*))))
|
||||
(format t "~a~%" formatted-msg)
|
||||
(finish-output)))
|
||||
60
library/perceive.lisp
Normal file
60
library/perceive.lisp
Normal file
@@ -0,0 +1,60 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *async-sensors* '(:chat-message :delegation :user-command)
|
||||
"List of sensors that should be processed asynchronously to avoid blocking gateways.")
|
||||
|
||||
(defvar *foveal-focus-id* nil
|
||||
"The Org ID of the node the user is currently interacting with.")
|
||||
|
||||
(defun inject-stimulus (raw-message &key stream (depth 0))
|
||||
"Enqueues a raw message into the reactive signal pipeline."
|
||||
(let* ((payload (getf raw-message :payload))
|
||||
(sensor (getf payload :sensor))
|
||||
(meta (getf raw-message :meta))
|
||||
(async-p (or (getf payload :async-p) (member sensor *async-sensors*))))
|
||||
|
||||
;; Ensure META exists and contains the stream if provided
|
||||
(unless meta (setf meta (list :SOURCE :SYSTEM :SESSION-ID "internal")))
|
||||
(when stream (setf (getf meta :reply-stream) stream))
|
||||
(setf (getf raw-message :meta) meta)
|
||||
|
||||
(if async-p
|
||||
(bt:make-thread
|
||||
(lambda ()
|
||||
(restart-case (handler-bind ((error (lambda (c) (harness-log "ASYNC ERROR: ~a" c) (invoke-restart 'skip-event))))
|
||||
(process-signal raw-message))
|
||||
(skip-event () nil)))
|
||||
:name "opencortex-async-task")
|
||||
(restart-case (handler-bind ((error (lambda (c) (harness-log "SYSTEM ERROR: ~a" c) (invoke-restart 'skip-event))))
|
||||
(process-signal raw-message))
|
||||
(skip-event () (harness-log "SYSTEM RECOVERY: Stimulus dropped.~%"))))))
|
||||
|
||||
(defun perceive-gate (signal)
|
||||
"Initial processing: Normalizes raw stimuli and updates memory."
|
||||
(let* ((payload (getf signal :payload))
|
||||
(type (getf signal :type))
|
||||
(meta (getf signal :meta))
|
||||
(sensor (getf payload :sensor)))
|
||||
(harness-log "GATE [Perceive]: ~a (~a) [Source: ~s]" type (or sensor "no-sensor") (getf meta :source))
|
||||
|
||||
(cond ((eq type :EVENT)
|
||||
(case sensor
|
||||
(:buffer-update
|
||||
(let ((ast (getf payload :ast)))
|
||||
(when ast
|
||||
(snapshot-memory)
|
||||
(ingest-ast ast))))
|
||||
(:point-update
|
||||
(let ((element (getf payload :element)))
|
||||
(when element
|
||||
(snapshot-memory)
|
||||
(setf *foveal-focus-id* (ignore-errors (getf element :id)))
|
||||
(ingest-ast element))))
|
||||
(:interrupt
|
||||
(bt:with-lock-held (*interrupt-lock*) (setf *interrupt-flag* t)))))
|
||||
((eq type :RESPONSE)
|
||||
(harness-log "GATE [Perceive]: Act Result -> ~a" (getf payload :status))))
|
||||
|
||||
(setf (getf signal :status) :perceived)
|
||||
(setf (getf signal :foveal-focus) *foveal-focus-id*)
|
||||
signal))
|
||||
25
library/policy.lisp
Normal file
25
library/policy.lisp
Normal file
@@ -0,0 +1,25 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defun policy-check-autonomy (action context)
|
||||
"Ensures the action does not violate the Autonomy invariant."
|
||||
(declare (ignore context))
|
||||
;; Implementation placeholder: currently permits all actions.
|
||||
;; Future: Scan for non-autonomous domain names or proprietary API endpoints.
|
||||
action)
|
||||
|
||||
(defun policy-deterministic-gate (action context)
|
||||
"The main policy gate. Sub-calls engineering standards if available."
|
||||
(let ((current-action (policy-check-autonomy action context)))
|
||||
(when current-action
|
||||
(let ((eng-pkg (find-package :opencortex.skills.org-skill-engineering-standards)))
|
||||
(when eng-pkg
|
||||
(let ((eng-gate (find-symbol "ENGINEERING-STANDARDS-GATE" eng-pkg)))
|
||||
(when (and eng-gate (fboundp eng-gate))
|
||||
(setf current-action (funcall (symbol-function eng-gate) current-action context)))))))
|
||||
current-action))
|
||||
|
||||
(defskill :skill-policy
|
||||
:priority 100
|
||||
:trigger (lambda (ctx) t)
|
||||
:probabilistic nil
|
||||
:deterministic #'policy-deterministic-gate)
|
||||
123
library/probabilistic.lisp
Normal file
123
library/probabilistic.lisp
Normal file
@@ -0,0 +1,123 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *probabilistic-backends* (make-hash-table :test 'equal))
|
||||
|
||||
(defvar *provider-cascade* nil)
|
||||
|
||||
(defun register-probabilistic-backend (name fn) (setf (gethash name *probabilistic-backends*) fn))
|
||||
|
||||
(defvar *model-selector-fn* nil "A function called with (provider context) to return a model ID.")
|
||||
|
||||
(defvar *consensus-enabled-p* nil "If T, ask-probabilistic queries all backends in parallel.")
|
||||
|
||||
(defun ask-probabilistic (prompt &key (system-prompt "You are the Probabilistic engine of a Probabilistic-Deterministic Lisp Machine.") (cascade nil) (context nil))
|
||||
"Dispatches a neural request through the provider cascade or parallel consensus."
|
||||
(let ((backends (cond
|
||||
((and cascade (listp cascade)) cascade)
|
||||
((functionp cascade) (funcall cascade context))
|
||||
(t *provider-cascade*))))
|
||||
(if *consensus-enabled-p*
|
||||
;; PARALLEL CONSENSUS MODE
|
||||
(let ((results nil)
|
||||
(threads nil)
|
||||
(lock (bt:make-lock)))
|
||||
(dolist (backend backends)
|
||||
(let ((backend-fn (gethash backend *probabilistic-backends*)))
|
||||
(when backend-fn
|
||||
(push (bt:make-thread
|
||||
(lambda ()
|
||||
(harness-log "PROBABILISTIC [Consensus]: Querying backend ~a..." backend)
|
||||
(let* ((model (when *model-selector-fn* (funcall *model-selector-fn* backend context)))
|
||||
(result (ignore-errors
|
||||
(if model
|
||||
(funcall backend-fn prompt system-prompt :model model)
|
||||
(funcall backend-fn prompt system-prompt)))))
|
||||
(bt:with-lock-held (lock)
|
||||
(push result results)))))
|
||||
threads))))
|
||||
;; Wait for all threads with a timeout (e.g., 30s)
|
||||
(let ((start-time (get-universal-time)))
|
||||
(loop while (and (< (length results) (length threads))
|
||||
(< (- (get-universal-time) start-time) 30))
|
||||
do (sleep 0.1)))
|
||||
;; Return the list of raw results (filtering out nils or errors)
|
||||
(let ((valid-results (remove-if-not #'stringp results)))
|
||||
(if valid-results
|
||||
(format nil "~{~a~^|CONSENSUS-SEP|~}" valid-results)
|
||||
"(:type :LOG :payload (:text \"Neural Consensus Failure\"))")))
|
||||
|
||||
;; SEQUENTIAL CASCADE MODE
|
||||
(or (dolist (backend backends)
|
||||
(let ((backend-fn (gethash backend *probabilistic-backends*)))
|
||||
(when backend-fn
|
||||
(harness-log "PROBABILISTIC: Attempting backend ~a..." backend)
|
||||
(let* ((model (when *model-selector-fn* (funcall *model-selector-fn* backend context)))
|
||||
(result (if model
|
||||
(funcall backend-fn prompt system-prompt :model model)
|
||||
(funcall backend-fn prompt system-prompt))))
|
||||
(unless (or (null result)
|
||||
(and (stringp result) (search ":LOG" result) (or (search "Failure" result) (search "missing" result))))
|
||||
(return result))))))
|
||||
"(:type :LOG :payload (:text \"Neural Cascade Failure\"))"))))
|
||||
|
||||
(defun think (context)
|
||||
"Invokes the neural Probabilistic engine to propose a Lisp action based on context."
|
||||
(let ((active-skill (find-triggered-skill context))
|
||||
(tool-belt (generate-tool-belt-prompt))
|
||||
(global-context (context-assemble-global-awareness)))
|
||||
(if active-skill
|
||||
(progn
|
||||
(harness-log "PROBABILISTIC: Engaging skill '~a'~%" (skill-name active-skill))
|
||||
(let* ((prompt-generator (skill-probabilistic-prompt active-skill))
|
||||
(raw-prompt (when prompt-generator (funcall prompt-generator context)))
|
||||
(full-system-prompt (concatenate 'string
|
||||
"ACTUATOR IDENTITY: You are the pure Lisp actuator for the opencortex kernel.
|
||||
MANDATE: Output EXACTLY ONE Common Lisp property list starting with (:type :REQUEST).
|
||||
ZERO CONVERSATION: Do not explain. Do not say 'Okay'. Do not use markdown blocks.
|
||||
STRICT RULE: Do not output multiple lists. Do not chain multiple requests.
|
||||
DO NOT embed tool calls inside text strings.
|
||||
|
||||
"
|
||||
global-context
|
||||
"
|
||||
"
|
||||
tool-belt
|
||||
"
|
||||
IMPORTANT: To reply to the user, you MUST use:
|
||||
(:type :REQUEST :target :emacs :action :insert-at-end :buffer \"*opencortex-chat*\" :text \"* <Response Text>\")
|
||||
|
||||
To call a tool, you MUST use:
|
||||
(:type :REQUEST :target :tool :action :call :tool \"<name>\" :args (:arg1 \"val\"))
|
||||
|
||||
")))
|
||||
(if (and raw-prompt (> (length raw-prompt) 1))
|
||||
(let* ((thought (ask-probabilistic raw-prompt :system-prompt full-system-prompt :context context))
|
||||
(raw-thoughts (cl-ppcre:split (cl-ppcre:quote-meta-chars "|CONSENSUS-SEP|") thought))
|
||||
(suggestions nil))
|
||||
(dolist (raw-thought raw-thoughts)
|
||||
(harness-log "PROBABILISTIC RAW: ~a~%" raw-thought)
|
||||
(let* ((cleaned-thought
|
||||
(let ((match (cl-ppcre:scan-to-strings "(?s)```(?:lisp)?\\n?(.*?)\\n?```" raw-thought)))
|
||||
(if match
|
||||
(let ((regs (nth-value 1 (cl-ppcre:scan-to-strings "(?s)```(?:lisp)?\\n?(.*?)\\n?```" raw-thought))))
|
||||
(if (and regs (> (length regs) 0)) (elt regs 0) raw-thought))
|
||||
(string-trim '(#\Space #\Newline #\Tab) raw-thought))))
|
||||
(suggestion (handler-case (read-from-string cleaned-thought)
|
||||
(error (c)
|
||||
;; EMIT ASYNCHRONOUS REPAIR STIMULUS
|
||||
(list :type :EVENT :payload
|
||||
(list :sensor :syntax-error
|
||||
:code cleaned-thought
|
||||
:error (format nil "~a" c)))))))
|
||||
(harness-log "PROBABILISTIC Suggestion: ~a~%" cleaned-thought)
|
||||
(when (and suggestion (listp suggestion))
|
||||
(push suggestion suggestions))))
|
||||
(if (and *consensus-enabled-p* suggestions)
|
||||
(nreverse suggestions)
|
||||
(first (nreverse suggestions))))
|
||||
'(:type :LOG :payload (:text "Skill triggered (Deterministic only)")))))
|
||||
nil)))
|
||||
|
||||
(defun distill-prompt (full-prompt successful-output)
|
||||
(let ((system-instr "You are a Meta-Cognitive Prompt Architect. DISTILL into template."))
|
||||
(ask-probabilistic (format nil "PROMPT: ~a~%RESULT: ~a" full-prompt successful-output) :system-prompt system-instr)))
|
||||
124
library/reason.lisp
Normal file
124
library/reason.lisp
Normal file
@@ -0,0 +1,124 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defvar *probabilistic-backends* (make-hash-table :test 'equal)
|
||||
"A global mapping of provider identifiers (keywords) to their respective execution functions.")
|
||||
|
||||
(defvar *provider-cascade* nil
|
||||
"An ordered list of providers to attempt if the primary one fails.")
|
||||
|
||||
(defvar *model-selector-fn* nil
|
||||
"A hook for dynamic model selection based on context complexity.")
|
||||
|
||||
(defvar *consensus-enabled-p* nil
|
||||
"Flag to enable parallel multi-model voting (not implemented in MVP).")
|
||||
|
||||
(defun register-probabilistic-backend (name fn)
|
||||
"Registers a neural provider with its calling function."
|
||||
(setf (gethash name *probabilistic-backends*) fn))
|
||||
|
||||
(defun probabilistic-call (prompt &key (system-prompt "You are the Probabilistic engine.") (cascade nil) (context nil))
|
||||
"Dispatches a neural request through the provider cascade. Returns a Lisp plist or a failure log."
|
||||
(let ((backends (or cascade *provider-cascade*)))
|
||||
(or (dolist (backend backends)
|
||||
(let ((backend-fn (gethash backend *probabilistic-backends*)))
|
||||
(when backend-fn
|
||||
(harness-log "PROBABILISTIC: Attempting backend ~a..." backend)
|
||||
(let* ((model (when *model-selector-fn* (funcall *model-selector-fn* backend context)))
|
||||
(result (if model
|
||||
(funcall backend-fn prompt system-prompt :model model)
|
||||
(funcall backend-fn prompt system-prompt))))
|
||||
(cond ((and (listp result) (eq (getf result :status) :success))
|
||||
(return (getf result :content)))
|
||||
((stringp result) (return result))
|
||||
(t (harness-log "PROBABILISTIC: Backend ~a failed: ~a" backend (getf result :message))))))))
|
||||
(list :type :LOG :payload (list :text "Neural Cascade Failure: All providers exhausted.")))))
|
||||
|
||||
(defun strip-markdown (text)
|
||||
"Strips common markdown code block markers from text to ensure valid S-expression parsing."
|
||||
(if (and text (stringp text))
|
||||
(let ((cleaned text))
|
||||
(setf cleaned (cl-ppcre:regex-replace-all "^```[a-z]*\\n" cleaned ""))
|
||||
(setf cleaned (cl-ppcre:regex-replace-all "\\n```$" cleaned ""))
|
||||
(setf cleaned (cl-ppcre:regex-replace-all "```" cleaned ""))
|
||||
(string-trim '(#\Space #\Newline #\Tab) cleaned))
|
||||
text))
|
||||
|
||||
(defun think (context)
|
||||
"Generates a Lisp action proposal based on current context."
|
||||
(let* ((active-skill (find-triggered-skill context))
|
||||
(tool-belt (generate-tool-belt-prompt))
|
||||
(global-context (context-assemble-global-awareness))
|
||||
(system-logs (context-get-system-logs))
|
||||
(assistant-name (or (uiop:getenv "MEMEX_ASSISTANT") "Agent")))
|
||||
(let* ((prompt-generator (when active-skill (skill-probabilistic-prompt active-skill)))
|
||||
(raw-prompt (if prompt-generator
|
||||
(funcall prompt-generator context)
|
||||
(let ((p (proto-get (proto-get context :payload) :text)))
|
||||
(if (and p (stringp p)) p "Maintain metabolic stasis."))))
|
||||
(system-prompt (format nil "IDENTITY: ~a. MANDATE: Respond with ONE Lisp plist. ~a ~a RECENT_LOGS: ~a
|
||||
IMPORTANT: To reply to the user, you MUST use:
|
||||
(:TYPE :REQUEST :PAYLOAD (:ACTION :MESSAGE :TEXT \"<Response Text>\"))
|
||||
|
||||
To call a tool, you MUST use:
|
||||
(:TYPE :REQUEST :TARGET :TOOL :ACTION :CALL :TOOL \"<name>\" :ARGS (:arg1 \"val\"))
|
||||
|
||||
PROVIDER RULE: Always use the default cascade provider unless a specific model or capability is required for the task."
|
||||
assistant-name global-context tool-belt system-logs)))
|
||||
(let* ((thought (probabilistic-call raw-prompt :system-prompt system-prompt :context context))
|
||||
(cleaned (strip-markdown thought))
|
||||
(meta (proto-get context :meta))
|
||||
(source (proto-get meta :source)))
|
||||
(if (and cleaned (stringp cleaned))
|
||||
(let ((*read-eval* nil))
|
||||
(if (and (> (length cleaned) 0) (char= (char cleaned 0) #\())
|
||||
(handler-case
|
||||
(let ((parsed (read-from-string cleaned)))
|
||||
(let ((type (proto-get parsed :TYPE))
|
||||
(target (or (proto-get parsed :TARGET) (proto-get parsed :target))))
|
||||
(cond ((member type '(:REQUEST :EVENT :STATUS :RESPONSE))
|
||||
(unless (proto-get parsed :target) (setf (getf parsed :target) (or source :CLI)))
|
||||
parsed)
|
||||
;; Handle raw plists or lists of plists that look like tool calls or data
|
||||
((or (eq target :TOOL) (eq target :tool) (getf parsed :TOOL) (getf parsed :tool)
|
||||
(and (listp parsed) (listp (car parsed)) (keywordp (caar parsed))))
|
||||
(list :TYPE :REQUEST :TARGET :TOOL :PAYLOAD parsed))
|
||||
(t (list :TYPE :REQUEST :TARGET (or source :CLI) :PAYLOAD (list :ACTION :MESSAGE :TEXT cleaned))))))
|
||||
(error (c) (list :TYPE :REQUEST :TARGET (or source :CLI) :PAYLOAD (list :ACTION :MESSAGE :TEXT cleaned))))
|
||||
(list :TYPE :REQUEST :TARGET (or source :CLI) :PAYLOAD (list :ACTION :MESSAGE :TEXT cleaned))))
|
||||
thought)))))
|
||||
|
||||
(defun deterministic-verify (proposed-action context)
|
||||
"Iterates through all skill deterministic-gates sorted by priority. Ensures absolute safety of the neural proposal."
|
||||
(let ((current-action proposed-action)
|
||||
(skills nil))
|
||||
(maphash (lambda (name skill) (declare (ignore name)) (when (skill-deterministic-fn skill) (push skill skills))) *skills-registry*)
|
||||
(setf skills (sort skills #'> :key #'skill-priority))
|
||||
(dolist (skill skills)
|
||||
(let ((trigger (skill-trigger-fn skill))
|
||||
(gate (skill-deterministic-fn skill)))
|
||||
(when (or (null trigger) (ignore-errors (funcall trigger context)))
|
||||
(let ((next-action (funcall gate current-action context)))
|
||||
(let ((original-type (proto-get current-action :type)))
|
||||
(when (and (listp next-action)
|
||||
(member (proto-get next-action :type) '(:LOG :EVENT :log :event))
|
||||
(or (not (member original-type '(:LOG :EVENT :log :event)))
|
||||
(not (eq next-action current-action))))
|
||||
(harness-log "DETERMINISTIC: Intercepted by skill '~a'" (skill-name skill))
|
||||
(return-from deterministic-verify next-action)))
|
||||
(setf current-action next-action)))))
|
||||
current-action))
|
||||
|
||||
(defun reason-gate (signal)
|
||||
"Unified Stage: Combines Probabilistic proposals and Deterministic verification."
|
||||
(let* ((type (proto-get signal :type))
|
||||
(payload (proto-get signal :payload))
|
||||
(sensor (proto-get payload :sensor)))
|
||||
;; Optimization: Only reason about user input or chat messages.
|
||||
(unless (and (eq type :EVENT) (member sensor '(:user-input :chat-message)))
|
||||
(return-from reason-gate signal))
|
||||
(let ((candidate (think signal)))
|
||||
(if candidate
|
||||
(setf (getf signal :approved-action) (deterministic-verify candidate signal))
|
||||
(setf (getf signal :approved-action) nil))
|
||||
(setf (getf signal :status) :reasoned)
|
||||
signal)))
|
||||
79
library/skills.lisp
Normal file
79
library/skills.lisp
Normal file
@@ -0,0 +1,79 @@
|
||||
(in-package :opencortex)
|
||||
|
||||
(defstruct skill
|
||||
"Represents a hot-reloadable module of intelligence or actuation."
|
||||
name
|
||||
priority
|
||||
dependencies
|
||||
trigger-fn
|
||||
probabilistic-prompt
|
||||
deterministic-fn)
|
||||
|
||||
(defmacro defskill (name &key (priority 0) dependencies trigger probabilistic deterministic)
|
||||
"Registers a new skill into the global harness registry."
|
||||
`(setf (gethash (string-downcase (string ',name)) *skills-registry*)
|
||||
(make-skill :name (string-downcase (string ',name))
|
||||
:priority ,priority
|
||||
:dependencies ,dependencies
|
||||
:trigger-fn ,trigger
|
||||
:probabilistic-prompt ,probabilistic
|
||||
:deterministic-fn ,deterministic)))
|
||||
|
||||
(defun validate-lisp-syntax (file-path)
|
||||
"Parses a Lisp file without evaluation to verify syntactic integrity."
|
||||
(handler-case
|
||||
(with-open-file (stream file-path)
|
||||
(loop for form = (read stream nil :eof)
|
||||
until (eq form :eof))
|
||||
t)
|
||||
(error (c)
|
||||
(harness-log "SYNTAX ERROR in ~a: ~a" file-path c)
|
||||
nil)))
|
||||
|
||||
(defun load-skill-from-org (org-file-path)
|
||||
"Tangles and loads a single Org-mode skill file."
|
||||
(let* ((filename (file-name-nondirectory (namestring org-file-path)))
|
||||
(skill-id (pathname-name org-file-path))
|
||||
(lisp-file (merge-pathnames (concatenate 'string "library/gen/" skill-id ".lisp")
|
||||
(asdf:system-source-directory :opencortex))))
|
||||
|
||||
(ensure-directories-exist lisp-file)
|
||||
(harness-log "LOADER: Loading ~a..." skill-id)
|
||||
|
||||
;; 1. Tangle the Org file into Lisp
|
||||
(uiop:run-program (list "emacs" "--batch" "--eval" "(require 'org)"
|
||||
"--eval" (format nil "(org-babel-tangle-file \"~a\")" org-file-path))
|
||||
:output t)
|
||||
|
||||
;; 2. Verify and Load
|
||||
(if (validate-lisp-syntax lisp-file)
|
||||
(progn
|
||||
(handler-case (load lisp-file)
|
||||
(error (c) (harness-log "LOADER ERROR in skill '~a': ~a" skill-id c)))
|
||||
t)
|
||||
nil)))
|
||||
|
||||
(defun topological-sort-skills (skills)
|
||||
"Calculates the correct loading order based on #+DEPENDS_ON metadata."
|
||||
;; Placeholder: Currently sorts by priority as a proxy for dependencies.
|
||||
(sort skills #'> :key #'skill-priority))
|
||||
|
||||
(defun initialize-all-skills ()
|
||||
"Discovers and loads all Org files in the SKILLS_DIR."
|
||||
(let* ((skills-dir (uiop:getenv "SKILLS_DIR"))
|
||||
(files (when (and skills-dir (uiop:directory-exists-p skills-dir))
|
||||
(uiop:directory-files skills-dir "*.org"))))
|
||||
(dolist (f files)
|
||||
(load-skill-from-org f))
|
||||
(harness-log "LOADER: Boot Complete. [Ready: ~a] [Failed: 0]" (hash-table-count *skills-registry*))))
|
||||
|
||||
(defun find-triggered-skill (context)
|
||||
"Iterates through the registry and returns the first skill whose trigger returns true."
|
||||
(let ((skills nil))
|
||||
(maphash (lambda (name skill) (declare (ignore name)) (push skill skills)) *skills-registry*)
|
||||
(setf skills (sort skills #'> :key #'skill-priority))
|
||||
(dolist (s skills)
|
||||
(let ((trigger (skill-trigger-fn s)))
|
||||
(when (and trigger (funcall trigger context))
|
||||
(return-from find-triggered-skill s))))
|
||||
nil))
|
||||
157
library/tui-client.lisp
Normal file
157
library/tui-client.lisp
Normal file
@@ -0,0 +1,157 @@
|
||||
(in-package :cl-user)
|
||||
(defpackage :opencortex.tui (:use :cl :croatoan) (:export :main))
|
||||
(in-package :opencortex.tui)
|
||||
|
||||
(defvar *daemon-host* "127.0.0.1")
|
||||
(defvar *daemon-port* 9105)
|
||||
(defvar *socket* nil)
|
||||
(defvar *stream* nil)
|
||||
|
||||
(defvar *chat-history* nil "A list of strings representing the scrollback buffer.")
|
||||
(defvar *input-buffer* (make-array 0 :element-type 'character :fill-pointer 0 :adjustable t))
|
||||
(defvar *is-running* t)
|
||||
(defvar *status-text* "Connecting...")
|
||||
|
||||
(defvar *msg-queue* nil)
|
||||
(defvar *queue-lock* (bt:make-lock "tui-msg-lock"))
|
||||
|
||||
(defun enqueue-msg (msg)
|
||||
(bt:with-lock-held (*queue-lock*) (push msg *msg-queue*)))
|
||||
|
||||
(defun dequeue-msgs ()
|
||||
(bt:with-lock-held (*queue-lock*) (let ((m (reverse *msg-queue*))) (setf *msg-queue* nil) m)))
|
||||
|
||||
(defun clean-keywords (msg)
|
||||
"Ensures all keys in a plist are uppercase keywords."
|
||||
(if (listp msg)
|
||||
(let ((clean nil))
|
||||
(loop for (k v) on msg by #'cddr
|
||||
do (push (intern (string k) :keyword) clean)
|
||||
(push v clean))
|
||||
(nreverse clean))
|
||||
msg))
|
||||
|
||||
(defun format-payload (payload)
|
||||
"Extracts human-readable text from a protocol payload, handling nested tool calls."
|
||||
(let* ((action (getf payload :ACTION))
|
||||
(text (getf payload :TEXT))
|
||||
(msg (getf payload :MESSAGE))
|
||||
(tool (getf payload :TOOL))
|
||||
(prompt (getf payload :PROMPT))
|
||||
(args (getf payload :ARGS))
|
||||
(result (getf payload :RESULT)))
|
||||
(cond (text text)
|
||||
(msg msg)
|
||||
((eq action :MESSAGE) (getf payload :TEXT))
|
||||
((and tool prompt) (format nil "THOUGHT [~a]: ~a" tool prompt))
|
||||
((and tool args)
|
||||
(let ((inner-prompt (or (getf args :PROMPT) (getf args :TEXT))))
|
||||
(if inner-prompt
|
||||
(format nil "THOUGHT [~a]: ~a" tool inner-prompt)
|
||||
(format nil "CALL [~a] (ARGS: ~s)" tool args))))
|
||||
(result (format nil "RESULT: ~a" result))
|
||||
(t (format nil "~s" payload)))))
|
||||
|
||||
(defun listen-thread ()
|
||||
(loop while *is-running* do
|
||||
(handler-case
|
||||
(when (and *stream* (open-stream-p *stream*))
|
||||
(let ((raw-msg (opencortex:read-framed-message *stream*)))
|
||||
(unless (member raw-msg '(:eof :error))
|
||||
(let* ((msg (clean-keywords raw-msg))
|
||||
(type (or (getf msg :TYPE) (getf msg :type)))
|
||||
(payload (or (getf msg :PAYLOAD) (getf msg :payload))))
|
||||
(cond ((and (listp msg) (eq type :EVENT))
|
||||
(let ((action (or (getf payload :ACTION) (getf payload :action)))
|
||||
(text (or (getf payload :TEXT) (getf payload :text) (getf payload :MESSAGE) (getf payload :message))))
|
||||
(cond ((eq action :handshake) (setf *status-text* "Ready"))
|
||||
(text (enqueue-msg (format nil "SYSTEM: ~a" text))))))
|
||||
((and (listp msg) (eq type :STATUS))
|
||||
(setf *status-text* (format nil "[Scribe: ~a] [Gardener: ~a]"
|
||||
(or (getf msg :SCRIBE) (getf msg :scribe))
|
||||
(or (getf msg :GARDENER) (getf msg :gardener)))))
|
||||
((and (listp msg) (member type '(:REQUEST :RESPONSE :LOG)))
|
||||
(let ((formatted (format-payload payload)))
|
||||
(when formatted (enqueue-msg formatted))))
|
||||
((and (listp msg) (eq type :EVENT) (eq (getf payload :SENSOR) :TOOL-OUTPUT))
|
||||
(let ((formatted (format-payload payload)))
|
||||
(when formatted (enqueue-msg formatted))))
|
||||
(t (harness-log "TUI: Ignored unknown type ~a" type)))))
|
||||
(when (eq raw-msg :eof) (setf *is-running* nil))
|
||||
(when (eq raw-msg :error) (setf *status-text* "Protocol Error"))))
|
||||
(error (c) (setf *status-text* (format nil "Net Error: ~a" c)) (setf *is-running* nil)))
|
||||
(sleep 0.05)))
|
||||
|
||||
(defun main ()
|
||||
"Primary entry point for the standalone TUI client."
|
||||
(handler-case
|
||||
(setf *socket* (usocket:socket-connect *daemon-host* *daemon-port*))
|
||||
(error (e) (format t "Error connecting: ~a~%" e) (return-from main)))
|
||||
(setf *stream* (usocket:socket-stream *socket*))
|
||||
(bt:make-thread #'listen-thread :name "tui-listener")
|
||||
|
||||
(unwind-protect
|
||||
(with-screen (scr :input-echoing nil :input-blocking nil :enable-colors t :cursor-visible t)
|
||||
(let* ((h (height scr))
|
||||
(w (width scr))
|
||||
(chat-win (make-instance 'window :height (- h 2) :width w :position (list 0 0)))
|
||||
(status-win (make-instance 'window :height 1 :width w :position (list (- h 2) 0)))
|
||||
(input-win (make-instance 'window :height 1 :width w :position (list (- h 1) 0)))
|
||||
(last-status nil))
|
||||
|
||||
(setf (function-keys-enabled-p input-win) t)
|
||||
(setf (input-blocking input-win) nil)
|
||||
|
||||
(loop while *is-running* do
|
||||
;; 1. Handle incoming messages from the queue
|
||||
(let ((new-msgs (dequeue-msgs)))
|
||||
(when new-msgs
|
||||
(dolist (msg new-msgs)
|
||||
(push msg *chat-history*)
|
||||
;; Maintenance: Cap scrollback to prevent memory bloat
|
||||
(setf *chat-history* (subseq *chat-history* 0 (min (length *chat-history*) 500))))
|
||||
|
||||
(clear chat-win)
|
||||
(let ((line-num 0))
|
||||
(dolist (m (reverse (subseq *chat-history* 0 (min (length *chat-history*) (- h 3)))))
|
||||
(add-string chat-win m :y line-num :x 0)
|
||||
(incf line-num)))
|
||||
(refresh chat-win)))
|
||||
|
||||
;; 2. Render Status Bar
|
||||
(unless (equal *status-text* last-status)
|
||||
(clear status-win)
|
||||
(add-string status-win *status-text* :attributes '(:reverse))
|
||||
(refresh status-win)
|
||||
(setf last-status *status-text*))
|
||||
|
||||
;; 3. Handle Keyboard Input
|
||||
(let* ((event (get-wide-event input-win))
|
||||
(ch (and event (typep event 'event) (event-key event))))
|
||||
(when ch
|
||||
(cond
|
||||
((or (eq ch #\Newline) (eq ch #\Return))
|
||||
(let ((cmd (coerce *input-buffer* 'string)))
|
||||
(setf (fill-pointer *input-buffer*) 0)
|
||||
(when (> (length cmd) 0)
|
||||
;; Frame and dispatch the message
|
||||
(let ((framed (opencortex:frame-message (list :TYPE :EVENT
|
||||
:META (list :SOURCE :tui :SESSION-ID "default")
|
||||
:PAYLOAD (list :SENSOR :user-input :TEXT cmd)))))
|
||||
(format *stream* "~a" framed)
|
||||
(finish-output *stream*)))
|
||||
(when (string= cmd "/exit") (setf *is-running* nil))))
|
||||
((or (eq ch :backspace) (eq ch #\Backspace) (eq ch #\Rubout) (eq ch #\Del))
|
||||
(when (> (length *input-buffer*) 0)
|
||||
(decf (fill-pointer *input-buffer*))))
|
||||
((characterp ch)
|
||||
(vector-push-extend ch *input-buffer*))))
|
||||
|
||||
(clear input-win)
|
||||
(add-string input-win (concatenate 'string "> " (coerce *input-buffer* 'string)))
|
||||
(move input-win 0 (+ 2 (length *input-buffer*)))
|
||||
(refresh input-win))
|
||||
|
||||
(sleep 0.02))))
|
||||
(setf *is-running* nil)
|
||||
(when *socket* (usocket:socket-close *socket*))))
|
||||
Reference in New Issue
Block a user