Files
passepartout/harness/act.org
Amr Gharbeia 2e8e79a193
Some checks failed
Deploy-Agent-V15-Stdin / JOB-V15-STDIN (push) Failing after 2s
fix(v0.2.0): finalize structural integrity and clean boot
- Fixed memory.org source blocks to ensure persistence functions are tangled.
- Improved extract-tangle-target to handle complex Elisp expressions.
- Corrected opencortex.sh initialization paths to prevent setup loops.
- Reordered variable definitions in policy and standards skills to eliminate forward-reference warnings.
2026-04-27 18:54:18 -04:00

16 KiB

Stage 3: Act (act.lisp)

Stage 3: Act (act.lisp)

Architectural Intent: The Last Mile

The Act stage is where cognition meets reality. After the Probabilistic engine proposes and the Deterministic engine verifies, Act executes the approved action.

The key insight of the Act stage is that execution is the point of no return. Once a command is sent to the shell or a file is written, side effects have occurred. Therefore, Act implements a "last-mile" safety check - even after skills have verified the action, there's a final validation before dispatch.

Why Separate Actuators?

The actuator pattern decouples what to do from how to do it:

  • The reasoning engine generates action plists like `(:TYPE :REQUEST :TARGET :SHELL :PAYLOAD …)`
  • The actuator interprets the target and executes appropriately
  • Adding a new actuator (Telegram, Matrix, etc.) doesn't require changing the reasoning code

This follows the Open/Closed principle: open for extension, closed for modification.

The Feedback Loop

Act is unique in the pipeline because it can generate new signals. When a tool executes and returns data, that data becomes a new signal that feeds back into Perceive → Reason → Act.

Example feedback chain:

  1. User asks "What files changed today?"
  2. Reason generates shell command action
  3. Act executes shell, gets file list
  4. Act returns file list as feedback signal
  5. Reason processes file list, generates human-readable response
  6. Act displays response

Package Context

(in-package :opencortex)

Actuator Configuration

Actuator Registry Variables

(defvar *default-actuator* :cli
  "The actuator used when no explicit target is specified.
  Override with DEFAULT_ACTUATOR environment variable.")

(defvar *silent-actuators* '(:cli :system-message :emacs)
  "List of actuators that don't generate tool-output feedback.
  These typically have their own feedback mechanisms (CLI prints directly, etc.)")

initialize-actuators: System Bootstrap

(defun initialize-actuators ()
  "Load actuator configuration from environment and register core actuators.

  Environment variables:
  - DEFAULT_ACTUATOR: Keyword for default target (:cli, :shell, etc.)
  - SILENT_ACTUATORS: Comma-separated list of actuators that skip feedback

  Registers three core actuators:
  1. :system - Internal commands (eval, create-skill, message)
  2. :tool - Cognitive tool execution
  3. :tui - Terminal UI output via reply stream"

  ;; Load environment configuration
  (let ((def (uiop:getenv "DEFAULT_ACTUATOR"))
        (silent (uiop:getenv "SILENT_ACTUATORS")))

    ;; Set default actuator
    (when def
      (setf *default-actuator*
            (intern (string-upcase def) "KEYWORD")))

    ;; Parse silent actuators list
    (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)

  ;; TUI actuator: sends response back through the reply stream
  (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))))))

Action Dispatching

dispatch-action: The Router

(defun dispatch-action (action context)
  "Route an approved action to its registered actuator.

  ACTION is a plist with structure:
    (:TYPE :REQUEST :TARGET :shell :PAYLOAD (...))

  CONTEXT is the signal being processed (for metadata access)

  The target is resolved in order of priority:
  1. Explicit :target in the action
  2. :source from the original signal's metadata
  3. *default-actuator* configuration variable

  Returns the actuator's result (may be a feedback signal or NIL)."

  (let ((payload (proto-get action :payload)))

    ;; Heartbeats don't generate actuation
    (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*)))

        ;; Preserve metadata in outbound action
        (when (and meta (null (getf action :meta)))
          (setf (getf action :meta) meta))

        ;; Execute or log error
        (if actuator-fn
            (funcall actuator-fn action context)
            (harness-log "ACT ERROR: No actuator registered for '~s' (requested by ~s)"
                         target raw-target))))))

Actuator Implementations

execute-system-action: Internal Commands

(defun execute-system-action (action context)
  "Execute internal harness commands.

  This actuator handles meta-commands that affect the harness itself,
  rather than external side effects. Commands include:

  - :eval - Evaluate arbitrary Lisp code (DANGEROUS, validate first!)
  - :create-skill - Write a new skill org file and reload
  - :message - Log a message to the harness log

  These commands bypass the normal actuator system since they operate
  on the harness internals rather than external systems."

  (declare (ignore context))

  (let* ((payload (ignore-errors (getf action :payload)))
         (cmd (ignore-errors (getf payload :action))))

    (case cmd
      ;; Evaluate Lisp code - guarded by lisp-utils skill
      (:eval
       (let ((code (getf payload :code)))
         (eval (read-from-string code))))

      ;; Create and load a new skill from content
      (: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)))

      ;; Log an informational message
      (:message
       (harness-log "ACT [System]: ~a" (getf payload :text)))

      ;; Unknown command
      (t
       (harness-log "ACT ERROR [System]: Unknown command '~s'" cmd)))))

execute-tool-action: Cognitive Tool Execution

(defun execute-tool-action (action context)
  "Execute a registered cognitive tool.

  Tools are registered functions with:
  - A guard function (optional, for safety checks)
  - A body function (the actual implementation)
  - Metadata (description, parameter specs)

  This actuator:
  1. Looks up the tool by name
  2. Runs the guard function (if present)
  3. Executes the body function with parsed arguments
  4. Returns a feedback signal with the result

  The feedback mechanism allows tool results to trigger further reasoning."

  (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
            ;; Parse arguments (handle both flat and nested plists)
            (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)))

              ;; Format result for source
              (when source
                (dispatch-action (list :TYPE :REQUEST
                                     :TARGET source
                                     :PAYLOAD (list :ACTION :MESSAGE
                                                  :TEXT (format-tool-result tool-name result)))
                              context))

              ;; Return feedback signal for potential further processing
              (list :TYPE :EVENT
                    :DEPTH (1+ depth)
                    :META meta
                    :PAYLOAD (list :SENSOR :tool-output
                                 :RESULT result
                                 :TOOL tool-name)))

          ;; Tool execution error
          (error (c)
            (list :TYPE :EVENT
                  :DEPTH (1+ depth)
                  :META meta
                  :PAYLOAD (list :SENSOR :tool-error
                               :TOOL tool-name
                               :MESSAGE (format nil "~a" c)))))

        ;; Tool not found
        (list :TYPE :EVENT
              :DEPTH (1+ depth)
              :META meta
              :PAYLOAD (list :SENSOR :tool-error
                            :MESSAGE (format nil "Tool '~a' not found" tool-name))))))

format-tool-result: Human-Readable Output

(defun format-tool-result (tool-name result)
  "Format a tool result for human-readable display.

  Tools return either:
  - A plist: (:status :success :content \"...\") or (:status :error :message \"...\")
  - A raw value (string, number, etc.)

  This function normalizes both formats into a consistent string presentation."

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

The Act Gate

act-gate: Final Pipeline Stage

(defun act-gate (signal)
  "Final stage of the metabolic pipeline: Actuation.

  This stage has three responsibilities:

  1. Last-mile safety check: Run deterministic gates one more time
     before execution (handles race conditions, concurrent modifications)

  2. Actuation: Dispatch the approved action to its target actuator

  3. Feedback generation: If the action produced results, create a
     feedback signal that feeds back into the pipeline

  Modifies the signal:
  - :approved-action - May be modified by last-mile verification
  - :status - Set to :acted

  Returns a feedback signal if the action produced results, otherwise NIL."

  (let* ((approved (getf signal :approved-action))
         (type (getf signal :type))
         (meta (getf signal :meta))
         (source (getf meta :source))
         (feedback nil)
         (context signal))

    ;; Step 1: Last-mile deterministic verification
    ;; This catches any issues that arose between reasoning and acting
    (when approved
      (let* ((original-type (getf approved :type))
             (verified (deterministic-verify approved signal)))

        ;; Check if deterministic verification blocked the action
        (if (and (listp verified)
                 (member (getf verified :type) '(:LOG :EVENT :log :event))
                 (not (member original-type '(:LOG :EVENT :log :event))))

            ;; Action was blocked by verification
            (progn
              (harness-log "ACT BLOCKED: Action failed last-mile deterministic check.")
              (setf (getf signal :approved-action) nil)
              (setf approved nil)
              (setf feedback verified))

            ;; Action passed verification
            (progn
              (setf (getf signal :approved-action) verified)
              (setf approved verified)))))

    ;; Step 2: Actuation based on signal type
    (case type
      ;; Explicit requests go directly to dispatch
      (:REQUEST
       (dispatch-action signal context))

      ;; Log messages also dispatch
      (:LOG
       (dispatch-action signal context))

      ;; Events with approved actions dispatch to their target
      (:EVENT
       (if approved
           (let* ((target (getf approved :target))
                  (result (dispatch-action approved context)))

             ;; Determine feedback based on actuator response
             (cond
               ;; Actuator returned a signal - use it as feedback
               ((and (listp result)
                     (member (getf result :type) '(:EVENT :LOG)))
                (setf feedback result))

               ;; Non-silent actuator with result - format as tool-output
               ((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))))))

           ;; No approved action, but have source - might be raw event
           (when source
             (dispatch-action signal context)))))

    ;; Step 3: Update signal status
    (setf (getf signal :status) :acted)
    feedback))

Test Suite

These tests verify the Act pipeline. Run with: (fiveam:run! 'pipeline-act-suite)

(defpackage :opencortex-pipeline-act-tests
  (:use :cl :fiveam :opencortex)
  (:export #:pipeline-act-suite))

(in-package :opencortex-pipeline-act-tests)

(def-suite pipeline-act-suite
  :description "Test suite for Act pipeline")

(in-suite pipeline-act-suite)

(test test-act-gate-symbolic-guard-bypass
  "Verify that act-gate proceeds normally when no skill intercepts."
  (clrhash opencortex::*skills-registry*)
  (let* ((signal (list :type :EVENT :status nil :depth 0 :approved-action '(:target :cli :payload (:text "Hello"))))
         (result (opencortex:act-gate signal)))
    (is (eq :acted (getf signal :status)))
    (is (null result))))

(test test-act-gate-symbolic-guard-interception
  "Verify that act-gate intercepts actions when a skill returns a LOG/EVENT."
  (clrhash opencortex::*skills-registry*)
  (opencortex::defskill :mock-bouncer
    :priority 200
    :trigger (lambda (ctx) (declare (ignore ctx)) t)
    :deterministic (lambda (action ctx)
                     (declare (ignore action ctx))
                     (list :type :LOG :payload (list :text "BLOCKED BY SYMBOLIC GUARD"))))
  (let* ((signal (list :type :EVENT :status nil :depth 0 :approved-action '(:target :shell :payload (:cmd "ls"))))
         (result (opencortex:act-gate signal)))
    (is (eq :acted (getf signal :status)))
    (is (not (null result)))
    (is (eq :LOG (getf result :type)))
    (let ((msg (getf (getf result :payload) :text)))
      (is (search "BLOCKED BY SYMBOLIC GUARD" msg)))))